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) && (
-
- )}