From 291792f864520d7bd73220ad6cbd18c5baf395ba Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Wed, 1 Apr 2026 17:24:05 +0200 Subject: [PATCH] Repair Edit Layout command menu item and add a button in settings to start edition too (#19208) https://github.com/user-attachments/assets/f23f0777-abf3-4ff4-8fab-ec2004df60bc --- ...ditRecordPageLayoutSingleRecordCommand.tsx | 22 +--- .../components/tabs/ObjectLayout.tsx | 60 ++++++++++ .../ui/layout/tab-list/components/TabList.tsx | 110 +++++++++++------- .../ui/layout/tab-list/types/TabListProps.ts | 3 + .../dimensions/components/NodeDimension.tsx | 6 +- .../data-model/SettingsObjectDetailPage.tsx | 65 ++++++----- .../constants/SettingsObjectDetailTabs.ts | 1 + ...-layout-command-menu-item-label.command.ts | 88 ++++++++++++++ .../1-21-upgrade-version-command.module.ts | 3 + .../upgrade.command.ts | 9 +- .../standard-command-menu-item.constant.ts | 2 +- 11 files changed, 277 insertions(+), 92 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-update-edit-layout-command-menu-item-label.command.ts diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx index 1a02679264..62183861fe 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx @@ -1,30 +1,14 @@ import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect'; -import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi'; -import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow'; -import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode'; -import { isDefined } from 'twenty-shared/utils'; +import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode'; import { useResetLocationHash } from 'twenty-ui/utilities'; export const EditRecordPageLayoutSingleRecordCommand = () => { - const { objectMetadataItem } = useHeadlessCommandContextApi(); - - if (!isDefined(objectMetadataItem)) { - throw new Error( - 'Object metadata item is required to edit record page layout', - ); - } - - const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({ - targetObjectNameSingular: objectMetadataItem.nameSingular, - }); - - const { setIsPageLayoutInEditMode } = - useSetIsPageLayoutInEditMode(pageLayoutId); + const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode(); const { resetLocationHash } = useResetLocationHash(); const handleExecute = () => { - setIsPageLayoutInEditMode(true); + enterLayoutCustomizationMode(); resetLocationHash(); }; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx new file mode 100644 index 0000000000..38acd439ca --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx @@ -0,0 +1,60 @@ +import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; + +import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { SettingsCard } from '@/settings/components/SettingsCard'; +import { useLingui } from '@lingui/react/macro'; +import { useContext } from 'react'; +import { AppPath } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { H2Title, IconLayoutDashboard } from 'twenty-ui/display'; +import { Section } from 'twenty-ui/layout'; +import { ThemeContext } from 'twenty-ui/theme-constants'; +import { useNavigateApp } from '~/hooks/useNavigateApp'; + +type ObjectLayoutProps = { + objectMetadataItem: EnrichedObjectMetadataItem; +}; + +export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => { + const { theme } = useContext(ThemeContext); + const { t } = useLingui(); + const navigateApp = useNavigateApp(); + const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode(); + + const { records } = useFindManyRecords({ + objectNameSingular: objectMetadataItem.nameSingular, + recordGqlFields: { id: true }, + limit: 1, + }); + + const firstRecord = records[0]; + + const handleCustomizeRecordPage = () => { + if (!isDefined(firstRecord)) { + return; + } + + enterLayoutCustomizationMode(); + + navigateApp(AppPath.RecordShowPage, { + objectNameSingular: objectMetadataItem.nameSingular, + objectRecordId: firstRecord.id, + }); + }; + + return ( +
+ + } + onClick={handleCustomizeRecordPage} + disabled={!isDefined(firstRecord)} + /> +
+ ); +}; diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab-list/components/TabList.tsx index 6412170805..e3d8d41477 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab-list/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab-list/components/TabList.tsx @@ -11,6 +11,7 @@ import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomC import { styled } from '@linaria/react'; import { useCallback, useEffect, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; +import { isDefined } from 'twenty-shared/utils'; import { TabButton } from 'twenty-ui/input'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { TabListDropdown } from './TabListDropdown'; @@ -35,6 +36,12 @@ const StyledContainer = styled.div` } `; +const StyledInnerContainer = styled.div` + display: flex; + flex: 1; + min-width: 0; +`; + const StyledDropdownContainer = styled.div` align-items: center; display: flex; @@ -48,6 +55,18 @@ const StyledTabContainer = styled.div` position: relative; `; +const StyledNodeDimension = styled(NodeDimension)` + display: flex; + flex: 1; + min-width: 0; +`; + +const StyledRightContainer = styled.div` + align-items: center; + display: flex; + margin-left: auto; +`; + export const TabList = ({ tabs, loading, @@ -56,6 +75,7 @@ export const TabList = ({ className, componentInstanceId, onChangeTab, + rightComponent, }: TabListProps) => { const visibleTabs = tabs.filter((tab) => !tab.hide); const navigate = useNavigate(); @@ -138,49 +158,55 @@ export const TabList = ({ /> )} - - - - {visibleTabs.slice(0, visibleTabCount).map((tab) => ( - onChangeTab?.(tab.id) - : () => handleTabSelect(tab.id) - } - /> - ))} - + + + + + {visibleTabs.slice(0, visibleTabCount).map((tab) => ( + onChangeTab?.(tab.id) + : () => handleTabSelect(tab.id) + } + /> + ))} + - {hasHiddenTabs && ( - - { - closeDropdown(dropdownId); - }} - overflow={{ - hiddenTabsCount, - isActiveTabHidden, - }} - hiddenTabs={hiddenTabs} - activeTabId={activeTabId || ''} - onTabSelect={handleTabSelectFromDropdown} - loading={loading} - /> - - )} - - + {hasHiddenTabs && ( + + { + closeDropdown(dropdownId); + }} + overflow={{ + hiddenTabsCount, + isActiveTabHidden, + }} + hiddenTabs={hiddenTabs} + activeTabId={activeTabId || ''} + onTabSelect={handleTabSelectFromDropdown} + loading={loading} + /> + + )} + + + + {isDefined(rightComponent) && ( + {rightComponent} + )} + ); diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabListProps.ts b/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabListProps.ts index 9d769e4fbc..f6cb77df72 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabListProps.ts +++ b/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabListProps.ts @@ -1,3 +1,5 @@ +import { type ReactNode } from 'react'; + import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps'; export type TabListProps = { @@ -8,4 +10,5 @@ export type TabListProps = { isInSidePanel?: boolean; componentInstanceId: string; onChangeTab?: (tabId: string) => void; + rightComponent?: ReactNode; }; diff --git a/packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimension.tsx b/packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimension.tsx index 08c8efc2d4..dd5a2ee285 100644 --- a/packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimension.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/dimensions/components/NodeDimension.tsx @@ -3,11 +3,13 @@ import { type ReactNode, useRef } from 'react'; type NodeDimensionProps = { children: ReactNode; + className?: string; onDimensionChange: (dimensions: { width: number; height: number }) => void; }; export const NodeDimension = ({ children, + className, onDimensionChange, }: NodeDimensionProps) => { const elementRef = useRef(null); @@ -18,7 +20,9 @@ export const NodeDimension = ({ elementRef={elementRef} onDimensionChange={onDimensionChange} /> -
{children}
+
+ {children} +
); }; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx index 396d68b4a6..75f2f22bd8 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx @@ -5,15 +5,20 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilte import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { ObjectFields } from '@/settings/data-model/object-details/components/tabs/ObjectFields'; import { ObjectIndexes } from '@/settings/data-model/object-details/components/tabs/ObjectIndexes'; +import { ObjectLayout } from '@/settings/data-model/object-details/components/tabs/ObjectLayout'; import { ObjectSettings } from '@/settings/data-model/object-details/components/tabs/ObjectSettings'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { styled } from '@linaria/react'; -import { AppPath, SettingsPath } from 'twenty-shared/types'; +import { + AppPath, + CoreObjectNameSingular, + SettingsPath, +} from 'twenty-shared/types'; import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; @@ -23,6 +28,7 @@ import { getAppPath, getSettingsPath, isDefined } from 'twenty-shared/utils'; import { IconArrowUpRight, IconCodeCircle, + IconLayout, IconListDetails, IconPlus, IconPoint, @@ -36,13 +42,6 @@ import { useNavigateApp } from '~/hooks/useNavigateApp'; import { SETTINGS_OBJECT_DETAIL_TABS } from '~/pages/settings/data-model/constants/SettingsObjectDetailTabs'; import { updatedObjectNamePluralState } from '~/pages/settings/data-model/states/updatedObjectNamePluralState'; -const StyledTabsRow = styled.div` - align-items: center; - display: flex; - justify-content: space-between; - width: 100%; -`; - const StyledContentContainer = styled.div` flex: 1; padding-left: 0; @@ -78,6 +77,9 @@ export const SettingsObjectDetailPage = () => { const isUniqueIndexesEnabled = useIsFeatureEnabled( FeatureFlagKey.IS_UNIQUE_INDEXES_ENABLED, ); + const isRecordPageLayoutEditingEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED, + ); const [isDeleting, setIsDeleting] = useState(false); @@ -112,6 +114,15 @@ export const SettingsObjectDetailPage = () => { Icon: IconSettings, hide: false, }, + { + id: SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.LAYOUT, + title: t`Layout`, + Icon: IconLayout, + hide: + !isRecordPageLayoutEditingEnabled || + objectMetadataItem.isRemote || + objectMetadataItem.nameSingular === CoreObjectNameSingular.Dashboard, + }, { id: SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.INDEXES, title: t`Indexes`, @@ -139,6 +150,8 @@ export const SettingsObjectDetailPage = () => { setIsDeleting={setIsDeleting} /> ); + case SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.LAYOUT: + return ; case SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.INDEXES: return ; default: @@ -179,23 +192,23 @@ export const SettingsObjectDetailPage = () => { } > - - -