diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx index f55f8f2eab..a1c04b1b48 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx @@ -3,6 +3,7 @@ import { PageLayoutInitializationQueryEffect } from '@/page-layout/components/Pa import { PageLayoutRecordPageCustomizationSessionRegistrationEffect } from '@/page-layout/components/PageLayoutRecordPageCustomizationSessionRegistrationEffect'; import { PageLayoutRendererContent } from '@/page-layout/components/PageLayoutRendererContent'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; +import { RecordTableWidgetViewDraftsInitializationEffect } from '@/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftsInitializationEffect'; import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext'; @@ -41,6 +42,7 @@ export const PageLayoutRenderer = ({ > + diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutSingleTabRenderer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutSingleTabRenderer.tsx index 7881357434..629b621755 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutSingleTabRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutSingleTabRenderer.tsx @@ -10,6 +10,7 @@ import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutIn import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState'; +import { RecordTableWidgetViewDraftsInitializationEffect } from '@/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftsInitializationEffect'; import { getTabLayoutMode } from '@/page-layout/utils/getTabLayoutMode'; import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord'; import { getTabPresentation } from '@/page-layout/utils/getTabPresentation'; @@ -112,6 +113,7 @@ export const PageLayoutSingleTabRenderer = ({ > + diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useSaveRecordTableWidgetViews.ts b/packages/twenty-front/src/modules/page-layout/hooks/useSaveRecordTableWidgetViews.ts index cd9014d161..dbeaa6f909 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useSaveRecordTableWidgetViews.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useSaveRecordTableWidgetViews.ts @@ -74,6 +74,17 @@ export const useSaveRecordTableWidgetViews = () => { continue; } + // Every view-backed widget gets a draft seeded on entering edit mode; + // only the ones the user actually touched need an upsert. + if ( + isDeeplyEqual( + recordTableWidgetViewPersisted[widget.id], + widgetViewDraft, + ) + ) { + continue; + } + const objectMetadataItem = objectMetadataItems.find( (objectMetadataItem) => objectMetadataItem.id === widgetViewDraft.view.objectMetadataId, @@ -93,10 +104,10 @@ export const useSaveRecordTableWidgetViews = () => { }; const persistedView = recordTableWidgetViewPersisted[widget.id]?.view; - const draftView = widgetViewDraft.view; - const draftViewSettings = - buildUpsertViewWidgetViewSettingsInput(draftView); + const draftViewSettings = buildUpsertViewWidgetViewSettingsInput( + widgetViewDraft.view, + ); const hasViewSettingsChanges = !isDefined(persistedView) || diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetRendererContent.tsx b/packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetRendererContent.tsx index c69b75b29a..d5181318fe 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetRendererContent.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetRendererContent.tsx @@ -6,7 +6,6 @@ import { RecordTableWidget } from '@/object-record/record-table-widget/component import { RecordTableWidgetProvider } from '@/object-record/record-table-widget/components/RecordTableWidgetProvider'; import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode'; import { recordTableWidgetViewDraftByWidgetIdComponentFamilySelector } from '@/page-layout/states/selectors/recordTableWidgetViewDraftByWidgetIdComponentFamilySelector'; -import { RecordTableWidgetViewDraftInitEffect } from '@/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftInitEffect'; import { constructViewFromRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot'; import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue'; import { useViewById } from '@/views/hooks/useViewById'; @@ -83,36 +82,30 @@ export const RecordTableWidgetRendererContent = ({ const calendarIsReadOnly = !canEditCalendar; return ( - <> - - - {isKanbanLayout ? ( - - ) : isCalendarLayout ? ( - - ) : ( - - )} - - + + {isKanbanLayout ? ( + + ) : isCalendarLayout ? ( + + ) : ( + + )} + ); }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftInitEffect.tsx b/packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftInitEffect.tsx deleted file mode 100644 index 9d32b9d24c..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftInitEffect.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode'; -import { useInitializeRecordTableWidgetViewDraft } from '@/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDraft'; -import { useViewById } from '@/views/hooks/useViewById'; - -type RecordTableWidgetViewDraftInitEffectProps = { - widgetId: string; - viewId: string; -}; - -export const RecordTableWidgetViewDraftInitEffect = ({ - widgetId, - viewId, -}: RecordTableWidgetViewDraftInitEffectProps) => { - const isPageLayoutInEditMode = useIsPageLayoutInEditMode(); - const { view } = useViewById(viewId); - - useInitializeRecordTableWidgetViewDraft({ - widgetId, - view: isPageLayoutInEditMode ? view : undefined, - }); - - return null; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftsInitializationEffect.tsx b/packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftsInitializationEffect.tsx new file mode 100644 index 0000000000..fab9ab326c --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftsInitializationEffect.tsx @@ -0,0 +1,7 @@ +import { useInitializeRecordTableWidgetViewDrafts } from '@/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDrafts'; + +export const RecordTableWidgetViewDraftsInitializationEffect = () => { + useInitializeRecordTableWidgetViewDrafts(); + + return null; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/__tests__/useInitializeRecordTableWidgetViewDrafts.test.tsx b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/__tests__/useInitializeRecordTableWidgetViewDrafts.test.tsx new file mode 100644 index 0000000000..683aedd2f2 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/__tests__/useInitializeRecordTableWidgetViewDrafts.test.tsx @@ -0,0 +1,165 @@ +import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; +import { + PAGE_LAYOUT_TEST_INSTANCE_ID, + PageLayoutTestWrapper, +} from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; +import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState'; +import { + makeDraft, + makeTab, + makeWidget, +} from '@/page-layout/testing/pageLayoutDraftFixtures'; +import { useInitializeRecordTableWidgetViewDrafts } from '@/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDrafts'; +import { buildRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/buildRecordTableWidgetViewSnapshot'; +import { constructViewFromRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { renderHook } from '@testing-library/react'; +import { createStore } from 'jotai'; +import { type ReactNode } from 'react'; +import { PageLayoutType } from '~/generated-metadata/graphql'; +import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow'; + +const view = constructViewFromRecordTableWidgetViewSnapshot( + buildRecordTableWidgetViewSnapshot( + getMockObjectMetadataItemOrThrow('company'), + ), +); + +jest.mock('@/views/states/selectors/viewsSelector', () => { + const { atom } = jest.requireActual('jotai'); + return { + viewsSelector: { + type: 'Selector', + key: 'viewsSelectorTestMock', + atom: atom(() => [view]), + }, + }; +}); + +const WIDGET_ID = 'widget-1'; +const OTHER_WIDGET_ID = 'widget-2'; + +const makeViewBackedWidget = (widgetId: string, index: number) => { + const widget = makeWidget(widgetId, index); + + return { + ...widget, + configuration: { ...widget.configuration, viewId: view.id }, + } as PageLayoutWidget; +}; + +const makeViewBackedDraftLayout = (widgetIds: string[] = [WIDGET_ID]) => ({ + ...makeDraft([ + makeTab( + 'tab-1', + widgetIds.map((widgetId, index) => makeViewBackedWidget(widgetId, index)), + ), + ]), + id: PAGE_LAYOUT_TEST_INSTANCE_ID, +}); + +const getWrapper = + (store: ReturnType) => + ({ children }: { children: ReactNode }) => ( + + {children} + + ); + +describe('useInitializeRecordTableWidgetViewDrafts', () => { + it('should seed the draft and persisted snapshots of view-backed widgets in edit mode', () => { + const store = createStore(); + + store.set(isLayoutCustomizationModeEnabledState.atom, true); + store.set( + pageLayoutDraftComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + makeViewBackedDraftLayout(), + ); + + renderHook(() => useInitializeRecordTableWidgetViewDrafts(), { + wrapper: getWrapper(store), + }); + + const draftSnapshots = store.get( + recordTableWidgetViewDraftComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + ); + const persistedSnapshots = store.get( + recordTableWidgetViewPersistedComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + ); + + expect(draftSnapshots[WIDGET_ID]).toBeDefined(); + expect(draftSnapshots[WIDGET_ID].view.id).toBe(view.id); + expect(persistedSnapshots[WIDGET_ID]).toBe(draftSnapshots[WIDGET_ID]); + }); + + it('should not seed anything outside edit mode', () => { + const store = createStore(); + + store.set(isLayoutCustomizationModeEnabledState.atom, false); + store.set( + pageLayoutDraftComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + makeViewBackedDraftLayout(), + ); + + renderHook(() => useInitializeRecordTableWidgetViewDrafts(), { + wrapper: getWrapper(store), + }); + + const draftSnapshots = store.get( + recordTableWidgetViewDraftComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + ); + + expect(draftSnapshots[WIDGET_ID]).toBeUndefined(); + }); + + it('should keep an existing snapshot over the freshly built one while seeding the missing ones', () => { + const store = createStore(); + + const existingSnapshot = buildRecordTableWidgetViewSnapshot( + getMockObjectMetadataItemOrThrow('company'), + ); + + store.set(isLayoutCustomizationModeEnabledState.atom, true); + store.set( + pageLayoutDraftComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + makeViewBackedDraftLayout([WIDGET_ID, OTHER_WIDGET_ID]), + ); + store.set( + recordTableWidgetViewDraftComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + { [WIDGET_ID]: existingSnapshot }, + ); + + renderHook(() => useInitializeRecordTableWidgetViewDrafts(), { + wrapper: getWrapper(store), + }); + + const draftSnapshots = store.get( + recordTableWidgetViewDraftComponentState.atomFamily({ + instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID, + }), + ); + + expect(draftSnapshots[OTHER_WIDGET_ID]).toBeDefined(); + expect(draftSnapshots[WIDGET_ID]).toBe(existingSnapshot); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDraft.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDraft.ts deleted file mode 100644 index 2f3f77dbc2..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDraft.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; -import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState'; -import { buildRecordTableWidgetViewSnapshotFromView } from '@/page-layout/widgets/record-table/utils/buildRecordTableWidgetViewSnapshotFromView'; -import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; -import { type View } from '@/views/types/View'; -import { useStore } from 'jotai'; -import { useCallback, useEffect } from 'react'; - -type UseInitializeRecordTableWidgetViewDraftParams = { - widgetId: string; - view: View | undefined; -}; - -export const useInitializeRecordTableWidgetViewDraft = ({ - widgetId, - view, -}: UseInitializeRecordTableWidgetViewDraftParams) => { - const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState( - recordTableWidgetViewDraftComponentState, - ); - - const recordTableWidgetViewPersistedState = - useAtomComponentStateCallbackState( - recordTableWidgetViewPersistedComponentState, - ); - - const store = useStore(); - - const initializeDraft = useCallback(() => { - const currentDraft = store.get(recordTableWidgetViewDraftState); - - if (widgetId in currentDraft) { - return; - } - - if (!view || view.viewFields.length === 0) { - return; - } - - const snapshot = buildRecordTableWidgetViewSnapshotFromView(view); - - store.set(recordTableWidgetViewDraftState, (prev) => ({ - ...prev, - [widgetId]: snapshot, - })); - - store.set(recordTableWidgetViewPersistedState, (prev) => ({ - ...prev, - [widgetId]: snapshot, - })); - }, [ - recordTableWidgetViewDraftState, - recordTableWidgetViewPersistedState, - widgetId, - view, - store, - ]); - - useEffect(initializeDraft, [initializeDraft]); -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDrafts.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDrafts.ts new file mode 100644 index 0000000000..b6657b208d --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDrafts.ts @@ -0,0 +1,73 @@ +import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; +import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState'; +import { buildMissingRecordTableWidgetViewDraftSnapshots } from '@/page-layout/widgets/record-table/utils/buildMissingRecordTableWidgetViewDraftSnapshots'; +import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { viewsSelector } from '@/views/states/selectors/viewsSelector'; +import { useStore } from 'jotai'; +import { useEffect } from 'react'; + +// Seeds the editable view snapshot of every widget backed by a persisted +// view as soon as the layout is in edit mode, and again whenever a widget +// gains a view (e.g. a field widget configured to table display) or views +// finish loading. The draft is the write target of all widget view settings, +// so it must exist before any of them run — including the ones in the side +// panel, which mounts outside this layout tree and cannot seed it itself. +export const useInitializeRecordTableWidgetViewDrafts = () => { + const isPageLayoutInEditMode = useIsPageLayoutInEditMode(); + + const pageLayoutDraft = useAtomComponentStateValue( + pageLayoutDraftComponentState, + ); + + const views = useAtomStateValue(viewsSelector); + + const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState( + recordTableWidgetViewDraftComponentState, + ); + + const recordTableWidgetViewPersistedState = + useAtomComponentStateCallbackState( + recordTableWidgetViewPersistedComponentState, + ); + + const store = useStore(); + + useEffect(() => { + if (!isPageLayoutInEditMode) { + return; + } + + const missingSnapshotsByWidgetId = + buildMissingRecordTableWidgetViewDraftSnapshots({ + widgets: pageLayoutDraft.tabs.flatMap((tab) => tab.widgets), + existingSnapshotsByWidgetId: store.get(recordTableWidgetViewDraftState), + views, + }); + + if (Object.keys(missingSnapshotsByWidgetId).length === 0) { + return; + } + + // Spread order lets entries added elsewhere in the meantime win over the + // freshly built snapshots. + store.set(recordTableWidgetViewDraftState, (previousSnapshots) => ({ + ...missingSnapshotsByWidgetId, + ...previousSnapshots, + })); + store.set(recordTableWidgetViewPersistedState, (previousSnapshots) => ({ + ...missingSnapshotsByWidgetId, + ...previousSnapshots, + })); + }, [ + isPageLayoutInEditMode, + pageLayoutDraft, + views, + recordTableWidgetViewDraftState, + recordTableWidgetViewPersistedState, + store, + ]); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/buildMissingRecordTableWidgetViewDraftSnapshots.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/buildMissingRecordTableWidgetViewDraftSnapshots.test.ts new file mode 100644 index 0000000000..62c7a34ba7 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/buildMissingRecordTableWidgetViewDraftSnapshots.test.ts @@ -0,0 +1,85 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { buildMissingRecordTableWidgetViewDraftSnapshots } from '@/page-layout/widgets/record-table/utils/buildMissingRecordTableWidgetViewDraftSnapshots'; +import { buildRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/buildRecordTableWidgetViewSnapshot'; +import { constructViewFromRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot'; +import { makeWidget } from '@/page-layout/testing/pageLayoutDraftFixtures'; +import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow'; + +const view = constructViewFromRecordTableWidgetViewSnapshot( + buildRecordTableWidgetViewSnapshot( + getMockObjectMetadataItemOrThrow('company'), + ), +); + +const makeViewBackedWidget = ( + id: string, + viewId: string | undefined, +): PageLayoutWidget => { + const widget = makeWidget(id, 0); + + return { + ...widget, + configuration: { + ...widget.configuration, + viewId, + }, + } as PageLayoutWidget; +}; + +describe('buildMissingRecordTableWidgetViewDraftSnapshots', () => { + it('should build a snapshot for a view-backed widget with no existing snapshot', () => { + const snapshots = buildMissingRecordTableWidgetViewDraftSnapshots({ + widgets: [makeViewBackedWidget('widget-1', view.id)], + existingSnapshotsByWidgetId: {}, + views: [view], + }); + + expect(snapshots['widget-1']).toBeDefined(); + expect(snapshots['widget-1'].view.id).toBe(view.id); + expect(snapshots['widget-1'].viewFields.length).toBeGreaterThan(0); + }); + + it('should not overwrite an existing snapshot', () => { + const existingSnapshot = buildRecordTableWidgetViewSnapshot( + getMockObjectMetadataItemOrThrow('company'), + ); + + const snapshots = buildMissingRecordTableWidgetViewDraftSnapshots({ + widgets: [makeViewBackedWidget('widget-1', view.id)], + existingSnapshotsByWidgetId: { 'widget-1': existingSnapshot }, + views: [view], + }); + + expect(snapshots).toEqual({}); + }); + + it('should skip widgets whose configuration has no view', () => { + const snapshots = buildMissingRecordTableWidgetViewDraftSnapshots({ + widgets: [makeViewBackedWidget('widget-1', undefined)], + existingSnapshotsByWidgetId: {}, + views: [view], + }); + + expect(snapshots).toEqual({}); + }); + + it('should skip widgets whose view is not loaded yet', () => { + const snapshots = buildMissingRecordTableWidgetViewDraftSnapshots({ + widgets: [makeViewBackedWidget('widget-1', 'not-loaded-view-id')], + existingSnapshotsByWidgetId: {}, + views: [view], + }); + + expect(snapshots).toEqual({}); + }); + + it('should skip views that have no view fields yet', () => { + const snapshots = buildMissingRecordTableWidgetViewDraftSnapshots({ + widgets: [makeViewBackedWidget('widget-1', view.id)], + existingSnapshotsByWidgetId: {}, + views: [{ ...view, viewFields: [] }], + }); + + expect(snapshots).toEqual({}); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/buildMissingRecordTableWidgetViewDraftSnapshots.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/buildMissingRecordTableWidgetViewDraftSnapshots.ts new file mode 100644 index 0000000000..bbd3a769fc --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/buildMissingRecordTableWidgetViewDraftSnapshots.ts @@ -0,0 +1,51 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId'; +import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot'; +import { buildRecordTableWidgetViewSnapshotFromView } from '@/page-layout/widgets/record-table/utils/buildRecordTableWidgetViewSnapshotFromView'; +import { type View } from '@/views/types/View'; +import { isDefined } from 'twenty-shared/utils'; + +type BuildMissingRecordTableWidgetViewDraftSnapshotsParams = { + widgets: PageLayoutWidget[]; + existingSnapshotsByWidgetId: Record; + views: View[]; +}; + +export const buildMissingRecordTableWidgetViewDraftSnapshots = ({ + widgets, + existingSnapshotsByWidgetId, + views, +}: BuildMissingRecordTableWidgetViewDraftSnapshotsParams): Record< + string, + RecordTableWidgetViewSnapshot +> => { + const missingSnapshotsByWidgetId: Record< + string, + RecordTableWidgetViewSnapshot + > = {}; + + for (const widget of widgets) { + if (widget.id in existingSnapshotsByWidgetId) { + continue; + } + + const viewId = getWidgetConfigurationViewId(widget.configuration); + + if (!isDefined(viewId)) { + continue; + } + + const view = views.find((viewToFind) => viewToFind.id === viewId); + + // A view without fields has not finished being created server-side; + // snapshotting it now would lock an empty column set into the draft. + if (!isDefined(view) || view.viewFields.length === 0) { + continue; + } + + missingSnapshotsByWidgetId[widget.id] = + buildRecordTableWidgetViewSnapshotFromView(view); + } + + return missingSnapshotsByWidgetId; +}; diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx index 3a11927de6..07a66dde0a 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx @@ -2,7 +2,6 @@ import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetada import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { isFieldMetadataItemAvailableAsCalendarField } from '@/object-record/record-calendar/utils/isFieldMetadataItemAvailableAsCalendarField'; import { getFieldWidgetAvailableDisplayModes } from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig'; -import { RecordTableWidgetViewDraftInitEffect } from '@/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftInitEffect'; import { useAddDraftViewForFieldRelationTableWidget } from '@/page-layout/widgets/record-table/hooks/useAddDraftViewForFieldRelationTableWidget'; import { type RecordTableWidgetLayoutViewType, @@ -208,16 +207,6 @@ export const FieldWidgetLayoutDropdownContent = () => { return ( - {/* The widget's draft snapshot is normally seeded by the table-family - renderer; while displayed as Field/Card that renderer isn't mounted, - so seed the draft here (idempotent) for direct e.g. Card -> Kanban - switches. */} - {isDefined(currentViewId) && isDefined(widgetInEditMode) && ( - - )}