Files
twenty/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutTab.ts
T
nitin eeca9cd42e fix(front): isolate record table dashboard widget filters on duplicate (#21936)
closes
https://discord.com/channels/1130383047699738754/1518291134382608394



https://github.com/user-attachments/assets/931e4e88-44e8-4634-a7f9-e0564bd80fff



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21936?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-22 16:27:01 +05:30

194 lines
6.4 KiB
TypeScript

import { useDuplicateFieldsWidgetForPageLayout } from '@/page-layout/hooks/useDuplicateFieldsWidgetForPageLayout';
import { useDuplicateRecordTableWidgetForPageLayout } from '@/page-layout/hooks/useDuplicateRecordTableWidgetForPageLayout';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { generateDuplicatedTimestamps } from '@/page-layout/utils/generateDuplicatedTimestamps';
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { SidePanelPages } from 'twenty-shared/types';
import { appendCopySuffix, isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
export const useDuplicatePageLayoutTab = ({
pageLayoutId: pageLayoutIdFromProps,
tabListInstanceId,
}: {
pageLayoutId: string;
tabListInstanceId: string;
}) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
);
const pageLayoutDraft = useAtomComponentStateCallbackState(
pageLayoutDraftComponentState,
pageLayoutId,
);
const pageLayoutCurrentLayouts = useAtomComponentStateCallbackState(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const store = useStore();
const setActiveTabId = useSetAtomComponentState(
activeTabIdComponentState,
tabListInstanceId,
);
const setPageLayoutTabSettingsOpenTabId = useSetAtomComponentState(
pageLayoutTabSettingsOpenTabIdComponentState,
pageLayoutId,
);
const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel();
const { closeSidePanelMenu } = useSidePanelMenu();
const { duplicateFieldsWidget } = useDuplicateFieldsWidgetForPageLayout({
pageLayoutId,
});
const { duplicateRecordTableWidget } =
useDuplicateRecordTableWidgetForPageLayout({
pageLayoutId,
});
const duplicateTab = useCallback(
(tabId: string): string => {
const currentPageLayoutDraft = store.get(pageLayoutDraft);
const allTabLayouts = store.get(pageLayoutCurrentLayouts);
const sourceTab = currentPageLayoutDraft.tabs.find(
(tab) => tab.id === tabId,
);
if (!isDefined(sourceTab)) {
throw new Error(`Tab with id ${tabId} not found`);
}
const newTabId = uuidv4();
const widgetOldIdNewIdMap = new Map<string, string>();
const clonedWidgets: PageLayoutWidget[] = sourceTab.widgets.map(
(widget) => {
const newWidgetId = uuidv4();
widgetOldIdNewIdMap.set(widget.id, newWidgetId);
const widgetViewCopyResult =
duplicateFieldsWidget({
sourceWidget: widget,
newWidgetId,
}) ??
duplicateRecordTableWidget({
sourceWidget: widget,
newWidgetId,
});
const clonedConfiguration = isDefined(widgetViewCopyResult)
? {
...widget.configuration,
viewId: widgetViewCopyResult.newViewId,
}
: widget.configuration;
return {
...widget,
id: newWidgetId,
pageLayoutTabId: newTabId,
configuration: clonedConfiguration,
...generateDuplicatedTimestamps(),
};
},
);
const sortedTabs = sortTabsByPosition(currentPageLayoutDraft.tabs);
const sourceIndex = sortedTabs.findIndex((t) => t.id === tabId);
const newTabPosition = calculateNewPosition({
items: sortedTabs,
destinationIndex: sourceIndex + 1,
sourceIndex,
});
const newTab: PageLayoutTab = {
...sourceTab,
id: newTabId,
title: appendCopySuffix(sourceTab.title),
position: newTabPosition,
widgets: clonedWidgets,
...generateDuplicatedTimestamps(),
};
const sourceLayouts = allTabLayouts[tabId] ?? {
desktop: [],
mobile: [],
};
const newLayouts = {
desktop: sourceLayouts.desktop.map((layout) => ({
...layout,
i: widgetOldIdNewIdMap.get(layout.i) || layout.i,
})),
mobile: sourceLayouts.mobile.map((layout) => ({
...layout,
i: widgetOldIdNewIdMap.get(layout.i) || layout.i,
})),
};
store.set(pageLayoutCurrentLayouts, {
...allTabLayouts,
[newTabId]: newLayouts,
});
const prev = store.get(pageLayoutDraft);
store.set(pageLayoutDraft, {
...prev,
tabs: [...prev.tabs, newTab],
});
closeSidePanelMenu();
setActiveTabId(newTabId);
navigatePageLayoutSidePanel({
sidePanelPage: SidePanelPages.PageLayoutTabSettings,
pageTitle: newTab.title,
focusTitleInput: true,
});
setPageLayoutTabSettingsOpenTabId(newTabId);
return newTabId;
},
[
closeSidePanelMenu,
duplicateFieldsWidget,
duplicateRecordTableWidget,
navigatePageLayoutSidePanel,
pageLayoutCurrentLayouts,
pageLayoutDraft,
setActiveTabId,
setPageLayoutTabSettingsOpenTabId,
store,
],
);
return { duplicateTab };
};