Fix duplicate Fields widget (#19696)

## Context
Tab duplication was broken after the view creation logic was deferred
and moved to the FE.

- Duplicating a tab or a FIELDS widget now produces a fully independent
copy: new view, new view field groups, new view fields — all with fresh
IDs — while preserving any unsaved edits
  from the source widget.
- Removed the backend auto-seed of default view fields / view field
groups in ViewService.createOne for FIELDS_WIDGET views. The frontend
always sends the complete layout via
upsertFieldsWidget, so the auto-seed was both redundant and the source
of potential bugs.
- Extracted a shared useDuplicateFieldsWidgetForPageLayout hook used by
both tab and widget duplication paths, plus a small
useCloneViewInMetadataStore helper that clones the FlatView
in the metadata store and returns the copied flat view fields/groups for
the caller.
This commit is contained in:
Weiko
2026-04-15 14:29:44 +02:00
committed by GitHub
parent 2df32f7003
commit e12b55a951
9 changed files with 402 additions and 607 deletions
@@ -1,8 +1,10 @@
import { useDuplicateFieldsWidgetForPageLayout } from '@/page-layout/hooks/useDuplicateFieldsWidgetForPageLayout';
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';
@@ -56,13 +58,19 @@ export const useDuplicatePageLayoutTab = ({
const { closeSidePanelMenu } = useSidePanelMenu();
const { duplicateFieldsWidget } = useDuplicateFieldsWidgetForPageLayout({
pageLayoutId,
});
const duplicateTab = useCallback(
(tabId: string): string => {
const currentPageLayoutDraft = store.get(pageLayoutDraft);
const allTabLayouts = store.get(pageLayoutCurrentLayouts);
const sourceTab = currentPageLayoutDraft.tabs.find((t) => t.id === tabId);
const sourceTab = currentPageLayoutDraft.tabs.find(
(tab) => tab.id === tabId,
);
if (!isDefined(sourceTab)) {
throw new Error(`Tab with id ${tabId} not found`);
@@ -71,17 +79,32 @@ export const useDuplicatePageLayoutTab = ({
const newTabId = uuidv4();
const widgetOldIdNewIdMap = new Map<string, string>();
const clonedWidgets = sourceTab.widgets.map((widget) => {
const newWidgetId = uuidv4();
widgetOldIdNewIdMap.set(widget.id, newWidgetId);
const clonedWidgets: PageLayoutWidget[] = sourceTab.widgets.map(
(widget) => {
const newWidgetId = uuidv4();
widgetOldIdNewIdMap.set(widget.id, newWidgetId);
return {
...widget,
id: newWidgetId,
pageLayoutTabId: newTabId,
...generateDuplicatedTimestamps(),
};
});
const fieldsWidgetCopyResult = duplicateFieldsWidget({
sourceWidget: widget,
newWidgetId,
});
const clonedConfiguration = isDefined(fieldsWidgetCopyResult)
? {
...widget.configuration,
viewId: fieldsWidgetCopyResult.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);
@@ -144,6 +167,7 @@ export const useDuplicatePageLayoutTab = ({
},
[
closeSidePanelMenu,
duplicateFieldsWidget,
navigatePageLayoutSidePanel,
pageLayoutCurrentLayouts,
pageLayoutDraft,