Fallback to field metadata (#19131)

Rely on the field metadata items to always display all object's fields
in the fields widget configuration editor. If fields are missing in the
returned view fields, we add the missing fields through object metadata.


https://github.com/user-attachments/assets/3c4d45e8-05d0-4943-be4b-bcf1e310155c
This commit is contained in:
Baptiste Devessier
2026-03-31 21:00:16 +02:00
committed by GitHub
parent 5bbfce7789
commit c11e4ece39
17 changed files with 591 additions and 68 deletions
@@ -73,19 +73,15 @@ export const useSaveFieldsWidgetGroups = () => {
name: group.name,
position: group.position,
isVisible: group.isVisible,
fields: group.fields.flatMap((field) => {
if (!isDefined(field.viewFieldId)) {
return [];
}
return [
{
viewFieldId: field.viewFieldId,
isVisible: field.isVisible,
position: field.position,
},
];
}),
fields: group.fields.map((field) => ({
...(isDefined(field.viewFieldId)
? { viewFieldId: field.viewFieldId }
: {
fieldMetadataId: field.fieldMetadataItem.id,
}),
isVisible: field.isVisible,
position: field.position,
})),
})),
},
},
@@ -97,19 +93,15 @@ export const useSaveFieldsWidgetGroups = () => {
variables: {
input: {
widgetId,
fields: ungroupedFields.flatMap((field) => {
if (!isDefined(field.viewFieldId)) {
return [];
}
return [
{
viewFieldId: field.viewFieldId,
isVisible: field.isVisible,
position: field.position,
},
];
}),
fields: ungroupedFields.map((field) => ({
...(isDefined(field.viewFieldId)
? { viewFieldId: field.viewFieldId }
: {
fieldMetadataId: field.fieldMetadataItem.id,
}),
isVisible: field.isVisible,
position: field.position,
})),
},
},
});
@@ -1,3 +1,4 @@
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { type FieldsWidgetEditorMode } from '@/page-layout/widgets/fields/types/FieldsWidgetEditorMode';
import {
@@ -6,7 +7,11 @@ import {
} from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { useViewById } from '@/views/hooks/useViewById';
import { useMemo } from 'react';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import {
isDefined,
isFieldMetadataEligibleForFieldsWidget,
isNonEmptyArray,
} from 'twenty-shared/utils';
type UseFieldsWidgetEditorGroupsDataParams = {
viewId: string | null;
@@ -28,6 +33,10 @@ export const useFieldsWidgetEditorGroupsData = ({
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({
objectNameSingular,
});
const result = useMemo<
Pick<
@@ -39,6 +48,45 @@ export const useFieldsWidgetEditorGroupsData = ({
return { groups: [], ungroupedFields: [], editorMode: 'ungrouped' };
}
const eligibleFieldMetadataIds = new Set(
objectMetadataItem.fields
.filter((field) =>
isFieldMetadataEligibleForFieldsWidget({
fieldName: field.name,
fieldType: field.type,
isLabelIdentifierField:
field.id === labelIdentifierFieldMetadataItem?.id,
}),
)
.map((field) => field.id),
);
const buildMissingFields = ({
existingFieldMetadataIds,
startGlobalIndex,
startPosition,
}: {
existingFieldMetadataIds: Set<string>;
startGlobalIndex: number;
startPosition: number;
}): FieldsWidgetGroupField[] => {
let globalIndex = startGlobalIndex;
let position = startPosition;
return objectMetadataItem.fields
.filter(
(field) =>
!existingFieldMetadataIds.has(field.id) &&
eligibleFieldMetadataIds.has(field.id),
)
.map((field) => ({
fieldMetadataItem: field,
position: position++,
isVisible: false,
globalIndex: globalIndex++,
}));
};
if (isDefined(view) && isNonEmptyArray(view.viewFieldGroups)) {
const viewFieldGroups = view.viewFieldGroups;
@@ -47,13 +95,14 @@ export const useFieldsWidgetEditorGroupsData = ({
);
let globalIndex = 0;
const existingFieldMetadataIds = new Set<string>();
const groups = sortedGroups.map((group) => {
const groupFields = [...(group.viewFields ?? [])].sort(
(a, b) => a.position - b.position,
);
const fields = groupFields
const fields: FieldsWidgetGroupField[] = groupFields
.map((viewField) => {
const fieldMetadataItem = objectMetadataItem.fields.find(
(f) => f.id === viewField.fieldMetadataId,
@@ -63,6 +112,8 @@ export const useFieldsWidgetEditorGroupsData = ({
return null;
}
existingFieldMetadataIds.add(viewField.fieldMetadataId);
return {
fieldMetadataItem,
position: viewField.position,
@@ -82,11 +133,28 @@ export const useFieldsWidgetEditorGroupsData = ({
};
});
const lastGroup = groups[groups.length - 1];
const lastFieldPosition =
lastGroup.fields.length > 0
? Math.max(...lastGroup.fields.map((f) => f.position)) + 1
: 0;
const missingFields = buildMissingFields({
existingFieldMetadataIds,
startGlobalIndex: globalIndex,
startPosition: lastFieldPosition,
});
if (missingFields.length > 0) {
lastGroup.fields = [...lastGroup.fields, ...missingFields];
}
return { groups, ungroupedFields: [], editorMode: 'grouped' };
}
if (isDefined(view) && view.viewFields.length > 0) {
let globalIndex = 0;
const existingFieldMetadataIds = new Set<string>();
const fields = [...view.viewFields]
.sort((a, b) => a.position - b.position)
@@ -99,6 +167,8 @@ export const useFieldsWidgetEditorGroupsData = ({
return null;
}
existingFieldMetadataIds.add(viewField.fieldMetadataId);
return {
fieldMetadataItem,
position: viewField.position,
@@ -109,13 +179,28 @@ export const useFieldsWidgetEditorGroupsData = ({
})
.filter(isDefined);
if (fields.length > 0) {
return { groups: [], ungroupedFields: fields, editorMode: 'ungrouped' };
const lastFieldPosition =
fields.length > 0 ? Math.max(...fields.map((f) => f.position)) + 1 : 0;
const missingFields = buildMissingFields({
existingFieldMetadataIds,
startGlobalIndex: globalIndex,
startPosition: lastFieldPosition,
});
const allFields = [...fields, ...missingFields];
if (allFields.length > 0) {
return {
groups: [],
ungroupedFields: allFields,
editorMode: 'ungrouped',
};
}
}
return { groups: [], ungroupedFields: [], editorMode: 'ungrouped' };
}, [objectMetadataItem, view]);
}, [objectMetadataItem, view, labelIdentifierFieldMetadataItem]);
return {
...result,