Skip to content

Commit a710c10

Browse files
Fix orphan views by deferring record table widget view creation to dashboard save (#20006)
1 parent a5cd64d commit a710c10

35 files changed

Lines changed: 1129 additions & 182 deletions

File tree

packages/twenty-front/src/modules/layout-customization/hooks/useCancelLayoutCustomization.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/useExitLayoutCustomizationMode';
2-
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
32
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
43
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
54
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
@@ -10,6 +9,9 @@ import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layou
109
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
1110
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
1211
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
12+
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
13+
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
14+
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
1315
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
1416
import { useStore } from 'jotai';
1517
import { useCallback } from 'react';
@@ -90,6 +92,18 @@ export const useCancelLayoutCustomization = () => {
9092
}),
9193
fieldsWidgetEditorModePersisted,
9294
);
95+
96+
const recordTableWidgetViewPersisted = store.get(
97+
recordTableWidgetViewPersistedComponentState.atomFamily({
98+
instanceId: pageLayoutId,
99+
}),
100+
);
101+
store.set(
102+
recordTableWidgetViewDraftComponentState.atomFamily({
103+
instanceId: pageLayoutId,
104+
}),
105+
recordTableWidgetViewPersisted,
106+
);
93107
}
94108

95109
exitLayoutCustomizationMode();

packages/twenty-front/src/modules/layout-customization/hooks/useIsLayoutCustomizationDirty.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { useCommandMenuItemsDraftState } from '@/command-menu-item/hooks/useCommandMenuItemsDraftState';
22
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
3-
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
43
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
54
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
65
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
76
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
87
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
98
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
109
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
10+
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
11+
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
12+
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
1113
import { atom, useAtomValue } from 'jotai';
1214
import { useMemo } from 'react';
1315
import { isDefined } from 'twenty-shared/utils';
@@ -86,6 +88,26 @@ export const useIsLayoutCustomizationDirty = () => {
8688
if (!isDeeplyEqual(ungroupedFieldsDraft, ungroupedFieldsPersisted)) {
8789
return true;
8890
}
91+
92+
const recordTableWidgetViewDraft = get(
93+
recordTableWidgetViewDraftComponentState.atomFamily({
94+
instanceId: pageLayoutId,
95+
}),
96+
);
97+
const recordTableWidgetViewPersisted = get(
98+
recordTableWidgetViewPersistedComponentState.atomFamily({
99+
instanceId: pageLayoutId,
100+
}),
101+
);
102+
103+
if (
104+
!isDeeplyEqual(
105+
recordTableWidgetViewDraft,
106+
recordTableWidgetViewPersisted,
107+
)
108+
) {
109+
return true;
110+
}
89111
}
90112

91113
return false;

packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/state
77
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
88
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft';
99
import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews';
10+
import { useCreatePendingRecordTableWidgetViews } from '@/page-layout/hooks/useCreatePendingRecordTableWidgetViews';
1011
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
1112
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
1213
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
@@ -42,6 +43,8 @@ export const useSaveLayoutCustomization = () => {
4243
useUpdatePageLayoutWithTabsAndWidgets();
4344
const { createPendingFieldsWidgetViews } =
4445
useCreatePendingFieldsWidgetViews();
46+
const { createPendingRecordTableWidgetViews } =
47+
useCreatePendingRecordTableWidgetViews();
4548
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
4649
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
4750

@@ -113,6 +116,7 @@ export const useSaveLayoutCustomization = () => {
113116
);
114117

115118
await createPendingFieldsWidgetViews(pageLayoutId);
119+
await createPendingRecordTableWidgetViews(pageLayoutId);
116120

117121
if (isPageLayoutStructureDirty) {
118122
const updateInput = convertPageLayoutDraftToUpdateInput(draft, {
@@ -185,6 +189,7 @@ export const useSaveLayoutCustomization = () => {
185189
saveCommandMenuItemsDraft,
186190
isCommandMenuItemsDirty,
187191
createPendingFieldsWidgetViews,
192+
createPendingRecordTableWidgetViews,
188193
updatePageLayoutWithTabsAndWidgets,
189194
savePageLayoutWidgetsData,
190195
exitLayoutCustomizationMode,

packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidgetProvider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export const RecordTableWidgetProvider = ({
9898
>
9999
<RecordTableWidgetViewLoadEffect
100100
viewId={viewId}
101+
widgetId={widgetId}
101102
objectMetadataItem={objectMetadataItem}
102103
/>
103104
{children}

packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidgetViewLoadEffect.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
22
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
33
import { lastLoadedRecordTableWidgetViewIdComponentState } from '@/object-record/record-table-widget/states/lastLoadedRecordTableWidgetViewIdComponentState';
4+
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
5+
import { recordTableWidgetViewDraftByWidgetIdComponentFamilySelector } from '@/page-layout/states/selectors/recordTableWidgetViewDraftByWidgetIdComponentFamilySelector';
6+
import { constructViewFromRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot';
7+
import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
48
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
59
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
610
import { viewFromViewIdFamilySelector } from '@/views/states/selectors/viewFromViewIdFamilySelector';
@@ -9,11 +13,13 @@ import { isDefined } from 'twenty-shared/utils';
913

1014
type RecordTableWidgetViewLoadEffectProps = {
1115
viewId: string;
16+
widgetId: string;
1217
objectMetadataItem: EnrichedObjectMetadataItem;
1318
};
1419

1520
export const RecordTableWidgetViewLoadEffect = ({
1621
viewId,
22+
widgetId,
1723
objectMetadataItem,
1824
}: RecordTableWidgetViewLoadEffectProps) => {
1925
const { loadRecordIndexStates } = useLoadRecordIndexStates();
@@ -23,18 +29,30 @@ export const RecordTableWidgetViewLoadEffect = ({
2329
setLastLoadedRecordTableWidgetViewId,
2430
] = useAtomComponentState(lastLoadedRecordTableWidgetViewIdComponentState);
2531

26-
const viewFromViewId = useAtomFamilySelectorValue(
32+
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
33+
34+
const draftSnapshot = useAtomComponentFamilySelectorValue(
35+
recordTableWidgetViewDraftByWidgetIdComponentFamilySelector,
36+
{ widgetId },
37+
);
38+
39+
const viewFromDraft =
40+
isPageLayoutInEditMode && isDefined(draftSnapshot)
41+
? constructViewFromRecordTableWidgetViewSnapshot(draftSnapshot)
42+
: undefined;
43+
44+
const viewFromSelector = useAtomFamilySelectorValue(
2745
viewFromViewIdFamilySelector,
28-
{
29-
viewId,
30-
},
46+
{ viewId },
3147
);
3248

49+
const currentView = viewFromDraft ?? viewFromSelector;
50+
3351
const viewHasFields =
34-
isDefined(viewFromViewId) && viewFromViewId.viewFields.length > 0;
52+
isDefined(currentView) && currentView.viewFields.length > 0;
3553

3654
useEffect(() => {
37-
if (!isDefined(viewFromViewId)) {
55+
if (!isDefined(currentView)) {
3856
return;
3957
}
4058

@@ -50,7 +68,7 @@ export const RecordTableWidgetViewLoadEffect = ({
5068
return;
5169
}
5270

53-
loadRecordIndexStates(viewFromViewId, objectMetadataItem);
71+
loadRecordIndexStates(currentView, objectMetadataItem);
5472

5573
setLastLoadedRecordTableWidgetViewId({
5674
viewId,
@@ -60,7 +78,7 @@ export const RecordTableWidgetViewLoadEffect = ({
6078
viewId,
6179
lastLoadedRecordTableWidgetViewId,
6280
setLastLoadedRecordTableWidgetViewId,
63-
viewFromViewId,
81+
currentView,
6482
viewHasFields,
6583
objectMetadataItem,
6684
loadRecordIndexStates,

packages/twenty-front/src/modules/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import {
99
} from './PageLayoutTestWrapper';
1010

1111
jest.mock(
12-
'@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget',
12+
'@/page-layout/widgets/record-table/hooks/useRemoveDraftViewForRecordTableWidget',
1313
() => ({
14-
useDeleteViewForRecordTableWidget: () => ({
15-
deleteViewForRecordTableWidget: jest.fn(),
14+
useRemoveDraftViewForRecordTableWidget: () => ({
15+
removeDraftViewForRecordTableWidget: jest.fn(),
1616
}),
1717
}),
1818
);
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
2+
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
3+
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
4+
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
5+
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
6+
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
7+
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
8+
import { useStore } from 'jotai';
9+
import { useCallback } from 'react';
10+
import { isDefined } from 'twenty-shared/utils';
11+
import { WidgetType } from '~/generated-metadata/graphql';
12+
13+
export const useCreatePendingRecordTableWidgetViews = () => {
14+
const { performViewAPICreate, performViewAPIDestroy } =
15+
usePerformViewAPIPersist();
16+
const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist();
17+
const store = useStore();
18+
19+
const createPendingRecordTableWidgetViews = useCallback(
20+
async (pageLayoutId: string) => {
21+
const draft = store.get(
22+
pageLayoutDraftComponentState.atomFamily({
23+
instanceId: pageLayoutId,
24+
}),
25+
);
26+
const persisted = store.get(
27+
pageLayoutPersistedComponentState.atomFamily({
28+
instanceId: pageLayoutId,
29+
}),
30+
);
31+
32+
const recordTableWidgetViewDraft = store.get(
33+
recordTableWidgetViewDraftComponentState.atomFamily({
34+
instanceId: pageLayoutId,
35+
}),
36+
);
37+
38+
const persistedRecordTableWidgets = new Map(
39+
(persisted?.tabs ?? [])
40+
.flatMap((tab) => tab.widgets)
41+
.filter((widget) => widget.type === WidgetType.RECORD_TABLE)
42+
.map((widget) => [
43+
widget.id,
44+
getWidgetConfigurationViewId(widget.configuration),
45+
]),
46+
);
47+
48+
const draftRecordTableWidgets = draft.tabs
49+
.flatMap((tab) => tab.widgets)
50+
.filter((widget) => widget.type === WidgetType.RECORD_TABLE);
51+
52+
const draftWidgetIds = new Set(
53+
draftRecordTableWidgets.map((widget) => widget.id),
54+
);
55+
56+
for (const widget of draftRecordTableWidgets) {
57+
const viewId = getWidgetConfigurationViewId(widget.configuration);
58+
59+
if (!isDefined(viewId)) {
60+
continue;
61+
}
62+
63+
const persistedViewId = persistedRecordTableWidgets.get(widget.id);
64+
65+
if (persistedViewId === viewId) {
66+
continue;
67+
}
68+
69+
if (isDefined(persistedViewId)) {
70+
await performViewAPIDestroy({ id: persistedViewId });
71+
}
72+
73+
const widgetViewDraft = recordTableWidgetViewDraft[widget.id];
74+
75+
if (!isDefined(widgetViewDraft)) {
76+
continue;
77+
}
78+
79+
const { view } = widgetViewDraft;
80+
81+
const result = await performViewAPICreate(
82+
{
83+
input: {
84+
id: view.id,
85+
name: view.name,
86+
icon: view.icon,
87+
objectMetadataId: view.objectMetadataId,
88+
type: view.type,
89+
isCompact: view.isCompact,
90+
position: view.position,
91+
openRecordIn: view.openRecordIn,
92+
visibility: view.visibility,
93+
shouldHideEmptyGroups: view.shouldHideEmptyGroups,
94+
},
95+
},
96+
view.objectMetadataId,
97+
);
98+
99+
if (result.status === 'failed') {
100+
throw new Error(
101+
`Failed to create view for RECORD_TABLE widget ${widget.id}`,
102+
);
103+
}
104+
105+
const viewFieldInputs = widgetViewDraft.viewFields.map((field) => ({
106+
id: field.id,
107+
viewId: field.viewId,
108+
fieldMetadataId: field.fieldMetadataId,
109+
position: field.position,
110+
size: field.size,
111+
isVisible: field.isVisible,
112+
}));
113+
114+
if (viewFieldInputs.length > 0) {
115+
await performViewFieldAPICreate({ inputs: viewFieldInputs });
116+
}
117+
}
118+
119+
for (const [widgetId, viewId] of persistedRecordTableWidgets) {
120+
if (!draftWidgetIds.has(widgetId) && isDefined(viewId)) {
121+
await performViewAPIDestroy({ id: viewId });
122+
}
123+
}
124+
125+
store.set(
126+
recordTableWidgetViewPersistedComponentState.atomFamily({
127+
instanceId: pageLayoutId,
128+
}),
129+
recordTableWidgetViewDraft,
130+
);
131+
},
132+
[
133+
performViewAPICreate,
134+
performViewAPIDestroy,
135+
performViewFieldAPICreate,
136+
store,
137+
],
138+
);
139+
140+
return { createPendingRecordTableWidgetViews };
141+
};

packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews';
2+
import { useCreatePendingRecordTableWidgetViews } from '@/page-layout/hooks/useCreatePendingRecordTableWidgetViews';
23
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
34
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
45
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
@@ -45,13 +46,17 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
4546
const { createPendingFieldsWidgetViews } =
4647
useCreatePendingFieldsWidgetViews();
4748

49+
const { createPendingRecordTableWidgetViews } =
50+
useCreatePendingRecordTableWidgetViews();
51+
4852
const featureFlags = useFeatureFlagsMap();
4953
const isRecordPageLayoutEditingEnabled =
5054
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
5155
const store = useStore();
5256

5357
const savePageLayout = useCallback(async () => {
5458
await createPendingFieldsWidgetViews(pageLayoutId);
59+
await createPendingRecordTableWidgetViews(pageLayoutId);
5560

5661
const pageLayoutDraft = store.get(pageLayoutDraftCallbackState);
5762
const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft, {
@@ -91,6 +96,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
9196
return result;
9297
}, [
9398
createPendingFieldsWidgetViews,
99+
createPendingRecordTableWidgetViews,
94100
isRecordPageLayoutEditingEnabled,
95101
pageLayoutCurrentLayoutsCallbackState,
96102
pageLayoutDraftCallbackState,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
2+
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
3+
4+
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
5+
6+
export const recordTableWidgetViewDraftComponentState =
7+
createAtomComponentState<Record<string, RecordTableWidgetViewSnapshot>>({
8+
key: 'recordTableWidgetViewDraftComponentState',
9+
defaultValue: {},
10+
componentInstanceContext: PageLayoutComponentInstanceContext,
11+
});

0 commit comments

Comments
 (0)