fix(twenty-front): stop field widget layout dropdown from crashing th… (#23784)

…e record page

RecordTableWidgetViewDraftInitEffect read the page layout edit mode and
the page layout instance id from context, but the widget settings side
panel renders outside the page layout tree. Opening the Layout picker on
a relation field widget displayed as a table threw
"PageLayoutEditModeContext Context not found" and took down the whole
record page.

Both values are now passed in by the caller.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23784?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Félix Malfait
2026-08-05 10:21:28 +02:00
committed by GitHub
parent ded3f1efb3
commit b05d2406ec
12 changed files with 424 additions and 129 deletions
@@ -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 = ({
>
<PageLayoutInitializationQueryEffect pageLayoutId={pageLayoutId} />
<PageLayoutRecordPageCustomizationSessionRegistrationEffect />
<RecordTableWidgetViewDraftsInitializationEffect />
<PageLayoutRendererContent />
</PageLayoutEditModeProvider>
</TabListComponentInstanceContext.Provider>
@@ -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 = ({
>
<PageLayoutInitializationQueryEffect pageLayoutId={pageLayoutId} />
<PageLayoutRecordPageCustomizationSessionRegistrationEffect />
<RecordTableWidgetViewDraftsInitializationEffect />
<PageLayoutSingleTabRendererContent />
</PageLayoutEditModeProvider>
</TabListComponentInstanceContext.Provider>
@@ -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) ||
@@ -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 (
<>
<RecordTableWidgetViewDraftInitEffect
widgetId={widgetId}
viewId={viewId}
/>
<RecordTableWidgetProvider
objectNameSingular={objectMetadataItem.nameSingular}
viewId={viewId}
widgetId={widgetId}
recordLimit={recordLimit}
instanceIdSuffix={instanceIdSuffix}
contextStoreViewType={
isKanbanLayout
? ContextStoreViewType.Kanban
: isCalendarLayout
? ContextStoreViewType.Calendar
: ContextStoreViewType.Table
}
>
{isKanbanLayout ? (
<RecordBoardWidget isReadOnly={isReadOnly} />
) : isCalendarLayout ? (
<RecordCalendarWidget isReadOnly={calendarIsReadOnly} />
) : (
<RecordTableWidget
isReadOnly={isReadOnly}
isEmptyStateHidden={isEmptyStateHidden}
/>
)}
</RecordTableWidgetProvider>
</>
<RecordTableWidgetProvider
objectNameSingular={objectMetadataItem.nameSingular}
viewId={viewId}
widgetId={widgetId}
recordLimit={recordLimit}
instanceIdSuffix={instanceIdSuffix}
contextStoreViewType={
isKanbanLayout
? ContextStoreViewType.Kanban
: isCalendarLayout
? ContextStoreViewType.Calendar
: ContextStoreViewType.Table
}
>
{isKanbanLayout ? (
<RecordBoardWidget isReadOnly={isReadOnly} />
) : isCalendarLayout ? (
<RecordCalendarWidget isReadOnly={calendarIsReadOnly} />
) : (
<RecordTableWidget
isReadOnly={isReadOnly}
isEmptyStateHidden={isEmptyStateHidden}
/>
)}
</RecordTableWidgetProvider>
);
};
@@ -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;
};
@@ -0,0 +1,7 @@
import { useInitializeRecordTableWidgetViewDrafts } from '@/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDrafts';
export const RecordTableWidgetViewDraftsInitializationEffect = () => {
useInitializeRecordTableWidgetViewDrafts();
return null;
};
@@ -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<typeof createStore>) =>
({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper
store={store}
layoutType={PageLayoutType.RECORD_PAGE}
instanceId={PAGE_LAYOUT_TEST_INSTANCE_ID}
>
{children}
</PageLayoutTestWrapper>
);
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);
});
});
@@ -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]);
};
@@ -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,
]);
};
@@ -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({});
});
});
@@ -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<string, RecordTableWidgetViewSnapshot>;
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;
};
@@ -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 (
<DropdownMenuItemsContainer>
{/* 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) && (
<RecordTableWidgetViewDraftInitEffect
widgetId={widgetInEditMode.id}
viewId={currentViewId}
/>
)}
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}