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
@@ -0,0 +1,83 @@
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
import { type FlatViewFieldGroup } from '@/metadata-store/types/FlatViewFieldGroup';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import {
type FieldsWidgetGroup,
type FieldsWidgetGroupField,
} from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { type FieldsWidgetEditorMode } from '@/page-layout/widgets/fields/types/FieldsWidgetEditorMode';
import { isDefined } from 'twenty-shared/utils';
type BuildResult = {
editorMode: FieldsWidgetEditorMode;
groups: FieldsWidgetGroup[];
ungroupedFields: FieldsWidgetGroupField[];
};
export const buildFieldsWidgetGroupsFromFlatViewData = ({
flatViewFieldGroups,
flatViewFields,
fieldMetadataItems,
}: {
flatViewFieldGroups: FlatViewFieldGroup[];
flatViewFields: FlatViewField[];
fieldMetadataItems: FieldMetadataItem[];
}): BuildResult => {
const fieldMetadataById = new Map(
fieldMetadataItems.map((fieldMetadataItem) => [
fieldMetadataItem.id,
fieldMetadataItem,
]),
);
if (flatViewFieldGroups.length > 0) {
const groups = flatViewFieldGroups.map((group) => ({
id: group.id,
name: group.name,
position: group.position,
isVisible: group.isVisible,
fields: flatViewFields
.filter((field) => field.viewFieldGroupId === group.id)
.sort((a, b) => a.position - b.position)
.map((field, index) => {
const fieldMetadataItem = fieldMetadataById.get(
field.fieldMetadataId,
);
if (!isDefined(fieldMetadataItem)) {
return undefined;
}
return {
fieldMetadataItem,
position: field.position,
isVisible: field.isVisible,
globalIndex: index,
};
})
.filter(isDefined),
}));
return { editorMode: 'grouped', groups, ungroupedFields: [] };
}
const ungroupedFields = flatViewFields
.sort((a, b) => a.position - b.position)
.map((field, index) => {
const fieldMetadataItem = fieldMetadataById.get(field.fieldMetadataId);
if (!isDefined(fieldMetadataItem)) {
return undefined;
}
return {
fieldMetadataItem,
position: field.position,
isVisible: field.isVisible,
globalIndex: index,
};
})
.filter(isDefined);
return { editorMode: 'ungrouped', groups: [], ungroupedFields };
};