Add record page layout tabs editing (#18702)

- Fix duplication of tabs in dashboards that didn't open the duplicated
tab in the side panel: replaced the logic that used Math.round with an
algorithm that switches the positions of the elements. We will keep
relying on integers, but it will work well in all cases.
- Make dnd work with record page layouts, where there is a pinned tab
- Make tab movements work with pinned tab, too
- Allow user to set a tab as the pinned tab
- Make backend changes to be able to save tabs with `layoutMode`



https://github.com/user-attachments/assets/ce1130fa-71df-49ba-ba2f-6f971e15dd49

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Baptiste Devessier
2026-03-18 15:48:23 +01:00
committed by GitHub
parent f2fa958f20
commit fe4512f80c
77 changed files with 1153 additions and 662 deletions
@@ -2,8 +2,10 @@ import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-c
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
export const CancelDashboardSingleRecordCommand = () => {
const { selectedRecords } = useMountedEngineCommandContext();
@@ -15,13 +17,25 @@ export const CancelDashboardSingleRecordCommand = () => {
const pageLayoutId = selectedRecord.pageLayoutId;
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
pageLayoutId: pageLayoutId ?? '',
layoutType: PageLayoutType.DASHBOARD,
targetRecordIdentifier: {
id: selectedRecord.id,
targetObjectNameSingular: '',
},
});
const { closeSidePanelMenu } = useSidePanelMenu();
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
const { resetDraftPageLayoutToPersistedPageLayout } =
useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
useResetDraftPageLayoutToPersistedPageLayout({
pageLayoutId,
tabListInstanceId,
});
const handleExecute = () => {
closeSidePanelMenu();
@@ -1,10 +1,13 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow';
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
export const CancelRecordPageLayoutSingleRecordCommand = () => {
const { objectMetadataItem } = useMountedEngineCommandContext();
@@ -15,17 +18,31 @@ export const CancelRecordPageLayoutSingleRecordCommand = () => {
);
}
const recordId = useSelectedRecordIdOrThrow();
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
targetObjectNameSingular: objectMetadataItem.nameSingular,
});
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
pageLayoutId,
layoutType: PageLayoutType.RECORD_PAGE,
targetRecordIdentifier: {
id: recordId,
targetObjectNameSingular: objectMetadataItem.nameSingular,
},
});
const { closeSidePanelMenu } = useSidePanelMenu();
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
const { resetDraftPageLayoutToPersistedPageLayout } =
useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
useResetDraftPageLayoutToPersistedPageLayout({
pageLayoutId,
tabListInstanceId,
});
const handleExecute = () => {
closeSidePanelMenu();