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 (
);