diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuDashboardPageLayoutInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuDashboardPageLayoutInfo.tsx
new file mode 100644
index 0000000000..195cf53a52
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuDashboardPageLayoutInfo.tsx
@@ -0,0 +1,13 @@
+import { CommandMenuPageLayoutInfoContent } from '@/command-menu/components/CommandMenuPageLayoutInfoContent';
+import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
+import { isDefined } from 'twenty-shared/utils';
+
+export const CommandMenuDashboardPageLayoutInfo = () => {
+ const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
+
+ if (!isDefined(pageLayoutId)) {
+ return null;
+ }
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx
index 72aa472f9f..531dea39bf 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx
@@ -55,6 +55,8 @@ export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => {
CommandMenuPages.PageLayoutGraphFilter,
CommandMenuPages.PageLayoutIframeSettings,
CommandMenuPages.PageLayoutTabSettings,
+ CommandMenuPages.PageLayoutFieldsSettings,
+ CommandMenuPages.PageLayoutFieldsLayout,
].includes(pageChip.page?.page)
: false;
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx
index d2b9c127f5..27964aae7e 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx
@@ -1,144 +1,30 @@
-import { usePageLayoutHeaderInfo } from '@/command-menu/components/hooks/usePageLayoutHeaderInfo';
-import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
-import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
-import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
-import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
-import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
-import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
-import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab';
-import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
-import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
-import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
-import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
-import { TitleInput } from '@/ui/input/components/TitleInput';
-import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
+import { CommandMenuDashboardPageLayoutInfo } from '@/command-menu/components/CommandMenuDashboardPageLayoutInfo';
+import { CommandMenuRecordPageLayoutInfo } from '@/command-menu/components/CommandMenuRecordPageLayoutInfo';
+import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
+import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
-import { useTheme } from '@emotion/react';
-import { isNonEmptyString } from '@sniptt/guards';
-import { useState } from 'react';
-import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
-import { useIcons } from 'twenty-ui/display';
-import { CommandMenuPageInfoLayout } from './CommandMenuPageInfoLayout';
export const CommandMenuPageLayoutInfo = () => {
- const theme = useTheme();
- const { getIcon } = useIcons();
- const commandMenuPage = useRecoilValue(commandMenuPageState);
- const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
- const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
-
- const [shouldFocusTitleInput, setShouldFocusTitleInput] =
- useRecoilComponentState(
- commandMenuShouldFocusTitleInputComponentState,
- commandMenuPageInfo.instanceId,
- );
-
- const handleTitleInputOpen = () => {
- setShouldFocusTitleInput(false);
- };
-
- const draftPageLayout = useRecoilComponentValue(
- pageLayoutDraftComponentState,
- pageLayoutId,
+ const objectMetadataId = useRecoilComponentValue(
+ contextStoreCurrentObjectMetadataItemIdComponentState,
);
- const pageLayoutEditingWidgetId = useRecoilComponentValue(
- pageLayoutEditingWidgetIdComponentState,
- pageLayoutId,
- );
-
- const [openTabId] = useRecoilComponentState(
- pageLayoutTabSettingsOpenTabIdComponentState,
- pageLayoutId,
- );
-
- const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
- const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
- const { updatePageLayoutTab } = useUpdatePageLayoutTab(pageLayoutId);
-
- const [editedTitle, setEditedTitle] = useState(null);
-
- const headerInfo = usePageLayoutHeaderInfo({
- commandMenuPage,
- draftPageLayout,
- pageLayoutEditingWidgetId,
- openTabId,
- editedTitle,
- });
-
- if (!headerInfo) {
- return null;
+ if (!isDefined(objectMetadataId)) {
+ throw new Error('Object metadata ID is not defined');
}
- const {
- headerIcon,
- headerIconColor,
- headerType,
- title,
- isReadonly,
- tab,
- widgetInEditMode,
- } = headerInfo;
+ const { objectMetadataItem } = useObjectMetadataItemById({
+ objectId: objectMetadataId,
+ });
- const Icon = headerIcon ?? getIcon('IconDefault');
+ const isDashboardContext =
+ objectMetadataItem.nameSingular === CoreObjectNameSingular.Dashboard;
- const handleTitleChange = (newTitle: string) => {
- setEditedTitle(newTitle);
- };
+ if (isDashboardContext) {
+ return ;
+ }
- const saveTitle = async () => {
- const finalTitle = editedTitle ?? title;
-
- if (!isNonEmptyString(finalTitle)) {
- return;
- }
-
- updateCommandMenuPageInfo({
- pageTitle: finalTitle,
- pageIcon: Icon,
- });
-
- if (
- commandMenuPage === CommandMenuPages.PageLayoutTabSettings &&
- isDefined(tab)
- ) {
- updatePageLayoutTab(tab.id, { title: finalTitle });
- } else if (isDefined(widgetInEditMode)) {
- updatePageLayoutWidget(widgetInEditMode.id, {
- title: finalTitle,
- });
- }
-
- setEditedTitle(null);
- };
-
- return (
-
- ) : undefined
- }
- iconColor={headerIconColor}
- title={
- setEditedTitle(null)}
- onClickOutside={saveTitle}
- onTab={saveTitle}
- onShiftTab={saveTitle}
- shouldFocus={shouldFocusTitleInput}
- onFocus={handleTitleInputOpen}
- />
- }
- label={headerType}
- />
- );
+ return ;
};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfoContent.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfoContent.tsx
new file mode 100644
index 0000000000..569f4bc4a5
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfoContent.tsx
@@ -0,0 +1,146 @@
+import { usePageLayoutHeaderInfo } from '@/command-menu/components/hooks/usePageLayoutHeaderInfo';
+import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
+import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
+import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
+import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
+import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab';
+import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
+import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
+import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
+import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
+import { TitleInput } from '@/ui/input/components/TitleInput';
+import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+import { useTheme } from '@emotion/react';
+import { isNonEmptyString } from '@sniptt/guards';
+import { useState } from 'react';
+import { useRecoilValue } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
+import { useIcons } from 'twenty-ui/display';
+import { CommandMenuPageInfoLayout } from './CommandMenuPageInfoLayout';
+
+export const CommandMenuPageLayoutInfoContent = ({
+ pageLayoutId,
+}: {
+ pageLayoutId: string;
+}) => {
+ const theme = useTheme();
+ const { getIcon } = useIcons();
+ const commandMenuPage = useRecoilValue(commandMenuPageState);
+ const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
+
+ const [shouldFocusTitleInput, setShouldFocusTitleInput] =
+ useRecoilComponentState(
+ commandMenuShouldFocusTitleInputComponentState,
+ commandMenuPageInfo.instanceId,
+ );
+
+ const handleTitleInputOpen = () => {
+ setShouldFocusTitleInput(false);
+ };
+
+ const draftPageLayout = useRecoilComponentValue(
+ pageLayoutDraftComponentState,
+ pageLayoutId,
+ );
+
+ const pageLayoutEditingWidgetId = useRecoilComponentValue(
+ pageLayoutEditingWidgetIdComponentState,
+ pageLayoutId,
+ );
+
+ const [openTabId] = useRecoilComponentState(
+ pageLayoutTabSettingsOpenTabIdComponentState,
+ pageLayoutId,
+ );
+
+ const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
+ const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
+ const { updatePageLayoutTab } = useUpdatePageLayoutTab(pageLayoutId);
+
+ const [editedTitle, setEditedTitle] = useState(null);
+
+ const headerInfo = usePageLayoutHeaderInfo({
+ commandMenuPage,
+ draftPageLayout,
+ pageLayoutEditingWidgetId,
+ openTabId,
+ editedTitle,
+ });
+
+ if (!headerInfo) {
+ return null;
+ }
+
+ const {
+ headerIcon,
+ headerIconColor,
+ headerType,
+ title,
+ isReadonly,
+ tab,
+ widgetInEditMode,
+ } = headerInfo;
+
+ const Icon = headerIcon ?? getIcon('IconDefault');
+
+ const handleTitleChange = (newTitle: string) => {
+ setEditedTitle(newTitle);
+ };
+
+ const saveTitle = async () => {
+ const finalTitle = editedTitle ?? title;
+
+ if (!isNonEmptyString(finalTitle)) {
+ return;
+ }
+
+ updateCommandMenuPageInfo({
+ pageTitle: finalTitle,
+ pageIcon: Icon,
+ });
+
+ if (
+ commandMenuPage === CommandMenuPages.PageLayoutTabSettings &&
+ isDefined(tab)
+ ) {
+ updatePageLayoutTab(tab.id, { title: finalTitle });
+ } else if (isDefined(widgetInEditMode)) {
+ updatePageLayoutWidget(widgetInEditMode.id, {
+ title: finalTitle,
+ });
+ }
+
+ setEditedTitle(null);
+ };
+
+ return (
+
+ ) : undefined
+ }
+ iconColor={headerIconColor}
+ title={
+ setEditedTitle(null)}
+ onClickOutside={saveTitle}
+ onTab={saveTitle}
+ onShiftTab={saveTitle}
+ shouldFocus={shouldFocusTitleInput}
+ onFocus={handleTitleInputOpen}
+ />
+ }
+ label={headerType}
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordPageLayoutInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordPageLayoutInfo.tsx
new file mode 100644
index 0000000000..6604f8f222
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordPageLayoutInfo.tsx
@@ -0,0 +1,9 @@
+import { CommandMenuPageLayoutInfoContent } from '@/command-menu/components/CommandMenuPageLayoutInfoContent';
+import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
+
+export const CommandMenuRecordPageLayoutInfo = () => {
+ const { pageLayoutId } =
+ usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts b/packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts
index 2776e6f188..3a8df0a0a1 100644
--- a/packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts
+++ b/packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts
@@ -10,6 +10,7 @@ import { isDefined } from 'twenty-shared/utils';
import {
IconAppWindow,
IconFrame,
+ IconList,
IconPlus,
type IconComponent,
} from 'twenty-ui/display';
@@ -149,6 +150,37 @@ export const usePageLayoutHeaderInfo = ({
};
}
+ case CommandMenuPages.PageLayoutFieldsSettings:
+ case CommandMenuPages.PageLayoutFieldsLayout: {
+ if (!isDefined(pageLayoutEditingWidgetId)) {
+ return null;
+ }
+
+ const widgetInEditMode = draftPageLayout.tabs
+ .flatMap((tab) => tab.widgets)
+ .find((widget) => widget.id === pageLayoutEditingWidgetId);
+
+ if (!isDefined(widgetInEditMode)) {
+ return null;
+ }
+
+ const title = isDefined(editedTitle)
+ ? editedTitle
+ : isDefined(widgetInEditMode.title) && widgetInEditMode.title !== ''
+ ? widgetInEditMode.title
+ : '';
+
+ return {
+ headerIcon: IconList,
+ headerIconColor: iconColor,
+ headerType: t`Fields Widget`,
+ title,
+ isReadonly: false,
+ tab: undefined,
+ widgetInEditMode,
+ };
+ }
+
case CommandMenuPages.PageLayoutWidgetTypeSelect: {
return {
headerIcon: IconPlus,
diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx
index 8f55e9528e..0fb940a277 100644
--- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx
+++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx
@@ -5,6 +5,8 @@ import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-even
import { CommandMenuFrontComponentPage } from '@/command-menu/pages/front-component/components/CommandMenuFrontComponentPage';
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
import { CommandMenuPageLayoutChartSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings';
+import { CommandMenuPageLayoutFieldsLayout } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout';
+import { CommandMenuPageLayoutFieldsSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings';
import { CommandMenuPageLayoutGraphFilter } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphFilter';
import { CommandMenuPageLayoutIframeSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings';
import { CommandMenuPageLayoutTabSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutTabSettings';
@@ -65,5 +67,13 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
CommandMenuPages.PageLayoutTabSettings,
,
],
+ [
+ CommandMenuPages.PageLayoutFieldsSettings,
+ ,
+ ],
+ [
+ CommandMenuPages.PageLayoutFieldsLayout,
+ ,
+ ],
[CommandMenuPages.ViewFrontComponent, ],
]);
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings.tsx
index 491cb25feb..817d9e3626 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings.tsx
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings.tsx
@@ -42,7 +42,7 @@ export const CommandMenuPageLayoutChartSettings = () => {
value={{ instanceId: widgetInEditMode.id }}
>
-
+
);
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout.tsx
new file mode 100644
index 0000000000..0fafed4278
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout.tsx
@@ -0,0 +1,65 @@
+import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
+import { SidePanelSubPageNavigationHeader } from '@/command-menu/pages/common/components/SidePanelSubPageNavigationHeader';
+import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
+import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
+import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration';
+import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
+import { FieldsConfigurationEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationEditor';
+import styled from '@emotion/styled';
+import { t } from '@lingui/core/macro';
+import { useState } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+
+const StyledOuterContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+`;
+
+const StyledContainer = styled.div`
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(3)};
+ padding: ${({ theme }) => theme.spacing(2)};
+ overflow-y: auto;
+`;
+
+export const CommandMenuPageLayoutFieldsLayout = () => {
+ const { goBackFromCommandMenu } = useCommandMenuHistory();
+
+ const { pageLayoutId, objectNameSingular } =
+ usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();
+
+ const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
+ const defaultFieldsConfiguration =
+ useTemporaryFieldsConfiguration(objectNameSingular);
+ const [fieldsConfiguration, setFieldsConfiguration] =
+ useState(defaultFieldsConfiguration);
+
+ if (!isDefined(widgetInEditMode)) {
+ return null;
+ }
+
+ const handleConfigurationChange = (
+ updatedConfiguration: FieldsConfiguration,
+ ) => {
+ // TODO: replace with a call to updatePageLayoutWidget
+ setFieldsConfiguration(updatedConfiguration);
+ };
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings.tsx
new file mode 100644
index 0000000000..2ad8f8c293
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings.tsx
@@ -0,0 +1,94 @@
+import { CommandGroup } from '@/command-menu/components/CommandGroup';
+import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
+import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
+import { WidgetSettingsFooter } from '@/command-menu/pages/page-layout/components/WidgetSettingsFooter';
+import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
+import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
+import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
+import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration';
+import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
+import styled from '@emotion/styled';
+import { useLingui } from '@lingui/react/macro';
+import { isDefined } from 'twenty-shared/utils';
+import { IconLayoutSidebarRight } from 'twenty-ui/display';
+
+const StyledContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+`;
+
+const StyledCommandMenuContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ overflow: hidden;
+`;
+
+export const CommandMenuPageLayoutFieldsSettings = () => {
+ const { t } = useLingui();
+ const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
+ const { pageLayoutId, objectNameSingular } =
+ usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();
+
+ const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
+ const fieldsConfiguration =
+ useTemporaryFieldsConfiguration(objectNameSingular);
+
+ if (!isDefined(widgetInEditMode)) {
+ return null;
+ }
+
+ const totalFieldsCount = fieldsConfiguration.sections.reduce(
+ (count, section) => count + section.fields.length,
+ 0,
+ );
+
+ const handleNavigateToLayout = () => {
+ navigatePageLayoutCommandMenu({
+ commandMenuPage: CommandMenuPages.PageLayoutFieldsLayout,
+ });
+ };
+
+ const selectableItemIds = [
+ 'layout',
+ 'display-more-fields-button',
+ 'action-button',
+ 'move-down',
+ 'move-up',
+ 'move-to-tab',
+ 'add-widget-above',
+ 'add-widget-below',
+ 'delete',
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings.tsx
index b7753d147f..0e3cba156f 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings.tsx
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings.tsx
@@ -93,7 +93,7 @@ export const CommandMenuPageLayoutIframeSettings = () => {
error={urlError}
/>
-
+
);
};
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/WidgetSettingsFooter.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/WidgetSettingsFooter.tsx
index b8d3a63744..aa14fb739a 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/WidgetSettingsFooter.tsx
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/WidgetSettingsFooter.tsx
@@ -1,4 +1,3 @@
-import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
import { useDuplicatePageLayoutWidget } from '@/page-layout/hooks/useDuplicatePageLayoutWidget';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
@@ -14,12 +13,14 @@ import { isDefined } from 'twenty-shared/utils';
import { IconCopyPlus, IconTrash } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
-export const WidgetSettingsFooter = () => {
+export const WidgetSettingsFooter = ({
+ pageLayoutId,
+}: {
+ pageLayoutId: string;
+}) => {
const dropdownId = useId();
const { t } = useLingui();
const { closeDropdown } = useCloseDropdown();
-
- const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { duplicateWidget } = useDuplicatePageLayoutWidget(pageLayoutId);
const { deletePageLayoutWidget } = useDeletePageLayoutWidget(pageLayoutId);
const editingWidgetId = useRecoilComponentValue(
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord.ts
new file mode 100644
index 0000000000..5ed470a0a9
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord.ts
@@ -0,0 +1,46 @@
+import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
+import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
+import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
+import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+import { isDefined } from 'twenty-shared/utils';
+
+export const usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord =
+ () => {
+ const targetedRecordsRule = useRecoilComponentValue(
+ contextStoreTargetedRecordsRuleComponentState,
+ );
+
+ const objectMetadataId = useRecoilComponentValue(
+ contextStoreCurrentObjectMetadataItemIdComponentState,
+ );
+
+ if (!isDefined(objectMetadataId)) {
+ throw new Error('Object metadata ID is not defined');
+ }
+
+ const { objectMetadataItem } = useObjectMetadataItemById({
+ objectId: objectMetadataId ?? undefined,
+ });
+
+ if (
+ !(
+ targetedRecordsRule.mode === 'selection' &&
+ targetedRecordsRule.selectedRecordIds.length === 1
+ )
+ ) {
+ throw new Error('Only one record should be selected');
+ }
+
+ const recordId: string = targetedRecordsRule.selectedRecordIds[0];
+
+ const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
+ id: recordId,
+ targetObjectNameSingular: objectMetadataItem.nameSingular,
+ });
+
+ return {
+ pageLayoutId,
+ objectNameSingular: objectMetadataItem.nameSingular,
+ };
+ };
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage.ts
index 0000c13dcb..c0c0c5e7d3 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage.ts
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage.ts
@@ -5,4 +5,6 @@ export type PageLayoutCommandMenuPage =
| CommandMenuPages.PageLayoutGraphTypeSelect
| CommandMenuPages.PageLayoutIframeSettings
| CommandMenuPages.PageLayoutGraphFilter
- | CommandMenuPages.PageLayoutTabSettings;
+ | CommandMenuPages.PageLayoutTabSettings
+ | CommandMenuPages.PageLayoutFieldsSettings
+ | CommandMenuPages.PageLayoutFieldsLayout;
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutIcon.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutIcon.ts
index 358aca3c91..73789d114e 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutIcon.ts
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutIcon.ts
@@ -6,6 +6,8 @@ import {
IconChartPie,
IconFilter,
IconFrame,
+ IconLayoutSidebarRight,
+ IconList,
} from 'twenty-ui/display';
export const getPageLayoutIcon = (page: PageLayoutCommandMenuPage) => {
@@ -20,6 +22,10 @@ export const getPageLayoutIcon = (page: PageLayoutCommandMenuPage) => {
return IconFilter;
case CommandMenuPages.PageLayoutTabSettings:
return IconAppWindow;
+ case CommandMenuPages.PageLayoutFieldsSettings:
+ return IconList;
+ case CommandMenuPages.PageLayoutFieldsLayout:
+ return IconLayoutSidebarRight;
default:
assertUnreachable(page);
}
diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutPageTitle.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutPageTitle.ts
index 441d9f3acf..97d9d21852 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutPageTitle.ts
+++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutPageTitle.ts
@@ -15,6 +15,10 @@ export const getPageLayoutPageTitle = (page: PageLayoutCommandMenuPage) => {
return t`Configure filters`;
case CommandMenuPages.PageLayoutTabSettings:
return t`Tab Settings`;
+ case CommandMenuPages.PageLayoutFieldsSettings:
+ return t`Fields Settings`;
+ case CommandMenuPages.PageLayoutFieldsLayout:
+ return t`Layout`;
default:
assertUnreachable(page);
}
diff --git a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts
index a8250f1ff6..1af134d3f7 100644
--- a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts
+++ b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts
@@ -21,5 +21,7 @@ export enum CommandMenuPages {
PageLayoutGraphFilter = 'page-layout-graph-filter',
PageLayoutIframeSettings = 'page-layout-iframe-settings',
PageLayoutTabSettings = 'page-layout-tab-settings',
+ PageLayoutFieldsSettings = 'page-layout-fields-settings',
+ PageLayoutFieldsLayout = 'page-layout-fields-layout',
ViewFrontComponent = 'view-front-component',
}
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts
index 2656da741c..680dab921c 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts
@@ -56,6 +56,15 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
return;
}
+ if (widgetType === WidgetType.FIELDS) {
+ navigatePageLayoutCommandMenu({
+ commandMenuPage: CommandMenuPages.PageLayoutFieldsSettings,
+ pageTitle: t`Edit Fields`,
+ resetNavigationStack: true,
+ });
+ return;
+ }
+
setCommandMenuPage(CommandMenuPages.Root);
closeCommandMenu();
},
diff --git a/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts b/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts
index 1f70d2aa7c..079c75340b 100644
--- a/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts
+++ b/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts
@@ -4,6 +4,8 @@ export type FieldsConfigurationFieldItem = {
fieldMetadataId: string;
position: number;
conditionalDisplay?: RulesLogic;
+ // TODO: This property will be removed. Instead, we will only store the fields we want to display in FieldsConfiguration
+ isVisible?: boolean;
};
export type FieldsConfigurationSection = {
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
new file mode 100644
index 0000000000..08501f2236
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx
@@ -0,0 +1,56 @@
+import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
+import {
+ type FieldsConfiguration,
+ type FieldsConfigurationSection,
+} from '@/page-layout/types/FieldsConfiguration';
+import { FieldsConfigurationSectionEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor';
+
+type FieldsConfigurationEditorProps = {
+ configuration: FieldsConfiguration;
+ onChange: (configuration: FieldsConfiguration) => void;
+};
+
+export const FieldsConfigurationEditor = ({
+ configuration,
+ onChange,
+}: FieldsConfigurationEditorProps) => {
+ const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
+
+ const handleSectionChange = (
+ sectionId: string,
+ updatedSection: FieldsConfigurationSection,
+ ) => {
+ const updatedSections = configuration.sections.map((section) =>
+ section.id === sectionId ? updatedSection : section,
+ );
+
+ onChange({
+ ...configuration,
+ sections: updatedSections,
+ });
+ };
+
+ const sortedSections = [...configuration.sections].sort(
+ (a, b) => a.position - b.position,
+ );
+
+ if (sortedSections.length === 0) {
+ return null;
+ }
+
+ return (
+ <>
+ {sortedSections.map((section, index) => (
+
+ handleSectionChange(section.id, updatedSection)
+ }
+ />
+ ))}
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor.tsx
new file mode 100644
index 0000000000..f51f37d10c
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor.tsx
@@ -0,0 +1,39 @@
+import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { type FieldsConfigurationFieldItem } from '@/page-layout/types/FieldsConfiguration';
+import { IconEye, IconEyeOff, useIcons } from 'twenty-ui/display';
+import { MenuItemDraggable } from 'twenty-ui/navigation';
+
+type FieldsConfigurationFieldEditorProps = {
+ field: FieldsConfigurationFieldItem;
+ fieldMetadata: FieldMetadataItem;
+ index: number;
+ onToggleVisibility: () => void;
+};
+
+export const FieldsConfigurationFieldEditor = ({
+ field,
+ fieldMetadata,
+ onToggleVisibility,
+}: FieldsConfigurationFieldEditorProps) => {
+ const { getIcon } = useIcons();
+ const isVisible = field.isVisible !== false;
+ const FieldIcon = getIcon(fieldMetadata.icon);
+
+ return (
+ {
+ e.stopPropagation();
+ onToggleVisibility();
+ },
+ },
+ ]}
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor.tsx
new file mode 100644
index 0000000000..d1e97ff48b
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor.tsx
@@ -0,0 +1,112 @@
+import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import {
+ type FieldsConfigurationFieldItem,
+ type FieldsConfigurationSection,
+} from '@/page-layout/types/FieldsConfiguration';
+import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor';
+import styled from '@emotion/styled';
+import { useState } from 'react';
+import { IconDotsVertical } from 'twenty-ui/display';
+import { MenuItem } from 'twenty-ui/navigation';
+
+const StyledFieldsContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+type FieldsConfigurationSectionEditorProps = {
+ section: FieldsConfigurationSection;
+ index: number;
+ objectMetadataItem: ObjectMetadataItem;
+ onSectionChange: (section: FieldsConfigurationSection) => void;
+};
+
+export const FieldsConfigurationSectionEditor = ({
+ section,
+ objectMetadataItem,
+ onSectionChange,
+}: FieldsConfigurationSectionEditorProps) => {
+ const [isExpanded, setIsExpanded] = useState(true);
+
+ const handleFieldChange = (
+ fieldMetadataId: string,
+ updatedField: FieldsConfigurationFieldItem,
+ ) => {
+ const updatedFields = section.fields.map((field) =>
+ field.fieldMetadataId === fieldMetadataId ? updatedField : field,
+ );
+
+ onSectionChange({
+ ...section,
+ fields: updatedFields,
+ });
+ };
+
+ const handleToggleFieldVisibility = (fieldMetadataId: string) => {
+ const field = section.fields.find(
+ (f) => f.fieldMetadataId === fieldMetadataId,
+ );
+
+ if (!field) {
+ return;
+ }
+
+ const currentlyVisible = field.isVisible !== false;
+
+ const updatedField: FieldsConfigurationFieldItem = {
+ ...field,
+ isVisible: !currentlyVisible,
+ };
+
+ handleFieldChange(fieldMetadataId, updatedField);
+ };
+
+ const sortedFields = [...section.fields].sort(
+ (a, b) => a.position - b.position,
+ );
+
+ return (
+ <>
+