From 9e385e44f9d3d5a6b1ce3c2cfa366980b9eb9bf0 Mon Sep 17 00:00:00 2001 From: Weiko Date: Sun, 12 Apr 2026 20:26:47 +0200 Subject: [PATCH] Fields widget draft view (#19562) - Defer backend view creation for FIELDS widgets to prevent orphan views when users cancel or leave before saving. Views are now created optimistically in Jotai state (client-side UUID) and only persisted to the database when the page layout is saved. - Align the frontend's default field groups fallback with the backend logic: split into General/Other groups, hide relation fields by default, and filter invisible fields consistently across all code paths. --- .../components/LayoutCustomizationBar.tsx | 2 +- .../hooks/useSaveLayoutCustomization.ts | 6 + .../useCreatePendingFieldsWidgetViews.ts | 94 ++++++++++++ .../hooks/useCreateRecordPageFieldsWidget.ts | 16 +- .../hooks/useDeletePageLayoutWidget.ts | 18 +++ ...movePageLayoutWidgetAndPreservePosition.ts | 18 ++- .../page-layout/hooks/useSavePageLayout.ts | 7 + .../__stories__/FieldsWidget.stories.tsx | 22 +-- .../hooks/useCreateViewForFieldsWidget.ts | 42 ------ .../hooks/useFieldsWidgetEditorGroupsData.ts | 19 ++- .../useFieldsWidgetFieldMetadataItems.ts | 22 --- .../fields/hooks/useFieldsWidgetGroups.ts | 140 ++---------------- .../utils/buildDefaultFieldsWidgetGroups.ts | 65 ++++++++ ...elPageLayoutRecordPageWidgetTypeSelect.tsx | 16 +- .../SidePanelRecordPageFieldsSettings.tsx | 2 +- 15 files changed, 253 insertions(+), 236 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/useCreatePendingFieldsWidgetViews.ts delete mode 100644 packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useCreateViewForFieldsWidget.ts delete mode 100644 packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/fields/utils/buildDefaultFieldsWidgetGroups.ts diff --git a/packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx b/packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx index 1d143b7051..3a3f5750f6 100644 --- a/packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx +++ b/packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx @@ -46,7 +46,7 @@ const LayoutCustomizationBarContent = () => { ease: 'easeInOut', }} > - + {t`Layout customization`} diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts index a55a3380ab..eff91bd5ff 100644 --- a/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts +++ b/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts @@ -6,6 +6,7 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/sta import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector'; import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems'; import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft'; +import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews'; import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData'; import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; @@ -38,6 +39,8 @@ export const useSaveLayoutCustomization = () => { const { enqueueErrorSnackBar } = useSnackBar(); const { updatePageLayoutWithTabsAndWidgets } = useUpdatePageLayoutWithTabsAndWidgets(); + const { createPendingFieldsWidgetViews } = + useCreatePendingFieldsWidgetViews(); const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode(); const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData(); @@ -104,6 +107,8 @@ export const useSaveLayoutCustomization = () => { persistedAsDraft, ); + await createPendingFieldsWidgetViews(pageLayoutId); + if (isPageLayoutStructureDirty) { const updateInput = convertPageLayoutDraftToUpdateInput(draft, { shouldFilterDynamicRelationWidgets: @@ -174,6 +179,7 @@ export const useSaveLayoutCustomization = () => { saveDraft, saveCommandMenuItemsDraft, isCommandMenuItemsDirty, + createPendingFieldsWidgetViews, updatePageLayoutWithTabsAndWidgets, savePageLayoutWidgetsData, exitLayoutCustomizationMode, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePendingFieldsWidgetViews.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePendingFieldsWidgetViews.ts new file mode 100644 index 0000000000..2f28bdf0b1 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePendingFieldsWidgetViews.ts @@ -0,0 +1,94 @@ +import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; +import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId'; +import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { ViewType, WidgetType } from '~/generated-metadata/graphql'; + +export const useCreatePendingFieldsWidgetViews = () => { + const { performViewAPICreate } = usePerformViewAPIPersist(); + const store = useStore(); + + const createPendingFieldsWidgetViews = useCallback( + async (pageLayoutId: string) => { + const draft = store.get( + pageLayoutDraftComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + ); + const persisted = store.get( + pageLayoutPersistedComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + ); + + const persistedWidgetIds = new Set( + persisted?.tabs.flatMap((tab) => + tab.widgets.map((widget) => widget.id), + ) ?? [], + ); + + const objectMetadataItems = store.get(objectMetadataItemsSelector.atom); + + const newFieldsWidgets = draft.tabs + .flatMap((tab) => tab.widgets) + .filter((widget) => { + if (widget.type !== WidgetType.FIELDS) { + return false; + } + + const viewId = getWidgetConfigurationViewId(widget.configuration); + + return isDefined(viewId) && !persistedWidgetIds.has(widget.id); + }); + + for (const widget of newFieldsWidgets) { + const viewId = getWidgetConfigurationViewId(widget.configuration); + + if (!isDefined(viewId)) { + continue; + } + + const objectMetadataId = + widget.objectMetadataId ?? draft.objectMetadataId; + + if (!isDefined(objectMetadataId)) { + continue; + } + + const objectMetadataItem = objectMetadataItems.find( + (item) => item.id === objectMetadataId, + ); + + const viewName = isDefined(objectMetadataItem) + ? `${objectMetadataItem.labelSingular} Fields` + : 'Fields'; + + const result = await performViewAPICreate( + { + input: { + id: viewId, + name: viewName, + icon: 'IconList', + objectMetadataId, + type: ViewType.FIELDS_WIDGET, + }, + }, + objectMetadataId, + ); + + if (result.status === 'failed') { + throw new Error( + `Failed to create view for FIELDS widget ${widget.id}`, + ); + } + } + }, + [performViewAPICreate, store], + ); + + return { createPendingFieldsWidgetViews }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldsWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldsWidget.ts index 59e0bc143f..adba66d68c 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldsWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldsWidget.ts @@ -5,7 +5,6 @@ import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDr import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; import { createDefaultFieldsWidget } from '@/page-layout/utils/createDefaultFieldsWidget'; -import { useCreateViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useCreateViewForFieldsWidget'; import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel'; import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord'; import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; @@ -25,8 +24,6 @@ export const useCreateRecordPageFieldsWidget = () => { const { currentPageLayout } = useCurrentPageLayoutOrThrow(); - const { createViewForFieldsWidget } = useCreateViewForFieldsWidget(); - const pageLayoutDraftState = useAtomComponentStateCallbackState( pageLayoutDraftComponentState, ); @@ -39,15 +36,8 @@ export const useCreateRecordPageFieldsWidget = () => { const store = useStore(); - const createRecordPageFieldsWidget = useCallback(async () => { - const viewId = await createViewForFieldsWidget({ - objectMetadataId: objectMetadataItem.id, - viewName: `${objectMetadataItem.labelSingular} Fields`, - }); - - if (viewId === null) { - return; - } + const createRecordPageFieldsWidget = useCallback(() => { + const viewId = uuidv4(); const activeTab = currentPageLayout.tabs.find((tab) => tab.id === tabId); const positionIndex = activeTab?.widgets.length ?? 0; @@ -75,11 +65,9 @@ export const useCreateRecordPageFieldsWidget = () => { resetNavigationStack: true, }); }, [ - createViewForFieldsWidget, currentPageLayout.tabs, navigatePageLayoutSidePanel, objectMetadataItem.id, - objectMetadataItem.labelSingular, pageLayoutDraftState, pageLayoutEditingWidgetIdState, store, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts index 5779b90949..83415898e3 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts @@ -2,6 +2,7 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab'; import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab'; import { useDeleteViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget'; @@ -42,6 +43,11 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => { const { deleteViewForRecordTableWidget } = useDeleteViewForRecordTableWidget(); + const pageLayoutPersistedState = useAtomComponentStateCallbackState( + pageLayoutPersistedComponentState, + pageLayoutId, + ); + const store = useStore(); const deletePageLayoutWidget = useCallback( @@ -59,7 +65,17 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => { (widget) => widget.id === widgetId, ); + const persisted = store.get(pageLayoutPersistedState); + const persistedWidgetIds = new Set( + persisted?.tabs.flatMap((tab) => + tab.widgets.map((widget) => widget.id), + ) ?? [], + ); + + const isWidgetPersisted = persistedWidgetIds.has(widgetId); + if ( + isWidgetPersisted && isDefined(widgetToDelete) && widgetToDelete.type === WidgetType.RECORD_TABLE && 'viewId' in widgetToDelete.configuration && @@ -71,6 +87,7 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => { } if ( + isWidgetPersisted && isDefined(widgetToDelete) && widgetToDelete.type === WidgetType.FIELDS && 'viewId' in widgetToDelete.configuration && @@ -112,6 +129,7 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => { pageLayoutCurrentLayoutsState, pageLayoutDraftState, pageLayoutEditingWidgetIdState, + pageLayoutPersistedState, store, ], ); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useRemovePageLayoutWidgetAndPreservePosition.ts b/packages/twenty-front/src/modules/page-layout/hooks/useRemovePageLayoutWidgetAndPreservePosition.ts index 4215087613..a6a0655061 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useRemovePageLayoutWidgetAndPreservePosition.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useRemovePageLayoutWidgetAndPreservePosition.ts @@ -3,6 +3,7 @@ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pag import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId'; import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab'; import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab'; @@ -48,6 +49,11 @@ export const useRemovePageLayoutWidgetAndPreservePosition = ( const { deleteViewForRecordTableWidget } = useDeleteViewForRecordTableWidget(); + const pageLayoutPersistedState = useAtomComponentStateCallbackState( + pageLayoutPersistedComponentState, + pageLayoutId, + ); + const store = useStore(); const removePageLayoutWidgetAndPreservePosition = useCallback( @@ -63,7 +69,16 @@ export const useRemovePageLayoutWidgetAndPreservePosition = ( (widget) => widget.id === widgetId, ); - if (isDefined(widgetToRemove)) { + const persisted = store.get(pageLayoutPersistedState); + const persistedWidgetIds = new Set( + persisted?.tabs.flatMap((tab) => + tab.widgets.map((widget) => widget.id), + ) ?? [], + ); + + const isWidgetPersisted = persistedWidgetIds.has(widgetId); + + if (isWidgetPersisted && isDefined(widgetToRemove)) { const viewId = getWidgetConfigurationViewId( widgetToRemove.configuration, ); @@ -122,6 +137,7 @@ export const useRemovePageLayoutWidgetAndPreservePosition = ( pageLayoutDraftState, pageLayoutDraggedAreaState, pageLayoutEditingWidgetIdState, + pageLayoutPersistedState, store, ], ); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts b/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts index 03ddd0b7b4..ac27f0c070 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts @@ -1,3 +1,4 @@ +import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews'; import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; @@ -41,12 +42,17 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => { const { updatePageLayoutWithTabsAndWidgets } = useUpdatePageLayoutWithTabsAndWidgets(); + const { createPendingFieldsWidgetViews } = + useCreatePendingFieldsWidgetViews(); + const featureFlags = useFeatureFlagsMap(); const isRecordPageLayoutEditingEnabled = featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED]; const store = useStore(); const savePageLayout = useCallback(async () => { + await createPendingFieldsWidgetViews(pageLayoutId); + const pageLayoutDraft = store.get(pageLayoutDraftCallbackState); const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft, { shouldFilterDynamicRelationWidgets: !isRecordPageLayoutEditingEnabled, @@ -84,6 +90,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => { return result; }, [ + createPendingFieldsWidgetViews, isRecordPageLayoutEditingEnabled, pageLayoutCurrentLayoutsCallbackState, pageLayoutDraftCallbackState, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/__stories__/FieldsWidget.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/__stories__/FieldsWidget.stories.tsx index 73e11f34df..891d7c3e15 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/__stories__/FieldsWidget.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/__stories__/FieldsWidget.stories.tsx @@ -368,15 +368,9 @@ export const WithViewFieldGroups: Story = { }, }; -export const WithInlineViewFields: Story = { +export const WithDefaultGroups: Story = { render: () => { - const view = createView({ - viewFields: [ - createViewField('vf-name', nameField.id, 0), - createViewField('vf-employees', employeesField.id, 1), - createViewField('vf-address', addressField.id, 2), - ], - }); + const view = createView(); const widget = createFieldsWidget(FIELDS_VIEW_ID); @@ -443,14 +437,12 @@ export const WithInlineViewFields: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const companyName = await canvas.findByText('Acme Corporation'); - expect(companyName).toBeVisible(); + const generalHeader = await canvas.findByText('General'); + expect(generalHeader).toBeVisible(); - const contactInfoHeader = canvas.queryByText('Contact Info'); - expect(contactInfoHeader).toBeNull(); - - const generalHeader = canvas.queryByText('General'); - expect(generalHeader).toBeNull(); + const creationDateElements = await canvas.findAllByText('Creation date'); + expect(creationDateElements.length).toBeGreaterThan(0); + expect(creationDateElements[0]).toBeVisible(); }, }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useCreateViewForFieldsWidget.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useCreateViewForFieldsWidget.ts deleted file mode 100644 index 13901f46f2..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useCreateViewForFieldsWidget.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist'; -import { useCallback } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import { ViewType } from '~/generated-metadata/graphql'; - -export const useCreateViewForFieldsWidget = () => { - const { performViewAPICreate } = usePerformViewAPIPersist(); - - const createViewForFieldsWidget = useCallback( - async ({ - objectMetadataId, - viewName, - }: { - objectMetadataId: string; - viewName: string; - }) => { - const viewId = uuidv4(); - - const result = await performViewAPICreate( - { - input: { - id: viewId, - name: viewName, - icon: 'IconList', - objectMetadataId, - type: ViewType.FIELDS_WIDGET, - }, - }, - objectMetadataId, - ); - - if (result.status === 'failed') { - return null; - } - - return viewId; - }, - [performViewAPICreate], - ); - - return { createViewForFieldsWidget }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData.ts index 91e918532c..251a3dd0b7 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetEditorGroupsData.ts @@ -5,6 +5,7 @@ import { type FieldsWidgetGroup, type FieldsWidgetGroupField, } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { buildDefaultFieldsWidgetGroups } from '@/page-layout/widgets/fields/utils/buildDefaultFieldsWidgetGroups'; import { useViewById } from '@/views/hooks/useViewById'; import { useMemo } from 'react'; import { @@ -199,8 +200,22 @@ export const useFieldsWidgetEditorGroupsData = ({ } } - return { groups: [], ungroupedFields: [], editorMode: 'ungrouped' }; - }, [objectMetadataItem, view, labelIdentifierFieldMetadataItem]); + return { + groups: buildDefaultFieldsWidgetGroups({ + fields: objectMetadataItem.fields, + objectNameSingular, + labelIdentifierFieldMetadataItemId: + labelIdentifierFieldMetadataItem?.id, + }), + ungroupedFields: [], + editorMode: 'grouped' as const, + }; + }, [ + objectMetadataItem, + objectNameSingular, + view, + labelIdentifierFieldMetadataItem, + ]); return { ...result, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems.ts deleted file mode 100644 index 3c49ba1654..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems'; - -type UseFieldsWidgetFieldMetadataItemsProps = { - objectNameSingular: string; -}; - -export const useFieldsWidgetFieldMetadataItems = ({ - objectNameSingular, -}: UseFieldsWidgetFieldMetadataItemsProps) => { - const { inlineFieldMetadataItems, legacyActivityTargetFieldMetadataItems } = - useFieldListFieldMetadataItems({ - objectNameSingular, - excludeFieldMetadataIds: [], - excludeCreatedAtAndUpdatedAt: true, - showRelationSections: true, - }); - - return { - inlineFieldMetadataItems, - legacyActivityTargetFieldMetadataItems, - }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroups.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroups.ts index 421493c8b2..0f2fafe573 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroups.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroups.ts @@ -1,12 +1,13 @@ +import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useFieldsWidgetFieldMetadataItems } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems'; import { type FieldsWidgetDisplayMode } from '@/page-layout/widgets/fields/types/FieldsWidgetDisplayMode'; import { type FieldsWidgetGroup, type FieldsWidgetGroupField, } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { buildDefaultFieldsWidgetGroups } from '@/page-layout/widgets/fields/utils/buildDefaultFieldsWidgetGroups'; +import { filterDraftGroupsForDisplay } from '@/page-layout/widgets/fields/utils/filterDraftGroupsForDisplay'; import { useViewById } from '@/views/hooks/useViewById'; -import { useLingui } from '@lingui/react/macro'; import { useMemo } from 'react'; import { isDefined, isNonEmptyArray } from 'twenty-shared/utils'; @@ -19,14 +20,12 @@ export const useFieldsWidgetGroups = ({ viewId, objectNameSingular, }: UseFieldsWidgetGroupsParams) => { - const { t } = useLingui(); const { view } = useViewById(viewId); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); - - const { inlineFieldMetadataItems, legacyActivityTargetFieldMetadataItems } = - useFieldsWidgetFieldMetadataItems({ + const { labelIdentifierFieldMetadataItem } = + useLabelIdentifierFieldMetadataItem({ objectNameSingular, }); @@ -82,133 +81,26 @@ export const useFieldsWidgetGroups = ({ .filter((group) => group.fields.length > 0); } - if (isDefined(view) && view.viewFields.length > 0) { - let globalIndex = 0; - - const fields: FieldsWidgetGroupField[] = [...view.viewFields] - .sort((a, b) => a.position - b.position) - .filter((viewField) => viewField.isVisible) - .map((viewField) => { - const fieldMetadataItem = objectMetadataItem.fields.find( - (f) => f.id === viewField.fieldMetadataId, - ); - - if (!isDefined(fieldMetadataItem)) { - return null; - } - - return { - fieldMetadataItem, - position: viewField.position, - isVisible: viewField.isVisible, - globalIndex: globalIndex++, - }; - }) - .filter(isDefined); - - if (fields.length > 0) { - return [ - { - id: `${view.id}-group-general`, - name: t`General`, - position: 0, - isVisible: true, - fields, - }, - ]; - } - } - - const fieldsToDisplay = [ - ...inlineFieldMetadataItems, - ...legacyActivityTargetFieldMetadataItems, - ]; - - if (fieldsToDisplay.length === 0) { - return []; - } - - const generalFields: Array<{ - fieldMetadataItem: (typeof fieldsToDisplay)[0]; - position: number; - }> = []; - const otherFields: Array<{ - fieldMetadataItem: (typeof fieldsToDisplay)[0]; - position: number; - }> = []; - - let generalPosition = 0; - let otherPosition = 0; - - fieldsToDisplay.forEach((field) => { - if (field.isCustom === true) { - otherFields.push({ - fieldMetadataItem: field, - position: otherPosition++, - }); - } else { - generalFields.push({ - fieldMetadataItem: field, - position: generalPosition++, - }); - } - }); - - const groups: FieldsWidgetGroup[] = []; - let globalIndex = 0; - - if (generalFields.length > 0) { - groups.push({ - id: `${objectNameSingular}-group-general`, - name: t`General`, - position: 0, - isVisible: true, - fields: generalFields.map((field) => ({ - fieldMetadataItem: field.fieldMetadataItem, - position: field.position, - isVisible: true, - globalIndex: globalIndex++, - })), - }); - } - - if (otherFields.length > 0) { - groups.push({ - id: `${objectNameSingular}-group-other`, - name: t`Other`, - position: 1, - isVisible: true, - fields: otherFields.map((field) => ({ - fieldMetadataItem: field.fieldMetadataItem, - position: field.position, - isVisible: true, - globalIndex: globalIndex++, - })), - }); - } - - return groups; + return filterDraftGroupsForDisplay( + buildDefaultFieldsWidgetGroups({ + fields: objectMetadataItem.fields, + objectNameSingular, + labelIdentifierFieldMetadataItemId: + labelIdentifierFieldMetadataItem?.id, + }), + ); }, [ objectMetadataItem, objectNameSingular, - t, + labelIdentifierFieldMetadataItem, view, - inlineFieldMetadataItems, - legacyActivityTargetFieldMetadataItems, ]); - const displayMode: FieldsWidgetDisplayMode = - isDefined(view) && - !isNonEmptyArray(view.viewFieldGroups) && - view.viewFields.length > 0 - ? 'inline' - : 'grouped'; + const displayMode: FieldsWidgetDisplayMode = 'grouped'; return { groups, displayMode, - isFromView: - isDefined(view) && - (isNonEmptyArray(view.viewFieldGroups) || view.viewFields.length > 0), + isFromView: isDefined(view) && isNonEmptyArray(view.viewFieldGroups), }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/buildDefaultFieldsWidgetGroups.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/buildDefaultFieldsWidgetGroups.ts new file mode 100644 index 0000000000..654125a2ad --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/buildDefaultFieldsWidgetGroups.ts @@ -0,0 +1,65 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isFieldMetadataEligibleForFieldsWidget } from 'twenty-shared/utils'; +import { v4 as uuidv4 } from 'uuid'; + +export const buildDefaultFieldsWidgetGroups = ({ + fields, + objectNameSingular, + labelIdentifierFieldMetadataItemId, +}: { + fields: FieldMetadataItem[]; + objectNameSingular: string; + labelIdentifierFieldMetadataItemId: string | undefined; +}): FieldsWidgetGroup[] => { + const eligibleFields = fields.filter((field) => + isFieldMetadataEligibleForFieldsWidget({ + fieldName: field.name, + fieldType: field.type, + isLabelIdentifierField: field.id === labelIdentifierFieldMetadataItemId, + }), + ); + + const standardFields = eligibleFields.filter((field) => !field.isCustom); + const customFields = eligibleFields.filter((field) => field.isCustom); + + const isFieldVisible = (fieldType: FieldMetadataType) => + fieldType !== FieldMetadataType.RELATION && + fieldType !== FieldMetadataType.MORPH_RELATION; + + const groups: FieldsWidgetGroup[] = []; + let globalIndex = 0; + + if (standardFields.length > 0) { + groups.push({ + id: uuidv4(), + name: 'General', + position: 0, + isVisible: true, + fields: standardFields.map((field, index) => ({ + fieldMetadataItem: field, + position: index, + isVisible: isFieldVisible(field.type), + globalIndex: globalIndex++, + })), + }); + } + + if (customFields.length > 0) { + groups.push({ + id: uuidv4(), + name: 'Other', + position: 1, + isVisible: true, + fields: customFields.map((field, index) => ({ + fieldMetadataItem: field, + position: index, + isVisible: isFieldVisible(field.type), + globalIndex: globalIndex++, + })), + }); + } + + return groups; +}; diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx index b25f00d139..2e0d71d173 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx @@ -15,7 +15,6 @@ import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPositi import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab'; import { useFieldWidgetEligibleFields } from '@/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields'; import { getFieldWidgetDefaultDisplayMode } from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig'; -import { useCreateViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useCreateViewForFieldsWidget'; import { useDeleteViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget'; import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget'; import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; @@ -92,8 +91,6 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { const { insertCreatedWidgetAtContext } = useInsertCreatedWidgetAtContext(pageLayoutId); - const { createViewForFieldsWidget } = useCreateViewForFieldsWidget(); - const { deleteViewForFieldsWidget } = useDeleteViewForFieldsWidget(); const { deleteViewForRecordTableWidget } = @@ -187,21 +184,14 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { }), ); - const handleCreateFieldsWidget = useCallback(async () => { + const handleCreateFieldsWidget = useCallback(() => { if (!isDefined(tabId)) { return; } const replacePositionIndex = getExistingWidgetPositionIndex(); - const viewId = await createViewForFieldsWidget({ - objectMetadataId: objectMetadataItem.id, - viewName: `${objectMetadataItem.labelSingular} Fields`, - }); - - if (viewId === null) { - return; - } + const viewId = uuidv4(); removeExistingWidgetIfReplacing(); @@ -233,12 +223,10 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { resetNavigationStack: true, }); }, [ - createViewForFieldsWidget, getExistingWidgetPositionIndex, insertCreatedWidgetAtContext, navigatePageLayoutSidePanel, objectMetadataItem.id, - objectMetadataItem.labelSingular, pageLayoutDraftState, removeExistingWidgetIfReplacing, setPageLayoutEditingWidgetId, diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldsSettings.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldsSettings.tsx index e69e2ab8de..213044802e 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldsSettings.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldsSettings.tsx @@ -108,7 +108,7 @@ export const SidePanelRecordPageFieldsSettings = () => { Icon={IconLayoutSidebarRight} hasSubMenu onClick={handleNavigateToLayout} - description={t`${totalFieldsCount} fields`} + description={t`${totalFieldsCount} visible fields`} contextualTextPosition="right" />