From a710c105cf300ae54381638cd9db0efd73d08cae Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:36:26 +0530 Subject: [PATCH] Fix orphan views by deferring record table widget view creation to dashboard save (#20006) --- .../hooks/useCancelLayoutCustomization.ts | 16 +- .../hooks/useIsLayoutCustomizationDirty.ts | 24 +- .../hooks/useSaveLayoutCustomization.ts | 5 + .../components/RecordTableWidgetProvider.tsx | 1 + .../RecordTableWidgetViewLoadEffect.tsx | 36 ++- .../useDeletePageLayoutWidget.test.tsx | 6 +- .../useCreatePendingRecordTableWidgetViews.ts | 141 +++++++++ .../page-layout/hooks/useSavePageLayout.ts | 6 + ...ecordTableWidgetViewDraftComponentState.ts | 11 + ...dTableWidgetViewPersistedComponentState.ts | 11 + ...wDraftByWidgetIdComponentFamilySelector.ts | 23 ++ ...FieldsWidgetGroupsFromFlatViewData.test.ts | 281 ++++++++++++++++++ .../RecordTableWidgetRendererContent.tsx | 21 +- .../RecordTableWidgetViewDraftInitEffect.tsx | 23 ++ .../useAddDraftViewForRecordTableWidget.ts | 89 ++++++ .../useCreateViewForRecordTableWidget.ts | 85 ------ .../useDeleteViewForRecordTableWidget.ts | 15 - ...useInitializeRecordTableWidgetViewDraft.ts | 71 +++++ .../useRecordTableWidgetViewFieldItems.ts | 23 +- .../useRecordTableWidgetViewForDisplay.ts | 35 +++ .../useRemoveDraftViewForRecordTableWidget.ts | 27 ++ .../useReorderRecordTableWidgetFields.ts | 59 +++- ...cordTableWidgetsViewDataOnDashboardSave.ts | 19 +- ...eToggleRecordTableWidgetFieldVisibility.ts | 49 ++- .../types/RecordTableWidgetViewSnapshot.ts | 7 + ...ldsByRelevanceForRecordTableWidget.test.ts | 130 ++++++++ ...ctViewFromRecordTableWidgetViewSnapshot.ts | 13 + ...nelPageLayoutDashboardWidgetTypeSelect.tsx | 10 +- .../RecordTableDataSourceDropdownContent.tsx | 26 +- .../RecordTableSettingsFieldVisibility.tsx | 14 +- .../RecordTableSettingsFilters.tsx | 12 +- .../RecordTableSettingsSorts.tsx | 12 +- .../SidePanelRecordTableFieldsSubPage.tsx | 8 +- .../SidePanelRecordTableFilterSubPage.tsx | 2 + .../SidePanelRecordTableSortSubPage.tsx | 2 + 35 files changed, 1130 insertions(+), 183 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/useCreatePendingRecordTableWidgetViews.ts create mode 100644 packages/twenty-front/src/modules/page-layout/states/recordTableWidgetViewDraftComponentState.ts create mode 100644 packages/twenty-front/src/modules/page-layout/states/recordTableWidgetViewPersistedComponentState.ts create mode 100644 packages/twenty-front/src/modules/page-layout/states/selectors/recordTableWidgetViewDraftByWidgetIdComponentFamilySelector.ts create mode 100644 packages/twenty-front/src/modules/page-layout/utils/__tests__/buildFieldsWidgetGroupsFromFlatViewData.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftInitEffect.tsx create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useAddDraftViewForRecordTableWidget.ts delete mode 100644 packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget.ts delete mode 100644 packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDraft.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useRemoveDraftViewForRecordTableWidget.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/sortFieldsByRelevanceForRecordTableWidget.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot.ts diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useCancelLayoutCustomization.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useCancelLayoutCustomization.ts index f1ff5093ca..f2d0cdee4b 100644 --- a/packages/twenty-front/src/modules/layout-customization/hooks/useCancelLayoutCustomization.ts +++ b/packages/twenty-front/src/modules/layout-customization/hooks/useCancelLayoutCustomization.ts @@ -1,5 +1,4 @@ import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/useExitLayoutCustomizationMode'; -import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout'; import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState'; import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState'; import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState'; @@ -10,6 +9,9 @@ import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layou import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; +import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState'; +import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout'; import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts'; import { useStore } from 'jotai'; import { useCallback } from 'react'; @@ -90,6 +92,18 @@ export const useCancelLayoutCustomization = () => { }), fieldsWidgetEditorModePersisted, ); + + const recordTableWidgetViewPersisted = store.get( + recordTableWidgetViewPersistedComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + ); + store.set( + recordTableWidgetViewDraftComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + recordTableWidgetViewPersisted, + ); } exitLayoutCustomizationMode(); diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useIsLayoutCustomizationDirty.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useIsLayoutCustomizationDirty.ts index 7ea1d5880d..dfc495e4bd 100644 --- a/packages/twenty-front/src/modules/layout-customization/hooks/useIsLayoutCustomizationDirty.ts +++ b/packages/twenty-front/src/modules/layout-customization/hooks/useIsLayoutCustomizationDirty.ts @@ -1,6 +1,5 @@ import { useCommandMenuItemsDraftState } from '@/command-menu-item/hooks/useCommandMenuItemsDraftState'; import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState'; -import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout'; import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState'; import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState'; @@ -8,6 +7,9 @@ import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/st import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; +import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState'; +import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout'; import { atom, useAtomValue } from 'jotai'; import { useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; @@ -86,6 +88,26 @@ export const useIsLayoutCustomizationDirty = () => { if (!isDeeplyEqual(ungroupedFieldsDraft, ungroupedFieldsPersisted)) { return true; } + + const recordTableWidgetViewDraft = get( + recordTableWidgetViewDraftComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + ); + const recordTableWidgetViewPersisted = get( + recordTableWidgetViewPersistedComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + ); + + if ( + !isDeeplyEqual( + recordTableWidgetViewDraft, + recordTableWidgetViewPersisted, + ) + ) { + return true; + } } return false; diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts index b4570b84f6..82c4e684ec 100644 --- a/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts +++ b/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts @@ -7,6 +7,7 @@ import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/state import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems'; import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft'; import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews'; +import { useCreatePendingRecordTableWidgetViews } from '@/page-layout/hooks/useCreatePendingRecordTableWidgetViews'; import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData'; import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; @@ -42,6 +43,8 @@ export const useSaveLayoutCustomization = () => { useUpdatePageLayoutWithTabsAndWidgets(); const { createPendingFieldsWidgetViews } = useCreatePendingFieldsWidgetViews(); + const { createPendingRecordTableWidgetViews } = + useCreatePendingRecordTableWidgetViews(); const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode(); const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData(); @@ -113,6 +116,7 @@ export const useSaveLayoutCustomization = () => { ); await createPendingFieldsWidgetViews(pageLayoutId); + await createPendingRecordTableWidgetViews(pageLayoutId); if (isPageLayoutStructureDirty) { const updateInput = convertPageLayoutDraftToUpdateInput(draft, { @@ -185,6 +189,7 @@ export const useSaveLayoutCustomization = () => { saveCommandMenuItemsDraft, isCommandMenuItemsDirty, createPendingFieldsWidgetViews, + createPendingRecordTableWidgetViews, updatePageLayoutWithTabsAndWidgets, savePageLayoutWidgetsData, exitLayoutCustomizationMode, diff --git a/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidgetProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidgetProvider.tsx index 77f613f587..7016aac436 100644 --- a/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidgetProvider.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidgetProvider.tsx @@ -98,6 +98,7 @@ export const RecordTableWidgetProvider = ({ > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidgetViewLoadEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidgetViewLoadEffect.tsx index e1406c5ca5..ea5e3a0905 100644 --- a/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidgetViewLoadEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table-widget/components/RecordTableWidgetViewLoadEffect.tsx @@ -1,6 +1,10 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates'; import { lastLoadedRecordTableWidgetViewIdComponentState } from '@/object-record/record-table-widget/states/lastLoadedRecordTableWidgetViewIdComponentState'; +import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode'; +import { recordTableWidgetViewDraftByWidgetIdComponentFamilySelector } from '@/page-layout/states/selectors/recordTableWidgetViewDraftByWidgetIdComponentFamilySelector'; +import { constructViewFromRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot'; +import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue'; import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState'; import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue'; import { viewFromViewIdFamilySelector } from '@/views/states/selectors/viewFromViewIdFamilySelector'; @@ -9,11 +13,13 @@ import { isDefined } from 'twenty-shared/utils'; type RecordTableWidgetViewLoadEffectProps = { viewId: string; + widgetId: string; objectMetadataItem: EnrichedObjectMetadataItem; }; export const RecordTableWidgetViewLoadEffect = ({ viewId, + widgetId, objectMetadataItem, }: RecordTableWidgetViewLoadEffectProps) => { const { loadRecordIndexStates } = useLoadRecordIndexStates(); @@ -23,18 +29,30 @@ export const RecordTableWidgetViewLoadEffect = ({ setLastLoadedRecordTableWidgetViewId, ] = useAtomComponentState(lastLoadedRecordTableWidgetViewIdComponentState); - const viewFromViewId = useAtomFamilySelectorValue( - viewFromViewIdFamilySelector, - { - viewId, - }, + const isPageLayoutInEditMode = useIsPageLayoutInEditMode(); + + const draftSnapshot = useAtomComponentFamilySelectorValue( + recordTableWidgetViewDraftByWidgetIdComponentFamilySelector, + { widgetId }, ); + const viewFromDraft = + isPageLayoutInEditMode && isDefined(draftSnapshot) + ? constructViewFromRecordTableWidgetViewSnapshot(draftSnapshot) + : undefined; + + const viewFromSelector = useAtomFamilySelectorValue( + viewFromViewIdFamilySelector, + { viewId }, + ); + + const currentView = viewFromDraft ?? viewFromSelector; + const viewHasFields = - isDefined(viewFromViewId) && viewFromViewId.viewFields.length > 0; + isDefined(currentView) && currentView.viewFields.length > 0; useEffect(() => { - if (!isDefined(viewFromViewId)) { + if (!isDefined(currentView)) { return; } @@ -50,7 +68,7 @@ export const RecordTableWidgetViewLoadEffect = ({ return; } - loadRecordIndexStates(viewFromViewId, objectMetadataItem); + loadRecordIndexStates(currentView, objectMetadataItem); setLastLoadedRecordTableWidgetViewId({ viewId, @@ -60,7 +78,7 @@ export const RecordTableWidgetViewLoadEffect = ({ viewId, lastLoadedRecordTableWidgetViewId, setLastLoadedRecordTableWidgetViewId, - viewFromViewId, + currentView, viewHasFields, objectMetadataItem, loadRecordIndexStates, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.tsx index 315551063a..ccb0848916 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.tsx +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.tsx @@ -9,10 +9,10 @@ import { } from './PageLayoutTestWrapper'; jest.mock( - '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget', + '@/page-layout/widgets/record-table/hooks/useRemoveDraftViewForRecordTableWidget', () => ({ - useDeleteViewForRecordTableWidget: () => ({ - deleteViewForRecordTableWidget: jest.fn(), + useRemoveDraftViewForRecordTableWidget: () => ({ + removeDraftViewForRecordTableWidget: jest.fn(), }), }), ); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePendingRecordTableWidgetViews.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePendingRecordTableWidgetViews.ts new file mode 100644 index 0000000000..d79631c687 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePendingRecordTableWidgetViews.ts @@ -0,0 +1,141 @@ +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState'; +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; +import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState'; +import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId'; +import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist'; +import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { WidgetType } from '~/generated-metadata/graphql'; + +export const useCreatePendingRecordTableWidgetViews = () => { + const { performViewAPICreate, performViewAPIDestroy } = + usePerformViewAPIPersist(); + const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist(); + const store = useStore(); + + const createPendingRecordTableWidgetViews = useCallback( + async (pageLayoutId: string) => { + const draft = store.get( + pageLayoutDraftComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + ); + const persisted = store.get( + pageLayoutPersistedComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + ); + + const recordTableWidgetViewDraft = store.get( + recordTableWidgetViewDraftComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + ); + + const persistedRecordTableWidgets = new Map( + (persisted?.tabs ?? []) + .flatMap((tab) => tab.widgets) + .filter((widget) => widget.type === WidgetType.RECORD_TABLE) + .map((widget) => [ + widget.id, + getWidgetConfigurationViewId(widget.configuration), + ]), + ); + + const draftRecordTableWidgets = draft.tabs + .flatMap((tab) => tab.widgets) + .filter((widget) => widget.type === WidgetType.RECORD_TABLE); + + const draftWidgetIds = new Set( + draftRecordTableWidgets.map((widget) => widget.id), + ); + + for (const widget of draftRecordTableWidgets) { + const viewId = getWidgetConfigurationViewId(widget.configuration); + + if (!isDefined(viewId)) { + continue; + } + + const persistedViewId = persistedRecordTableWidgets.get(widget.id); + + if (persistedViewId === viewId) { + continue; + } + + if (isDefined(persistedViewId)) { + await performViewAPIDestroy({ id: persistedViewId }); + } + + const widgetViewDraft = recordTableWidgetViewDraft[widget.id]; + + if (!isDefined(widgetViewDraft)) { + continue; + } + + const { view } = widgetViewDraft; + + const result = await performViewAPICreate( + { + input: { + id: view.id, + name: view.name, + icon: view.icon, + objectMetadataId: view.objectMetadataId, + type: view.type, + isCompact: view.isCompact, + position: view.position, + openRecordIn: view.openRecordIn, + visibility: view.visibility, + shouldHideEmptyGroups: view.shouldHideEmptyGroups, + }, + }, + view.objectMetadataId, + ); + + if (result.status === 'failed') { + throw new Error( + `Failed to create view for RECORD_TABLE widget ${widget.id}`, + ); + } + + const viewFieldInputs = widgetViewDraft.viewFields.map((field) => ({ + id: field.id, + viewId: field.viewId, + fieldMetadataId: field.fieldMetadataId, + position: field.position, + size: field.size, + isVisible: field.isVisible, + })); + + if (viewFieldInputs.length > 0) { + await performViewFieldAPICreate({ inputs: viewFieldInputs }); + } + } + + for (const [widgetId, viewId] of persistedRecordTableWidgets) { + if (!draftWidgetIds.has(widgetId) && isDefined(viewId)) { + await performViewAPIDestroy({ id: viewId }); + } + } + + store.set( + recordTableWidgetViewPersistedComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + recordTableWidgetViewDraft, + ); + }, + [ + performViewAPICreate, + performViewAPIDestroy, + performViewFieldAPICreate, + store, + ], + ); + + return { createPendingRecordTableWidgetViews }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts b/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts index ac27f0c070..8bfa57eccc 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useSavePageLayout.ts @@ -1,4 +1,5 @@ import { useCreatePendingFieldsWidgetViews } from '@/page-layout/hooks/useCreatePendingFieldsWidgetViews'; +import { useCreatePendingRecordTableWidgetViews } from '@/page-layout/hooks/useCreatePendingRecordTableWidgetViews'; import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; @@ -45,6 +46,9 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => { const { createPendingFieldsWidgetViews } = useCreatePendingFieldsWidgetViews(); + const { createPendingRecordTableWidgetViews } = + useCreatePendingRecordTableWidgetViews(); + const featureFlags = useFeatureFlagsMap(); const isRecordPageLayoutEditingEnabled = featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED]; @@ -52,6 +56,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => { const savePageLayout = useCallback(async () => { await createPendingFieldsWidgetViews(pageLayoutId); + await createPendingRecordTableWidgetViews(pageLayoutId); const pageLayoutDraft = store.get(pageLayoutDraftCallbackState); const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft, { @@ -91,6 +96,7 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => { return result; }, [ createPendingFieldsWidgetViews, + createPendingRecordTableWidgetViews, isRecordPageLayoutEditingEnabled, pageLayoutCurrentLayoutsCallbackState, pageLayoutDraftCallbackState, diff --git a/packages/twenty-front/src/modules/page-layout/states/recordTableWidgetViewDraftComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/recordTableWidgetViewDraftComponentState.ts new file mode 100644 index 0000000000..ada843df18 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/recordTableWidgetViewDraftComponentState.ts @@ -0,0 +1,11 @@ +import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot'; +import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const recordTableWidgetViewDraftComponentState = + createAtomComponentState>({ + key: 'recordTableWidgetViewDraftComponentState', + defaultValue: {}, + componentInstanceContext: PageLayoutComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/page-layout/states/recordTableWidgetViewPersistedComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/recordTableWidgetViewPersistedComponentState.ts new file mode 100644 index 0000000000..c23d726030 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/recordTableWidgetViewPersistedComponentState.ts @@ -0,0 +1,11 @@ +import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot'; +import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState'; + +import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext'; + +export const recordTableWidgetViewPersistedComponentState = + createAtomComponentState>({ + key: 'recordTableWidgetViewPersistedComponentState', + defaultValue: {}, + componentInstanceContext: PageLayoutComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/page-layout/states/selectors/recordTableWidgetViewDraftByWidgetIdComponentFamilySelector.ts b/packages/twenty-front/src/modules/page-layout/states/selectors/recordTableWidgetViewDraftByWidgetIdComponentFamilySelector.ts new file mode 100644 index 0000000000..3186d24bd6 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/states/selectors/recordTableWidgetViewDraftByWidgetIdComponentFamilySelector.ts @@ -0,0 +1,23 @@ +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; +import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot'; +import { createAtomComponentFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomComponentFamilySelector'; + +import { PageLayoutComponentInstanceContext } from '../contexts/PageLayoutComponentInstanceContext'; + +export const recordTableWidgetViewDraftByWidgetIdComponentFamilySelector = + createAtomComponentFamilySelector< + RecordTableWidgetViewSnapshot | undefined, + { widgetId: string } + >({ + key: 'recordTableWidgetViewDraftByWidgetIdComponentFamilySelector', + componentInstanceContext: PageLayoutComponentInstanceContext, + get: + ({ instanceId, familyKey }) => + ({ get }) => { + const draftMap = get(recordTableWidgetViewDraftComponentState, { + instanceId, + }); + + return draftMap[familyKey.widgetId]; + }, + }); diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/buildFieldsWidgetGroupsFromFlatViewData.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/buildFieldsWidgetGroupsFromFlatViewData.test.ts new file mode 100644 index 0000000000..b0af5bd1cc --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/buildFieldsWidgetGroupsFromFlatViewData.test.ts @@ -0,0 +1,281 @@ +import { type FlatViewField } from '@/metadata-store/types/FlatViewField'; +import { type FlatViewFieldGroup } from '@/metadata-store/types/FlatViewFieldGroup'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { buildFieldsWidgetGroupsFromFlatViewData } from '@/page-layout/utils/buildFieldsWidgetGroupsFromFlatViewData'; +import { FieldMetadataType } from 'twenty-shared/types'; + +const createFieldMetadata = ( + overrides: Partial & { id: string }, +): FieldMetadataItem => + ({ + name: 'field', + label: 'Field', + type: FieldMetadataType.TEXT, + isActive: true, + isSystem: false, + ...overrides, + }) as FieldMetadataItem; + +const createFlatViewField = ( + overrides: Partial & { + id: string; + fieldMetadataId: string; + viewId: string; + }, +): FlatViewField => + ({ + position: 0, + isVisible: true, + isActive: true, + ...overrides, + }) as FlatViewField; + +const createFlatViewFieldGroup = ( + overrides: Partial & { id: string }, +): FlatViewFieldGroup => + ({ + name: 'Group', + position: 0, + isVisible: true, + ...overrides, + }) as FlatViewFieldGroup; + +describe('buildFieldsWidgetGroupsFromFlatViewData', () => { + const fm1 = createFieldMetadata({ id: 'fm-1', name: 'name', label: 'Name' }); + const fm2 = createFieldMetadata({ + id: 'fm-2', + name: 'email', + label: 'Email', + }); + const fm3 = createFieldMetadata({ + id: 'fm-3', + name: 'phone', + label: 'Phone', + }); + + describe('ungrouped mode', () => { + it('should return ungrouped fields sorted by position when no groups exist', () => { + const flatViewFields = [ + createFlatViewField({ + id: 'vf-2', + fieldMetadataId: 'fm-2', + viewId: 'v1', + position: 1, + }), + createFlatViewField({ + id: 'vf-1', + fieldMetadataId: 'fm-1', + viewId: 'v1', + position: 0, + }), + ]; + + const result = buildFieldsWidgetGroupsFromFlatViewData({ + flatViewFieldGroups: [], + flatViewFields, + fieldMetadataItems: [fm1, fm2], + }); + + expect(result.editorMode).toBe('ungrouped'); + expect(result.groups).toEqual([]); + expect(result.ungroupedFields).toHaveLength(2); + expect(result.ungroupedFields[0].fieldMetadataItem.id).toBe('fm-1'); + expect(result.ungroupedFields[1].fieldMetadataItem.id).toBe('fm-2'); + }); + + it('should assign sequential globalIndex based on sorted position', () => { + const flatViewFields = [ + createFlatViewField({ + id: 'vf-1', + fieldMetadataId: 'fm-1', + viewId: 'v1', + position: 5, + }), + createFlatViewField({ + id: 'vf-2', + fieldMetadataId: 'fm-2', + viewId: 'v1', + position: 2, + }), + ]; + + const result = buildFieldsWidgetGroupsFromFlatViewData({ + flatViewFieldGroups: [], + flatViewFields, + fieldMetadataItems: [fm1, fm2], + }); + + expect(result.ungroupedFields[0].globalIndex).toBe(0); + expect(result.ungroupedFields[0].fieldMetadataItem.id).toBe('fm-2'); + expect(result.ungroupedFields[1].globalIndex).toBe(1); + expect(result.ungroupedFields[1].fieldMetadataItem.id).toBe('fm-1'); + }); + + it('should skip fields whose fieldMetadataId has no matching metadata', () => { + const flatViewFields = [ + createFlatViewField({ + id: 'vf-1', + fieldMetadataId: 'fm-1', + viewId: 'v1', + position: 0, + }), + createFlatViewField({ + id: 'vf-orphan', + fieldMetadataId: 'fm-nonexistent', + viewId: 'v1', + position: 1, + }), + ]; + + const result = buildFieldsWidgetGroupsFromFlatViewData({ + flatViewFieldGroups: [], + flatViewFields, + fieldMetadataItems: [fm1], + }); + + expect(result.ungroupedFields).toHaveLength(1); + expect(result.ungroupedFields[0].fieldMetadataItem.id).toBe('fm-1'); + }); + + it('should return empty ungroupedFields when there are no view fields', () => { + const result = buildFieldsWidgetGroupsFromFlatViewData({ + flatViewFieldGroups: [], + flatViewFields: [], + fieldMetadataItems: [fm1], + }); + + expect(result.editorMode).toBe('ungrouped'); + expect(result.ungroupedFields).toHaveLength(0); + }); + }); + + describe('grouped mode', () => { + it('should return grouped fields when groups exist', () => { + const group1 = createFlatViewFieldGroup({ + id: 'g1', + name: 'General', + position: 0, + }); + const group2 = createFlatViewFieldGroup({ + id: 'g2', + name: 'Details', + position: 1, + }); + + const flatViewFields = [ + createFlatViewField({ + id: 'vf-1', + fieldMetadataId: 'fm-1', + viewId: 'v1', + position: 0, + viewFieldGroupId: 'g1', + }), + createFlatViewField({ + id: 'vf-2', + fieldMetadataId: 'fm-2', + viewId: 'v1', + position: 0, + viewFieldGroupId: 'g2', + }), + ]; + + const result = buildFieldsWidgetGroupsFromFlatViewData({ + flatViewFieldGroups: [group1, group2], + flatViewFields, + fieldMetadataItems: [fm1, fm2], + }); + + expect(result.editorMode).toBe('grouped'); + expect(result.ungroupedFields).toEqual([]); + expect(result.groups).toHaveLength(2); + expect(result.groups[0].name).toBe('General'); + expect(result.groups[0].fields).toHaveLength(1); + expect(result.groups[0].fields[0].fieldMetadataItem.id).toBe('fm-1'); + expect(result.groups[1].name).toBe('Details'); + expect(result.groups[1].fields[0].fieldMetadataItem.id).toBe('fm-2'); + }); + + it('should sort fields within each group by position', () => { + const group = createFlatViewFieldGroup({ id: 'g1', name: 'All' }); + + const flatViewFields = [ + createFlatViewField({ + id: 'vf-3', + fieldMetadataId: 'fm-3', + viewId: 'v1', + position: 2, + viewFieldGroupId: 'g1', + }), + createFlatViewField({ + id: 'vf-1', + fieldMetadataId: 'fm-1', + viewId: 'v1', + position: 0, + viewFieldGroupId: 'g1', + }), + createFlatViewField({ + id: 'vf-2', + fieldMetadataId: 'fm-2', + viewId: 'v1', + position: 1, + viewFieldGroupId: 'g1', + }), + ]; + + const result = buildFieldsWidgetGroupsFromFlatViewData({ + flatViewFieldGroups: [group], + flatViewFields, + fieldMetadataItems: [fm1, fm2, fm3], + }); + + expect( + result.groups[0].fields.map((f) => f.fieldMetadataItem.id), + ).toEqual(['fm-1', 'fm-2', 'fm-3']); + }); + + it('should skip fields with missing metadata in grouped mode', () => { + const group = createFlatViewFieldGroup({ id: 'g1', name: 'All' }); + + const flatViewFields = [ + createFlatViewField({ + id: 'vf-1', + fieldMetadataId: 'fm-1', + viewId: 'v1', + position: 0, + viewFieldGroupId: 'g1', + }), + createFlatViewField({ + id: 'vf-orphan', + fieldMetadataId: 'fm-nonexistent', + viewId: 'v1', + position: 1, + viewFieldGroupId: 'g1', + }), + ]; + + const result = buildFieldsWidgetGroupsFromFlatViewData({ + flatViewFieldGroups: [group], + flatViewFields, + fieldMetadataItems: [fm1], + }); + + expect(result.groups[0].fields).toHaveLength(1); + }); + + it('should preserve group visibility in the output', () => { + const group = createFlatViewFieldGroup({ + id: 'g1', + name: 'Hidden Group', + isVisible: false, + }); + + const result = buildFieldsWidgetGroupsFromFlatViewData({ + flatViewFieldGroups: [group], + flatViewFields: [], + fieldMetadataItems: [], + }); + + expect(result.groups[0].isVisible).toBe(false); + }); + }); +}); 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 863d8c6f4a..c525b234d3 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 @@ -1,6 +1,7 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { RecordTableWidget } from '@/object-record/record-table-widget/components/RecordTableWidget'; import { RecordTableWidgetProvider } from '@/object-record/record-table-widget/components/RecordTableWidgetProvider'; +import { RecordTableWidgetViewDraftInitEffect } from '@/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftInitEffect'; type RecordTableWidgetRendererContentProps = { objectMetadataId: string; @@ -18,12 +19,18 @@ export const RecordTableWidgetRendererContent = ({ }); return ( - - - + <> + + + + + ); }; 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 new file mode 100644 index 0000000000..9d32b9d24c --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/components/RecordTableWidgetViewDraftInitEffect.tsx @@ -0,0 +1,23 @@ +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/hooks/useAddDraftViewForRecordTableWidget.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useAddDraftViewForRecordTableWidget.ts new file mode 100644 index 0000000000..1811d97288 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useAddDraftViewForRecordTableWidget.ts @@ -0,0 +1,89 @@ +import { type FlatView } from '@/metadata-store/types/FlatView'; +import { type FlatViewField } from '@/metadata-store/types/FlatViewField'; +import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; +import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; +import { filterFieldsForRecordTableViewCreation } from '@/page-layout/widgets/record-table/utils/filterFieldsForRecordTableViewCreation'; +import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget'; +import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; +import { v4 } from 'uuid'; +import { + ViewOpenRecordIn, + ViewType, + ViewVisibility, + WidgetConfigurationType, +} from '~/generated-metadata/graphql'; + +const DEFAULT_VIEW_FIELD_SIZE = 180; +const INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET = 6; + +export const useAddDraftViewForRecordTableWidget = (pageLayoutId: string) => { + const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId); + + const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState( + recordTableWidgetViewDraftComponentState, + pageLayoutId, + ); + + const store = useStore(); + + const addDraftViewForRecordTableWidget = useCallback( + (widgetId: string, objectMetadataItem: EnrichedObjectMetadataItem) => { + const newViewId = v4(); + + const flatView: FlatView = { + id: newViewId, + name: `${objectMetadataItem.labelPlural} Table`, + icon: objectMetadataItem.icon ?? 'IconTable', + objectMetadataId: objectMetadataItem.id, + type: ViewType.TABLE_WIDGET, + isCompact: false, + position: 0, + openRecordIn: ViewOpenRecordIn.RECORD_PAGE, + visibility: ViewVisibility.UNLISTED, + shouldHideEmptyGroups: false, + }; + + const eligibleFields = objectMetadataItem.fields.filter( + filterFieldsForRecordTableViewCreation, + ); + + const sortedFields = eligibleFields.toSorted( + sortFieldsByRelevanceForRecordTableWidget( + objectMetadataItem.labelIdentifierFieldMetadataId, + ), + ); + + const flatViewFields: FlatViewField[] = sortedFields.map( + (field, index) => ({ + id: v4(), + viewId: newViewId, + fieldMetadataId: field.id, + position: index, + size: DEFAULT_VIEW_FIELD_SIZE, + isVisible: index < INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET, + isActive: true, + }), + ); + + store.set(recordTableWidgetViewDraftState, (prev) => ({ + ...prev, + [widgetId]: { view: flatView, viewFields: flatViewFields }, + })); + + requestAnimationFrame(() => { + updatePageLayoutWidget(widgetId, { + configuration: { + configurationType: WidgetConfigurationType.RECORD_TABLE, + viewId: newViewId, + }, + }); + }); + }, + [store, recordTableWidgetViewDraftState, updatePageLayoutWidget], + ); + + return { addDraftViewForRecordTableWidget }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget.ts deleted file mode 100644 index 8f977cb5c4..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; -import { filterFieldsForRecordTableViewCreation } from '@/page-layout/widgets/record-table/utils/filterFieldsForRecordTableViewCreation'; -import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget'; -import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; -import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist'; -import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist'; -import { useCallback } from 'react'; -import { v4 } from 'uuid'; -import { - WidgetConfigurationType, - ViewType, -} from '~/generated-metadata/graphql'; - -const DEFAULT_VIEW_FIELD_SIZE = 180; -const INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET = 6; - -export const useCreateViewForRecordTableWidget = (pageLayoutId: string) => { - const { performViewAPICreate } = usePerformViewAPIPersist(); - const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist(); - const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId); - - const createViewForRecordTableWidget = useCallback( - async ( - widgetId: string, - objectMetadataItem: EnrichedObjectMetadataItem, - ) => { - const newViewId = v4(); - - const viewResult = await performViewAPICreate( - { - input: { - id: newViewId, - name: `${objectMetadataItem.labelPlural} Table`, - icon: objectMetadataItem.icon ?? 'IconTable', - objectMetadataId: objectMetadataItem.id, - type: ViewType.TABLE_WIDGET, - }, - }, - objectMetadataItem.id, - ); - - if (viewResult.status !== 'successful') { - return; - } - - const eligibleFields = objectMetadataItem.fields.filter( - filterFieldsForRecordTableViewCreation, - ); - - const sortedFields = eligibleFields.toSorted( - sortFieldsByRelevanceForRecordTableWidget( - objectMetadataItem.labelIdentifierFieldMetadataId, - ), - ); - - const viewFieldInputs = sortedFields.map((field, index) => ({ - id: v4(), - viewId: newViewId, - fieldMetadataId: field.id, - position: index, - size: DEFAULT_VIEW_FIELD_SIZE, - isVisible: index < INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET, - })); - - try { - await performViewFieldAPICreate({ inputs: viewFieldInputs }); - - updatePageLayoutWidget(widgetId, { - configuration: { - configurationType: WidgetConfigurationType.RECORD_TABLE, - viewId: newViewId, - }, - }); - } catch (error) { - throw new Error( - 'Failed to create view fields for record table widget', - { cause: error }, - ); - } - }, - [performViewAPICreate, performViewFieldAPICreate, updatePageLayoutWidget], - ); - - return { createViewForRecordTableWidget }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget.ts deleted file mode 100644 index 7f78fc1f00..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist'; -import { useCallback } from 'react'; - -export const useDeleteViewForRecordTableWidget = () => { - const { performViewAPIDestroy } = usePerformViewAPIPersist(); - - const deleteViewForRecordTableWidget = useCallback( - async (viewId: string) => { - await performViewAPIDestroy({ id: viewId }); - }, - [performViewAPIDestroy], - ); - - return { deleteViewForRecordTableWidget }; -}; 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 new file mode 100644 index 0000000000..247bf2bf6d --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useInitializeRecordTableWidgetViewDraft.ts @@ -0,0 +1,71 @@ +import { type FlatViewField } from '@/metadata-store/types/FlatViewField'; +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; +import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState'; +import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot'; +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 { viewFields, ...viewProps } = view; + + const flatViewFields: FlatViewField[] = viewFields.map((field) => ({ + ...field, + viewId: view.id, + })); + + const snapshot: RecordTableWidgetViewSnapshot = { + view: viewProps, + viewFields: flatViewFields, + }; + + 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/useRecordTableWidgetViewFieldItems.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewFieldItems.ts index 6e57335b34..e18461de0e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewFieldItems.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewFieldItems.ts @@ -1,18 +1,33 @@ import { useMapViewFieldToRecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/hooks/useMapViewFieldToRecordTableWidgetViewFieldItem'; +import { useRecordTableWidgetViewForDisplay } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay'; import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem'; -import { useViewById } from '@/views/hooks/useViewById'; import { useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { sortByProperty } from '~/utils/array/sortByProperty'; -export const useRecordTableWidgetViewFieldItems = (viewId: string) => { - const { view } = useViewById(viewId); +type UseRecordTableWidgetViewFieldItemsParams = { + viewId: string; + widgetId: string; + pageLayoutId: string; +}; + +export const useRecordTableWidgetViewFieldItems = ({ + viewId, + widgetId, + pageLayoutId, +}: UseRecordTableWidgetViewFieldItemsParams) => { + const { view } = useRecordTableWidgetViewForDisplay({ + viewId, + widgetId, + pageLayoutId, + }); + const { mapViewFieldToRecordTableWidgetViewFieldItem } = useMapViewFieldToRecordTableWidgetViewFieldItem(); const recordTableWidgetViewFieldItems: RecordTableWidgetViewFieldItem[] = useMemo(() => { - if (!view) { + if (!isDefined(view)) { return []; } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay.ts new file mode 100644 index 0000000000..b5ba92e00c --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay.ts @@ -0,0 +1,35 @@ +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; +import { constructViewFromRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useViewById } from '@/views/hooks/useViewById'; +import { type View } from '@/views/types/View'; +import { isDefined } from 'twenty-shared/utils'; + +type UseRecordTableWidgetViewForDisplayParams = { + viewId: string; + widgetId: string; + pageLayoutId: string; +}; + +export const useRecordTableWidgetViewForDisplay = ({ + viewId, + widgetId, + pageLayoutId, +}: UseRecordTableWidgetViewForDisplayParams): { + view: View | undefined; +} => { + const { view } = useViewById(viewId); + + const recordTableWidgetViewDraft = useAtomComponentStateValue( + recordTableWidgetViewDraftComponentState, + pageLayoutId, + ); + + const draftSnapshot = recordTableWidgetViewDraft[widgetId]; + + const viewFromDraft = isDefined(draftSnapshot) + ? constructViewFromRecordTableWidgetViewSnapshot(draftSnapshot) + : undefined; + + return { view: viewFromDraft ?? view }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useRemoveDraftViewForRecordTableWidget.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useRemoveDraftViewForRecordTableWidget.ts new file mode 100644 index 0000000000..4ab2cfdfc5 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useRemoveDraftViewForRecordTableWidget.ts @@ -0,0 +1,27 @@ +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; +import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; + +export const useRemoveDraftViewForRecordTableWidget = ( + pageLayoutId: string, +) => { + const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState( + recordTableWidgetViewDraftComponentState, + pageLayoutId, + ); + + const store = useStore(); + + const removeDraftViewForRecordTableWidget = useCallback( + (widgetId: string) => { + store.set(recordTableWidgetViewDraftState, (prev) => { + const { [widgetId]: _, ...rest } = prev; + return rest; + }); + }, + [store, recordTableWidgetViewDraftState], + ); + + return { removeDraftViewForRecordTableWidget }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useReorderRecordTableWidgetFields.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useReorderRecordTableWidgetFields.ts index 75b2292896..82f7a788af 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useReorderRecordTableWidgetFields.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useReorderRecordTableWidgetFields.ts @@ -1,12 +1,28 @@ +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem'; -import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist'; +import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { useStore } from 'jotai'; import { useCallback } from 'react'; +import { isDefined } from 'twenty-shared/utils'; -export const useReorderRecordTableWidgetFields = () => { - const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist(); +type UseReorderRecordTableWidgetFieldsParams = { + pageLayoutId: string; + widgetId: string; +}; + +export const useReorderRecordTableWidgetFields = ({ + pageLayoutId, + widgetId, +}: UseReorderRecordTableWidgetFieldsParams) => { + const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState( + recordTableWidgetViewDraftComponentState, + pageLayoutId, + ); + + const store = useStore(); const reorderRecordTableWidgetFields = useCallback( - async ( + ( sourceIndex: number, destinationIndex: number, visibleFieldItems: RecordTableWidgetViewFieldItem[], @@ -19,16 +35,35 @@ export const useReorderRecordTableWidgetFields = () => { const [movedField] = reorderedFields.splice(sourceIndex, 1); reorderedFields.splice(destinationIndex, 0, movedField); - const updates = reorderedFields.map((fieldItem, index) => ({ - input: { - id: fieldItem.viewField.id, - update: { position: index }, - }, - })); + const updatedPositions = new Map( + reorderedFields.map((fieldItem, index) => [ + fieldItem.viewField.id, + index, + ]), + ); - await performViewFieldAPIUpdate(updates); + store.set(recordTableWidgetViewDraftState, (prev) => { + const widgetViewDraft = prev[widgetId]; + + if (!isDefined(widgetViewDraft)) { + return prev; + } + + return { + ...prev, + [widgetId]: { + ...widgetViewDraft, + viewFields: widgetViewDraft.viewFields.map((field) => { + const newPosition = updatedPositions.get(field.id); + return newPosition !== undefined + ? { ...field, position: newPosition } + : field; + }), + }, + }; + }); }, - [performViewFieldAPIUpdate], + [store, recordTableWidgetViewDraftState, widgetId], ); return { reorderRecordTableWidgetFields }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useSaveRecordTableWidgetsViewDataOnDashboardSave.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useSaveRecordTableWidgetsViewDataOnDashboardSave.ts index e54a642d59..1b47fe1b6f 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useSaveRecordTableWidgetsViewDataOnDashboardSave.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useSaveRecordTableWidgetsViewDataOnDashboardSave.ts @@ -5,6 +5,7 @@ import { currentRecordFiltersComponentState } from '@/object-record/record-filte import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; import { useMapRecordFieldToViewFieldWithCurrentAggregateOperation } from '@/page-layout/widgets/record-table/hooks/useMapRecordFieldToViewFieldWithCurrentAggregateOperation'; import { computeViewFieldsToCreateAndUpdate } from '@/page-layout/widgets/record-table/utils/computeViewFieldsToCreateAndUpdate'; import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist'; @@ -65,6 +66,11 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => { ); const views = store.get(viewsSelector.atom); const objectMetadataItems = store.get(objectMetadataItemsSelector.atom); + const recordTableWidgetViewDraft = store.get( + recordTableWidgetViewDraftComponentState.atomFamily({ + instanceId: pageLayoutId, + }), + ); const recordTableWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets.filter( @@ -182,16 +188,21 @@ export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => { }), ); - const newViewFields = currentRecordFields.map( + const recordIndexViewFields = currentRecordFields.map( mapRecordFieldToViewFieldWithCurrentAggregateOperation, ); - const existingViewFields = currentView.viewFields ?? []; + const draftSnapshot = recordTableWidgetViewDraft[widget.id]; + const draftViewFields = draftSnapshot?.viewFields ?? []; + const metadataStoreViewFields = + draftViewFields.length > 0 + ? draftViewFields + : (currentView.viewFields ?? []); const { viewFieldsToCreate, viewFieldsToUpdate } = computeViewFieldsToCreateAndUpdate({ - newViewFields, - existingViewFields, + newViewFields: metadataStoreViewFields, + existingViewFields: recordIndexViewFields, viewId, }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useToggleRecordTableWidgetFieldVisibility.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useToggleRecordTableWidgetFieldVisibility.ts index 731baa321a..304d169ed6 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useToggleRecordTableWidgetFieldVisibility.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/hooks/useToggleRecordTableWidgetFieldVisibility.ts @@ -1,21 +1,46 @@ -import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist'; +import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState'; +import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { useStore } from 'jotai'; import { useCallback } from 'react'; +import { isDefined } from 'twenty-shared/utils'; -export const useToggleRecordTableWidgetFieldVisibility = () => { - const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist(); +type UseToggleRecordTableWidgetFieldVisibilityParams = { + pageLayoutId: string; + widgetId: string; +}; + +export const useToggleRecordTableWidgetFieldVisibility = ({ + pageLayoutId, + widgetId, +}: UseToggleRecordTableWidgetFieldVisibilityParams) => { + const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState( + recordTableWidgetViewDraftComponentState, + pageLayoutId, + ); + + const store = useStore(); const toggleRecordTableWidgetFieldVisibility = useCallback( - async (viewFieldId: string, isVisible: boolean) => { - await performViewFieldAPIUpdate([ - { - input: { - id: viewFieldId, - update: { isVisible }, + (viewFieldId: string, isVisible: boolean) => { + store.set(recordTableWidgetViewDraftState, (prev) => { + const widgetViewDraft = prev[widgetId]; + + if (!isDefined(widgetViewDraft)) { + return prev; + } + + return { + ...prev, + [widgetId]: { + ...widgetViewDraft, + viewFields: widgetViewDraft.viewFields.map((field) => + field.id === viewFieldId ? { ...field, isVisible } : field, + ), }, - }, - ]); + }; + }); }, - [performViewFieldAPIUpdate], + [store, recordTableWidgetViewDraftState, widgetId], ); return { toggleRecordTableWidgetFieldVisibility }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot.ts new file mode 100644 index 0000000000..a33e4667f3 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot.ts @@ -0,0 +1,7 @@ +import { type FlatView } from '@/metadata-store/types/FlatView'; +import { type FlatViewField } from '@/metadata-store/types/FlatViewField'; + +export type RecordTableWidgetViewSnapshot = { + view: FlatView; + viewFields: FlatViewField[]; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/sortFieldsByRelevanceForRecordTableWidget.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/sortFieldsByRelevanceForRecordTableWidget.test.ts new file mode 100644 index 0000000000..09ae67f3ec --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/sortFieldsByRelevanceForRecordTableWidget.test.ts @@ -0,0 +1,130 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget'; +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; + +const createField = ( + overrides: Partial & { id: string }, +): FieldMetadataItem => + ({ + name: 'field', + label: 'Field', + type: FieldMetadataType.TEXT, + isActive: true, + isSystem: false, + settings: null, + ...overrides, + }) as FieldMetadataItem; + +describe('sortFieldsByRelevanceForRecordTableWidget', () => { + const labelIdentifierId = 'label-field-id'; + const sorter = sortFieldsByRelevanceForRecordTableWidget(labelIdentifierId); + + it('should place the label identifier field first', () => { + const labelField = createField({ id: labelIdentifierId }); + const textField = createField({ id: 'text-1' }); + + expect(sorter(labelField, textField)).toBe(-1); + expect(sorter(textField, labelField)).toBe(1); + }); + + it('should place ONE_TO_MANY relation fields after non-relation fields', () => { + const reverseSide = createField({ + id: 'reverse-1', + type: FieldMetadataType.RELATION, + settings: { relationType: RelationType.ONE_TO_MANY }, + }); + const textField = createField({ id: 'text-1' }); + + expect(sorter(reverseSide, textField)).toBe(1); + expect(sorter(textField, reverseSide)).toBe(-1); + }); + + it('should place regular relation fields after non-relation fields', () => { + const relation = createField({ + id: 'rel-1', + type: FieldMetadataType.RELATION, + settings: { relationType: RelationType.MANY_TO_ONE }, + }); + const textField = createField({ id: 'text-1' }); + + expect(sorter(relation, textField)).toBe(1); + expect(sorter(textField, relation)).toBe(-1); + }); + + it('should place ONE_TO_MANY relations after MANY_TO_ONE relations', () => { + const reverseSide = createField({ + id: 'reverse-1', + type: FieldMetadataType.RELATION, + settings: { relationType: RelationType.ONE_TO_MANY }, + }); + const manyToOne = createField({ + id: 'rel-1', + type: FieldMetadataType.RELATION, + settings: { relationType: RelationType.MANY_TO_ONE }, + }); + + expect(sorter(reverseSide, manyToOne)).toBe(1); + expect(sorter(manyToOne, reverseSide)).toBe(-1); + }); + + it('should return 0 for two non-relation fields of equal priority', () => { + const fieldA = createField({ id: 'a' }); + const fieldB = createField({ id: 'b' }); + + expect(sorter(fieldA, fieldB)).toBe(0); + }); + + it('should return 0 for two ONE_TO_MANY relation fields', () => { + const reverseA = createField({ + id: 'reverse-a', + type: FieldMetadataType.RELATION, + settings: { relationType: RelationType.ONE_TO_MANY }, + }); + const reverseB = createField({ + id: 'reverse-b', + type: FieldMetadataType.RELATION, + settings: { relationType: RelationType.ONE_TO_MANY }, + }); + + expect(sorter(reverseA, reverseB)).toBe(0); + }); + + it('should produce a correct full sort order', () => { + const labelField = createField({ id: labelIdentifierId }); + const textField = createField({ id: 'text-1' }); + const numberField = createField({ + id: 'number-1', + type: FieldMetadataType.NUMBER, + }); + const manyToOneField = createField({ + id: 'rel-1', + type: FieldMetadataType.RELATION, + settings: { relationType: RelationType.MANY_TO_ONE }, + }); + const oneToManyField = createField({ + id: 'reverse-1', + type: FieldMetadataType.RELATION, + settings: { relationType: RelationType.ONE_TO_MANY }, + }); + + const fields = [ + oneToManyField, + manyToOneField, + textField, + numberField, + labelField, + ]; + + const sorted = [...fields].sort(sorter); + + expect(sorted[0].id).toBe(labelIdentifierId); + expect(sorted[sorted.length - 1].id).toBe('reverse-1'); + const labelIdx = sorted.findIndex((f) => f.id === labelIdentifierId); + const textIdx = sorted.findIndex((f) => f.id === 'text-1'); + const relIdx = sorted.findIndex((f) => f.id === 'rel-1'); + const reverseIdx = sorted.findIndex((f) => f.id === 'reverse-1'); + expect(labelIdx).toBeLessThan(textIdx); + expect(textIdx).toBeLessThan(relIdx); + expect(relIdx).toBeLessThan(reverseIdx); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot.ts new file mode 100644 index 0000000000..2de2adfbee --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot.ts @@ -0,0 +1,13 @@ +import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot'; +import { type View } from '@/views/types/View'; + +export const constructViewFromRecordTableWidgetViewSnapshot = ( + snapshot: RecordTableWidgetViewSnapshot, +): View => ({ + ...snapshot.view, + viewFields: snapshot.viewFields, + viewFilters: [], + viewSorts: [], + viewGroups: [], + viewFilterGroups: [], +}); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutDashboardWidgetTypeSelect.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutDashboardWidgetTypeSelect.tsx index de63a22491..c0516bf55c 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutDashboardWidgetTypeSelect.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutDashboardWidgetTypeSelect.tsx @@ -11,7 +11,7 @@ import { useRemovePageLayoutWidgetAndPreservePosition } from '@/page-layout/hook import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord'; -import { useCreateViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget'; +import { useAddDraftViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useAddDraftViewForRecordTableWidget'; import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; import { SidePanelList } from '@/side-panel/components/SidePanelList'; import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; @@ -83,8 +83,8 @@ export const SidePanelPageLayoutDashboardWidgetTypeSelect = () => { const { removePageLayoutWidgetAndPreservePosition } = useRemovePageLayoutWidgetAndPreservePosition(pageLayoutId); - const { createViewForRecordTableWidget } = - useCreateViewForRecordTableWidget(pageLayoutId); + const { addDraftViewForRecordTableWidget } = + useAddDraftViewForRecordTableWidget(pageLayoutId); const { readableObjectMetadataItems } = useReadableObjectMetadataItems(); const firstAvailableObjectMetadataItem = @@ -185,7 +185,7 @@ export const SidePanelPageLayoutDashboardWidgetTypeSelect = () => { closeSidePanelMenu(); }; - const handleNavigateToRecordTableSettings = async () => { + const handleNavigateToRecordTableSettings = () => { if ( isExistingWidgetMissingOrDifferentType( existingWidget?.type, @@ -202,7 +202,7 @@ export const SidePanelPageLayoutDashboardWidgetTypeSelect = () => { setPageLayoutEditingWidgetId(newRecordTableWidget.id); - await createViewForRecordTableWidget( + addDraftViewForRecordTableWidget( newRecordTableWidget.id, firstAvailableObjectMetadataItem, ); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableDataSourceDropdownContent.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableDataSourceDropdownContent.tsx index 5a0ef44b90..440700affc 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableDataSourceDropdownContent.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableDataSourceDropdownContent.tsx @@ -3,8 +3,8 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat import { filterReadableActiveObjectMetadataItems } from '@/object-metadata/utils/filterReadableActiveObjectMetadataItems'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; -import { useCreateViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget'; -import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget'; +import { useAddDraftViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useAddDraftViewForRecordTableWidget'; +import { useRemoveDraftViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useRemoveDraftViewForRecordTableWidget'; import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore'; import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig'; import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode'; @@ -47,11 +47,11 @@ export const RecordTableDataSourceDropdownContent = () => { const { updateCurrentWidgetConfig } = useUpdateCurrentWidgetConfig(pageLayoutId); - const { createViewForRecordTableWidget } = - useCreateViewForRecordTableWidget(pageLayoutId); + const { addDraftViewForRecordTableWidget } = + useAddDraftViewForRecordTableWidget(pageLayoutId); - const { deleteViewForRecordTableWidget } = - useDeleteViewForRecordTableWidget(); + const { removeDraftViewForRecordTableWidget } = + useRemoveDraftViewForRecordTableWidget(pageLayoutId); const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId); @@ -77,20 +77,14 @@ export const RecordTableDataSourceDropdownContent = () => { getSearchableValues: (item) => [item.labelPlural, item.namePlural], }); - const currentViewId = - widgetInEditMode?.configuration && - 'viewId' in widgetInEditMode.configuration - ? (widgetInEditMode.configuration.viewId as string | undefined) - : undefined; - - const handleSelectSource = async (newObjectMetadataItemId: string) => { + const handleSelectSource = (newObjectMetadataItemId: string) => { if (currentObjectMetadataItemId === newObjectMetadataItemId) { closeDropdown(); return; } - if (isDefined(currentViewId)) { - await deleteViewForRecordTableWidget(currentViewId); + if (isDefined(widgetInEditMode)) { + removeDraftViewForRecordTableWidget(widgetInEditMode.id); } updateCurrentWidgetConfig({ @@ -105,7 +99,7 @@ export const RecordTableDataSourceDropdownContent = () => { ); if (isDefined(selectedObjectMetadataItem) && isDefined(widgetInEditMode)) { - await createViewForRecordTableWidget( + addDraftViewForRecordTableWidget( widgetInEditMode.id, selectedObjectMetadataItem, ); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFieldVisibility.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFieldVisibility.tsx index b2eb1ad981..ea66abd997 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFieldVisibility.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFieldVisibility.tsx @@ -27,19 +27,25 @@ const StyledSectionLabel = styled.div` type RecordTableSettingsFieldVisibilityProps = { viewId: string; + widgetId: string; + pageLayoutId: string; }; export const RecordTableSettingsFieldVisibility = ({ viewId, + widgetId, + pageLayoutId, }: RecordTableSettingsFieldVisibilityProps) => { const { recordTableWidgetViewFieldItems } = - useRecordTableWidgetViewFieldItems(viewId); + useRecordTableWidgetViewFieldItems({ viewId, widgetId, pageLayoutId }); const { toggleRecordTableWidgetFieldVisibility } = - useToggleRecordTableWidgetFieldVisibility(); + useToggleRecordTableWidgetFieldVisibility({ pageLayoutId, widgetId }); - const { reorderRecordTableWidgetFields } = - useReorderRecordTableWidgetFields(); + const { reorderRecordTableWidgetFields } = useReorderRecordTableWidgetFields({ + pageLayoutId, + widgetId, + }); const { getIcon } = useIcons(); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFilters.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFilters.tsx index bab6000270..bb45cd073b 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFilters.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFilters.tsx @@ -3,9 +3,9 @@ import { AdvancedFilterSidePanelContainer } from '@/object-record/advanced-filte import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; +import { useRecordTableWidgetViewForDisplay } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay'; import { RecordTableSettingsFiltersInitializeStateEffect } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFiltersInitializeStateEffect'; import { InputLabel } from '@/ui/input/components/InputLabel'; -import { useViewById } from '@/views/hooks/useViewById'; import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; @@ -20,14 +20,22 @@ const StyledFilterSettingsContainer = styled.div` type RecordTableSettingsFiltersProps = { viewId: string; + widgetId: string; + pageLayoutId: string; objectMetadataId: string; }; export const RecordTableSettingsFilters = ({ viewId, + widgetId, + pageLayoutId, objectMetadataId, }: RecordTableSettingsFiltersProps) => { - const { view } = useViewById(viewId); + const { view } = useRecordTableWidgetViewForDisplay({ + viewId, + widgetId, + pageLayoutId, + }); const { objectMetadataItem } = useObjectMetadataItemById({ objectId: objectMetadataId, }); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsSorts.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsSorts.tsx index e71052f6ed..d9510374b1 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsSorts.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsSorts.tsx @@ -2,10 +2,10 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMeta import { filterSortableFieldMetadataItems } from '@/object-metadata/utils/filterSortableFieldMetadataItems'; import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; +import { useRecordTableWidgetViewForDisplay } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewForDisplay'; import { RecordTableSettingsSortsContent } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsSortsContent'; import { RecordTableSettingsSortsInitializeStateEffect } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsSortsInitializeStateEffect'; import { InputLabel } from '@/ui/input/components/InputLabel'; -import { useViewById } from '@/views/hooks/useViewById'; import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; @@ -23,14 +23,22 @@ const StyledSortSettingsContainer = styled.div` type RecordTableSettingsSortsProps = { viewId: string; + widgetId: string; + pageLayoutId: string; objectMetadataId: string; }; export const RecordTableSettingsSorts = ({ viewId, + widgetId, + pageLayoutId, objectMetadataId, }: RecordTableSettingsSortsProps) => { - const { view } = useViewById(viewId); + const { view } = useRecordTableWidgetViewForDisplay({ + viewId, + widgetId, + pageLayoutId, + }); const { objectMetadataItem } = useObjectMetadataItemById({ objectId: objectMetadataId, }); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableFieldsSubPage.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableFieldsSubPage.tsx index 1ba197225d..c35269db13 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableFieldsSubPage.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableFieldsSubPage.tsx @@ -28,5 +28,11 @@ export const SidePanelRecordTableFieldsSubPage = () => { return null; } - return ; + return ( + + ); }; diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableFilterSubPage.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableFilterSubPage.tsx index 566af6f0ff..5e492e81d5 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableFilterSubPage.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableFilterSubPage.tsx @@ -31,6 +31,8 @@ export const SidePanelRecordTableFilterSubPage = () => { return ( ); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableSortSubPage.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableSortSubPage.tsx index 3c74f677bc..adbdcc3a34 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableSortSubPage.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableSortSubPage.tsx @@ -32,6 +32,8 @@ export const SidePanelRecordTableSortSubPage = () => { return ( );