From ab881350b29a491dd7178791370c66cc14f2dba7 Mon Sep 17 00:00:00 2001
From: nitin <142569587+ehconitin@users.noreply.github.com>
Date: Tue, 17 Mar 2026 20:54:09 +0530
Subject: [PATCH] refactor: unify layout customization mode (record pages +
navigation) (#18640)
### What
Unifies record page layout editing and navigation menu editing into a
single global "layout customization" session. Dashboard editing stays
separate.
### How it works
**Two edit mode systems, one context-based read:**
- `isLayoutCustomizationModeEnabledState` -- global atom for record
pages + navigation
- `isDashboardInEditModeComponentState` -- dashboard-only, independent
per-component atom
- `PageLayoutEditModeProvider` -- context that dispatches to
`RecordPageLayoutEditModeProvider` (reads global atom) or
`DashboardPageLayoutEditModeProvider` (reads component atom), one
component per file
**Session registry + independent atoms:**
- `activeCustomizationPageLayoutIdsState` -- accumulates page layout IDs
as user navigates during customization (`string[]`)
- Save/cancel iterate the ID list and read each layout's draft/persisted
atoms independently
- Follows the same pattern as `settingsRoleIdsState` +
`settingsDraftRoleFamilyState`
**Unified UI:**
- `LayoutCustomizationBar` replaces the old `NavigationMenuEditModeBar`
- Enter once -- edit record layouts + navigation -- save/cancel
everything together
- `useSaveLayoutCustomization` orchestrates sequential save: navigation
draft -- page layouts -- field widget groups
- Error snackbar on partial save failure (with TODO for future atomic
server mutation)
**Draft protection during customization:**
- `PageLayoutRelationWidgetsSyncEffect` guarded -- only updates
persisted state from server, skips draft/currentLayouts while
customization is active
- `useExecuteTasksOnAnyLocationChange` skips draft reset when
customization mode is enabled
- Command execution blocked during layout customization
### Cleanup
- Deleted `NavigationMenuEditModeBar`,
`isNavigationMenuInEditModeState`,
`isPageLayoutInEditModeComponentState`,
`useIsGlobalLayoutCustomizationActive`
- `DraftPageLayout` type changed from `Omit` to `Pick` (explicit fields)
- Removed save/cancel from `DefaultRecordCommandMenuItemsConfig` (bar
handles it now)
- Extracted `useSaveFieldsWidgetGroups` from save orchestration
- Split `PageLayoutEditModeProvider` into 3 separate files (one
component per file, Twenty convention)
### Known issues
- **Stale deleted widget after save (pre-existing on `main`)**: Delete
widget -- save -- exit customization -- Apollo cache stale -- sync
effect overwrites Jotai from stale data -- widget reappears until
refresh. Separate PR needed, likely tied to the planned server-side
`saveLayoutCustomization` atomic endpoint.
### Open questions
- **Module location**: Layout customization hooks/states live in `/app`
-- should they move to their own `modules/layout-customization/`?
- **Atomic server mutation**: All save mutations are on metadata schema
(`createNavigationMenuItem`, `deleteNavigationMenuItem`,
`updateNavigationMenuItem`, `updatePageLayoutWithTabsAndWidgets`,
`upsertFieldsWidget`). A single `saveLayoutCustomization` endpoint could
make saves truly atomic.
https://github.com/user-attachments/assets/036ef542-97f3-485b-a68f-3726002c81fb
---
.../src/generated-metadata/graphql.ts | 2 -
...seExecuteTasksOnAnyLocationChange.test.tsx | 93 +++++++
.../useExecuteTasksOnAnyLocationChange.ts | 14 +-
.../EngineComponentKeyComponentMap.tsx | 8 -
.../components/CommandDropdownItem.tsx | 7 +
.../display/components/CommandListItem.tsx | 4 +-
.../components/CommandMenuItemButton.tsx | 11 +-
.../components/CommandMenuItemDisplay.tsx | 36 ++-
.../HeadlessFrontComponentCommandMenuItem.tsx | 8 +-
...lockedByGlobalLayoutCustomization.test.tsx | 87 +++++++
.../useRegisteredCommandMenuItems.test.tsx | 186 ++++++++++++++
...mmandBlockedByGlobalLayoutCustomization.ts | 17 ++
.../DefaultRecordCommandMenuItemsConfig.tsx | 59 -----
...igationSidebarNoSelectionRecordCommand.tsx | 9 +-
...celRecordPageLayoutSingleRecordCommand.tsx | 31 ---
...ditRecordPageLayoutSingleRecordCommand.tsx | 15 +-
...aveRecordPageLayoutSingleRecordCommand.tsx | 38 ---
...RecordPageLayoutSingleRecordCommandKeys.ts | 2 -
.../types/CommandMenuItemConfig.ts | 1 +
.../components/CommandMenuButton.tsx | 4 +
.../components/LayoutCustomizationBar.tsx | 78 ++++++
.../useIsLayoutCustomizationDirty.test.tsx | 224 +++++++++++++++++
.../hooks/useCancelLayoutCustomization.ts | 99 ++++++++
.../hooks/useEnterLayoutCustomizationMode.ts | 35 +++
.../hooks/useExitLayoutCustomizationMode.ts | 42 ++++
.../hooks/useIsLayoutCustomizationDirty.ts | 97 ++++++++
.../hooks/useSaveLayoutCustomization.ts | 167 +++++++++++++
.../activeCustomizationPageLayoutIdsState.ts | 6 +
.../isLayoutCustomizationModeEnabledState.ts | 6 +
.../states/isNavigationMenuInEditModeState.ts | 6 -
.../dnd/hooks/useHandleAddToNavigationDrop.ts | 10 +-
...eWorkspaceNavigationMenuItemDragAndDrop.ts | 8 +-
...rkspaceNavigationMenuItemFolderSubItem.tsx | 8 +-
.../WorkspaceNavigationMenuItemsFolder.tsx | 17 +-
.../hooks/useNavigationMenuItemsData.ts | 8 +-
.../NavigationMenuItemLinkDisplay.tsx | 18 +-
...igationDrawerItemForObjectMetadataItem.tsx | 16 +-
...vigationDrawerSectionForWorkspaceItems.tsx | 10 +-
...awerSectionForWorkspaceItemsListDndKit.tsx | 12 +-
.../components/FavoritesSectionDispatcher.tsx | 8 +-
.../WorkspaceNavigationMenuItems.tsx | 33 +--
.../components/NavigationMenuEditModeBar.tsx | 124 ----------
.../hooks/useNavigationMenuItemsDraftState.ts | 10 +-
.../PageDragDropProviderMountEffect.tsx | 10 +-
.../read-only/hooks/useIsRecordReadOnly.ts | 8 +-
.../RecordTableEmptyStateDisplay.tsx | 8 +-
...bleHeaderLabelIdentifierCellPlusButton.tsx | 8 +-
.../DashboardPageLayoutEditModeProvider.tsx | 21 ++
.../components/PageLayoutContent.tsx | 7 +-
.../components/PageLayoutEditModeProvider.tsx | 30 +++
.../components/PageLayoutGridLayout.tsx | 6 +-
.../PageLayoutInitializationQueryEffect.tsx | 12 +-
...CustomizationSessionRegistrationEffect.tsx | 42 ++++
.../PageLayoutRelationWidgetsSyncEffect.tsx | 105 +++++++-
.../components/PageLayoutRenderer.tsx | 14 +-
.../components/PageLayoutRendererContent.tsx | 6 +-
.../components/PageLayoutTabList.tsx | 7 +-
...youtTabListReorderableOverflowDropdown.tsx | 7 +-
.../RecordPageLayoutEditModeProvider.tsx | 24 ++
.../__stories__/PageLayoutTabList.stories.tsx | 13 +-
.../contexts/PageLayoutEditModeContext.ts | 10 +
.../hooks/__tests__/PageLayoutTestWrapper.tsx | 40 ++-
.../useDeletePageLayoutWidget.test.tsx | 62 ++++-
.../useIsPageLayoutInEditMode.test.tsx | 111 +++++++++
.../useSetIsPageLayoutInEditMode.test.tsx | 104 ++++++++
.../page-layout/hooks/useCurrentPageLayout.ts | 6 +-
.../hooks/useDeletePageLayoutWidget.ts | 25 +-
.../useIsDashboardPageLayoutInEditMode.ts | 13 +
.../hooks/useIsPageLayoutInEditMode.ts | 7 +
...ePageLayoutTabWithVisibleWidgetsOrThrow.ts | 7 +-
...setDraftPageLayoutToPersistedPageLayout.ts | 8 +-
.../hooks/useSaveFieldsWidgetGroups.ts | 233 +++++++-----------
.../hooks/useSetIsPageLayoutInEditMode.ts | 30 ++-
...=> isDashboardInEditModeComponentState.ts} | 4 +-
.../states/pageLayoutDraftComponentState.ts | 1 +
.../page-layout/types/DraftPageLayout.ts | 9 +-
.../components/DashboardWidgetPlaceholder.tsx | 16 +-
.../widgets/components/WidgetRenderer.tsx | 6 +-
.../__stories__/WidgetRenderer.stories.tsx | 10 +-
.../hooks/useFieldsWidgetGroupsForDisplay.ts | 6 +-
.../useFieldsWidgetHiddenFieldsForDisplay.ts | 6 +-
.../FrontComponentWidgetRenderer.tsx | 9 +-
.../__tests__/GraphWidgetTestWrapper.tsx | 17 +-
.../graph/components/GraphWidgetLegend.tsx | 7 +-
.../GraphWidgetBarChartRenderer.tsx | 7 +-
.../GraphWidgetLineChartRenderer.tsx | 7 +-
.../GraphWidgetPieChartRenderer.tsx | 7 +-
.../hooks/useIsCurrentWidgetLastOfTab.ts | 7 +-
.../widgets/hooks/useIsInPinnedTab.ts | 7 +-
.../iframe/components/IframeWidget.tsx | 7 +-
.../StandaloneRichTextEditorContent.tsx | 12 +-
.../components/StandaloneRichTextWidget.tsx | 6 +-
.../side-panel/hooks/useSidePanelMenu.ts | 13 +-
.../PageHeaderToggleSidePanelButton.tsx | 8 +-
.../layout/page/components/DefaultLayout.tsx | 4 +-
.../clients/generated/metadata/schema.graphql | 2 -
.../src/clients/generated/metadata/schema.ts | 4 +-
...e-cancel-record-page-layout-engine-keys.ts | 40 +++
.../enums/engine-component-key.enum.ts | 2 -
.../standard-command-menu-item.constant.ts | 30 ---
.../display/icon/components/TablerIcons.ts | 1 +
packages/twenty-ui/src/display/index.ts | 1 +
102 files changed, 2176 insertions(+), 738 deletions(-)
create mode 100644 packages/twenty-front/src/modules/app/hooks/__tests__/useExecuteTasksOnAnyLocationChange.test.tsx
create mode 100644 packages/twenty-front/src/modules/command-menu-item/hooks/__tests__/useIsCommandBlockedByGlobalLayoutCustomization.test.tsx
create mode 100644 packages/twenty-front/src/modules/command-menu-item/hooks/__tests__/useRegisteredCommandMenuItems.test.tsx
create mode 100644 packages/twenty-front/src/modules/command-menu-item/hooks/useIsCommandBlockedByGlobalLayoutCustomization.ts
delete mode 100644 packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/CancelRecordPageLayoutSingleRecordCommand.tsx
delete mode 100644 packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/SaveRecordPageLayoutSingleRecordCommand.tsx
create mode 100644 packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx
create mode 100644 packages/twenty-front/src/modules/layout-customization/hooks/__tests__/useIsLayoutCustomizationDirty.test.tsx
create mode 100644 packages/twenty-front/src/modules/layout-customization/hooks/useCancelLayoutCustomization.ts
create mode 100644 packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
create mode 100644 packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts
create mode 100644 packages/twenty-front/src/modules/layout-customization/hooks/useIsLayoutCustomizationDirty.ts
create mode 100644 packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts
create mode 100644 packages/twenty-front/src/modules/layout-customization/states/activeCustomizationPageLayoutIdsState.ts
create mode 100644 packages/twenty-front/src/modules/layout-customization/states/isLayoutCustomizationModeEnabledState.ts
delete mode 100644 packages/twenty-front/src/modules/navigation-menu-item/common/states/isNavigationMenuInEditModeState.ts
delete mode 100644 packages/twenty-front/src/modules/navigation-menu-item/edit/components/NavigationMenuEditModeBar.tsx
create mode 100644 packages/twenty-front/src/modules/page-layout/components/DashboardPageLayoutEditModeProvider.tsx
create mode 100644 packages/twenty-front/src/modules/page-layout/components/PageLayoutEditModeProvider.tsx
create mode 100644 packages/twenty-front/src/modules/page-layout/components/PageLayoutRecordPageCustomizationSessionRegistrationEffect.tsx
create mode 100644 packages/twenty-front/src/modules/page-layout/components/RecordPageLayoutEditModeProvider.tsx
create mode 100644 packages/twenty-front/src/modules/page-layout/contexts/PageLayoutEditModeContext.ts
create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/__tests__/useIsPageLayoutInEditMode.test.tsx
create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/__tests__/useSetIsPageLayoutInEditMode.test.tsx
create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/useIsDashboardPageLayoutInEditMode.ts
create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/useIsPageLayoutInEditMode.ts
rename packages/twenty-front/src/modules/page-layout/states/{isPageLayoutInEditModeComponentState.ts => isDashboardInEditModeComponentState.ts} (77%)
create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1773668124779-remove-save-cancel-record-page-layout-engine-keys.ts
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index cbfd9c92f9..e607657d0a 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -1471,7 +1471,6 @@ export enum EngineComponentKey {
ADD_TO_FAVORITES = 'ADD_TO_FAVORITES',
ASK_AI = 'ASK_AI',
CANCEL_DASHBOARD_LAYOUT = 'CANCEL_DASHBOARD_LAYOUT',
- CANCEL_RECORD_PAGE_LAYOUT = 'CANCEL_RECORD_PAGE_LAYOUT',
CREATE_NEW_RECORD = 'CREATE_NEW_RECORD',
CREATE_NEW_VIEW = 'CREATE_NEW_VIEW',
DEACTIVATE_WORKFLOW = 'DEACTIVATE_WORKFLOW',
@@ -1507,7 +1506,6 @@ export enum EngineComponentKey {
RESTORE_MULTIPLE_RECORDS = 'RESTORE_MULTIPLE_RECORDS',
RESTORE_SINGLE_RECORD = 'RESTORE_SINGLE_RECORD',
SAVE_DASHBOARD_LAYOUT = 'SAVE_DASHBOARD_LAYOUT',
- SAVE_RECORD_PAGE_LAYOUT = 'SAVE_RECORD_PAGE_LAYOUT',
SEARCH_RECORDS = 'SEARCH_RECORDS',
SEARCH_RECORDS_FALLBACK = 'SEARCH_RECORDS_FALLBACK',
SEE_ACTIVE_VERSION_WORKFLOW = 'SEE_ACTIVE_VERSION_WORKFLOW',
diff --git a/packages/twenty-front/src/modules/app/hooks/__tests__/useExecuteTasksOnAnyLocationChange.test.tsx b/packages/twenty-front/src/modules/app/hooks/__tests__/useExecuteTasksOnAnyLocationChange.test.tsx
new file mode 100644
index 0000000000..b58b7c6dc5
--- /dev/null
+++ b/packages/twenty-front/src/modules/app/hooks/__tests__/useExecuteTasksOnAnyLocationChange.test.tsx
@@ -0,0 +1,93 @@
+import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksOnAnyLocationChange';
+import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
+import { isDashboardInEditModeComponentState } from '@/page-layout/states/isDashboardInEditModeComponentState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { act, renderHook } from '@testing-library/react';
+import { createStore, Provider as JotaiProvider } from 'jotai';
+import { type ReactNode } from 'react';
+
+const mockCloseAnyOpenDropdown = jest.fn();
+
+jest.mock('@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown', () => ({
+ useCloseAnyOpenDropdown: () => ({
+ closeAnyOpenDropdown: mockCloseAnyOpenDropdown,
+ }),
+}));
+
+const PAGE_LAYOUT_ID = 'test-page-layout-id';
+
+const getWrapper =
+ (store = createStore()) =>
+ ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+
+describe('useExecuteTasksOnAnyLocationChange', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should reset page layout edit state when layout customization is inactive', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(currentPageLayoutIdState.atom, PAGE_LAYOUT_ID);
+ store.set(
+ isDashboardInEditModeComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID,
+ }),
+ true,
+ );
+ store.set(isLayoutCustomizationModeEnabledState.atom, false);
+
+ const { result } = renderHook(() => useExecuteTasksOnAnyLocationChange(), {
+ wrapper,
+ });
+
+ act(() => {
+ result.current.executeTasksOnAnyLocationChange();
+ });
+
+ expect(mockCloseAnyOpenDropdown).toHaveBeenCalledTimes(1);
+ expect(store.get(currentPageLayoutIdState.atom)).toBeNull();
+ expect(
+ store.get(
+ isDashboardInEditModeComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID,
+ }),
+ ),
+ ).toBe(false);
+ });
+
+ it('should not reset page layout edit state when layout customization is active', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(currentPageLayoutIdState.atom, PAGE_LAYOUT_ID);
+ store.set(
+ isDashboardInEditModeComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID,
+ }),
+ true,
+ );
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+
+ const { result } = renderHook(() => useExecuteTasksOnAnyLocationChange(), {
+ wrapper,
+ });
+
+ act(() => {
+ result.current.executeTasksOnAnyLocationChange();
+ });
+
+ expect(mockCloseAnyOpenDropdown).toHaveBeenCalledTimes(1);
+ expect(store.get(currentPageLayoutIdState.atom)).toBe(PAGE_LAYOUT_ID);
+ expect(
+ store.get(
+ isDashboardInEditModeComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID,
+ }),
+ ),
+ ).toBe(true);
+ });
+});
diff --git a/packages/twenty-front/src/modules/app/hooks/useExecuteTasksOnAnyLocationChange.ts b/packages/twenty-front/src/modules/app/hooks/useExecuteTasksOnAnyLocationChange.ts
index 4d8947cd95..851ded549f 100644
--- a/packages/twenty-front/src/modules/app/hooks/useExecuteTasksOnAnyLocationChange.ts
+++ b/packages/twenty-front/src/modules/app/hooks/useExecuteTasksOnAnyLocationChange.ts
@@ -1,3 +1,4 @@
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
@@ -8,7 +9,7 @@ import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { isDashboardInEditModeComponentState } from '@/page-layout/states/isDashboardInEditModeComponentState';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState';
@@ -57,7 +58,7 @@ export const useExecuteTasksOnAnyLocationChange = () => {
}
store.set(
- isPageLayoutInEditModeComponentState.atomFamily({
+ isDashboardInEditModeComponentState.atomFamily({
instanceId: pageLayoutId,
}),
false,
@@ -137,7 +138,14 @@ export const useExecuteTasksOnAnyLocationChange = () => {
*/
const executeTasksOnAnyLocationChange = () => {
closeAnyOpenDropdown();
- resetPageLayoutEditMode();
+
+ const isLayoutCustomizationModeEnabled = store.get(
+ isLayoutCustomizationModeEnabledState.atom,
+ );
+
+ if (!isLayoutCustomizationModeEnabled) {
+ resetPageLayoutEditMode();
+ }
};
return { executeTasksOnAnyLocationChange };
diff --git a/packages/twenty-front/src/modules/command-menu-item/constants/EngineComponentKeyComponentMap.tsx b/packages/twenty-front/src/modules/command-menu-item/constants/EngineComponentKeyComponentMap.tsx
index b35ea90839..d829e365c4 100644
--- a/packages/twenty-front/src/modules/command-menu-item/constants/EngineComponentKeyComponentMap.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/constants/EngineComponentKeyComponentMap.tsx
@@ -24,9 +24,7 @@ import { CancelDashboardSingleRecordCommand } from '@/command-menu-item/record/s
import { DuplicateDashboardSingleRecordCommand } from '@/command-menu-item/record/single-record/dashboard/components/DuplicateDashboardSingleRecordCommand';
import { EditDashboardSingleRecordCommand } from '@/command-menu-item/record/single-record/dashboard/components/EditDashboardSingleRecordCommand';
import { SaveDashboardSingleRecordCommand } from '@/command-menu-item/record/single-record/dashboard/components/SaveDashboardSingleRecordCommand';
-import { CancelRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/record/single-record/record-page-layout/components/CancelRecordPageLayoutSingleRecordCommand';
import { EditRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand';
-import { SaveRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/record/single-record/record-page-layout/components/SaveRecordPageLayoutSingleRecordCommand';
import { SeeVersionWorkflowRunSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-runs/components/SeeVersionWorkflowRunSingleRecordCommand';
import { SeeWorkflowWorkflowRunSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-runs/components/SeeWorkflowWorkflowRunSingleRecordCommand';
import { StopWorkflowRunSingleRecordCommand } from '@/command-menu-item/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand';
@@ -96,9 +94,6 @@ export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record<
[EngineComponentKey.USE_AS_DRAFT_WORKFLOW_VERSION]: (
),
- [EngineComponentKey.SAVE_RECORD_PAGE_LAYOUT]: (
-
- ),
[EngineComponentKey.SAVE_DASHBOARD_LAYOUT]: (
),
@@ -136,9 +131,6 @@ export const ENGINE_COMPONENT_KEY_COMPONENT_MAP: Record<
[EngineComponentKey.EDIT_RECORD_PAGE_LAYOUT]: (
),
- [EngineComponentKey.CANCEL_RECORD_PAGE_LAYOUT]: (
-
- ),
[EngineComponentKey.EDIT_DASHBOARD_LAYOUT]: (
),
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandDropdownItem.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandDropdownItem.tsx
index ca37270f0c..7ca628cb0a 100644
--- a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandDropdownItem.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandDropdownItem.tsx
@@ -13,14 +13,20 @@ export const CommandDropdownItem = ({
action,
onClick,
to,
+ disabled = false,
}: {
action: CommandMenuItemDisplayProps;
onClick?: () => void;
to?: string;
+ disabled?: boolean;
}) => {
const navigate = useNavigate();
const handleClick = () => {
+ if (disabled) {
+ return;
+ }
+
onClick?.();
if (isDefined(to)) {
navigate(to);
@@ -45,6 +51,7 @@ export const CommandDropdownItem = ({
LeftIcon={action.Icon}
onClick={handleClick}
text={getCommandMenuItemLabel(action.label)}
+ disabled={disabled}
/>
);
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandListItem.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandListItem.tsx
index f99c0da071..1b002750fc 100644
--- a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandListItem.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandListItem.tsx
@@ -11,11 +11,13 @@ export const CommandListItem = ({
onClick,
to,
disabled = false,
+ showDisabledLoader = false,
}: {
action: CommandMenuItemDisplayProps;
onClick?: () => void;
to?: string;
disabled?: boolean;
+ showDisabledLoader?: boolean;
}) => {
const navigate = useNavigate();
@@ -41,7 +43,7 @@ export const CommandListItem = ({
onClick={disabled ? undefined : onClick}
hotKeys={action.hotKeys}
disabled={disabled}
- RightComponent={disabled ? : undefined}
+ RightComponent={disabled && showDisabledLoader ? : undefined}
/>
);
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemButton.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemButton.tsx
index 05072f7d5f..ac3f70b71c 100644
--- a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemButton.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemButton.tsx
@@ -5,10 +5,19 @@ export const CommandMenuItemButton = ({
action,
onClick,
to,
+ disabled = false,
}: {
action: CommandMenuItemDisplayProps;
onClick?: (event?: React.MouseEvent) => void;
to?: string;
+ disabled?: boolean;
}) => {
- return ;
+ return (
+
+ );
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemDisplay.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemDisplay.tsx
index 56833c5893..5d306f0bc9 100644
--- a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemDisplay.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemDisplay.tsx
@@ -3,6 +3,7 @@ import { CommandDropdownItem } from '@/command-menu-item/display/components/Comm
import { CommandListItem } from '@/command-menu-item/display/components/CommandListItem';
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
+import { useIsCommandBlockedByGlobalLayoutCustomization } from '@/command-menu-item/hooks/useIsCommandBlockedByGlobalLayoutCustomization';
import { type MessageDescriptor } from '@lingui/core';
import { useContext } from 'react';
import { type Nullable } from 'twenty-shared/types';
@@ -25,35 +26,60 @@ export const CommandMenuItemDisplay = ({
onClick,
to,
disabled,
+ showDisabledLoader = false,
}: {
onClick?: (event?: React.MouseEvent) => void;
to?: string;
disabled?: boolean;
+ showDisabledLoader?: boolean;
}) => {
const action = useContext(CommandConfigContext);
const { displayType } = useContext(CommandMenuContext);
+ const isBlockedByGlobalLayoutCustomization =
+ useIsCommandBlockedByGlobalLayoutCustomization(action);
if (!action) {
return null;
}
+ const isDisabled =
+ disabled === true || isBlockedByGlobalLayoutCustomization === true;
+
+ const onClickWhenEnabled = isDisabled ? undefined : onClick;
+ const toWhenEnabled = isDisabled ? undefined : to;
+
if (displayType === 'button') {
- return ;
+ return (
+
+ );
}
if (displayType === 'listItem') {
return (
);
}
if (displayType === 'dropdownItem') {
- return ;
+ return (
+
+ );
}
return assertUnreachable(displayType, 'Unsupported display type');
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx
index 63e3aedada..da7ab84126 100644
--- a/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem.tsx
@@ -38,5 +38,11 @@ export const HeadlessFrontComponentCommandMenuItem = ({
onClick();
};
- return ;
+ return (
+
+ );
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/hooks/__tests__/useIsCommandBlockedByGlobalLayoutCustomization.test.tsx b/packages/twenty-front/src/modules/command-menu-item/hooks/__tests__/useIsCommandBlockedByGlobalLayoutCustomization.test.tsx
new file mode 100644
index 0000000000..9c05f14243
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/hooks/__tests__/useIsCommandBlockedByGlobalLayoutCustomization.test.tsx
@@ -0,0 +1,87 @@
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { useIsCommandBlockedByGlobalLayoutCustomization } from '@/command-menu-item/hooks/useIsCommandBlockedByGlobalLayoutCustomization';
+import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
+import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
+import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
+import { renderHook } from '@testing-library/react';
+import { createStore, Provider as JotaiProvider } from 'jotai';
+import { type ReactNode } from 'react';
+import { CommandMenuItemViewType } from 'twenty-shared/types';
+import { Icon123 } from 'twenty-ui/display';
+
+const getWrapper =
+ (store = createStore()) =>
+ ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+
+const buildCommandMenuItemConfig = (
+ isAllowedDuringGlobalLayoutCustomization?: boolean,
+): CommandMenuItemConfig => ({
+ type: CommandMenuItemType.Standard,
+ scope: CommandMenuItemScope.Global,
+ key: 'test-command',
+ label: 'Test Command',
+ position: 1,
+ Icon: Icon123,
+ availableOn: [CommandMenuItemViewType.GLOBAL],
+ shouldBeRegistered: () => true,
+ component: null,
+ isAllowedDuringGlobalLayoutCustomization,
+});
+
+describe('useIsCommandBlockedByGlobalLayoutCustomization', () => {
+ it('should not block commands when global layout customization is inactive', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+ const commandMenuItemConfig = buildCommandMenuItemConfig(false);
+
+ store.set(isLayoutCustomizationModeEnabledState.atom, false);
+
+ const { result } = renderHook(
+ () =>
+ useIsCommandBlockedByGlobalLayoutCustomization(commandMenuItemConfig),
+ {
+ wrapper,
+ },
+ );
+
+ expect(result.current).toBe(false);
+ });
+
+ it('should block commands by default when global layout customization is active', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+ const commandMenuItemConfig = buildCommandMenuItemConfig();
+
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+
+ const { result } = renderHook(
+ () =>
+ useIsCommandBlockedByGlobalLayoutCustomization(commandMenuItemConfig),
+ {
+ wrapper,
+ },
+ );
+
+ expect(result.current).toBe(true);
+ });
+
+ it('should allow commands explicitly marked for global layout customization', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+ const commandMenuItemConfig = buildCommandMenuItemConfig(true);
+
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+
+ const { result } = renderHook(
+ () =>
+ useIsCommandBlockedByGlobalLayoutCustomization(commandMenuItemConfig),
+ {
+ wrapper,
+ },
+ );
+
+ expect(result.current).toBe(false);
+ });
+});
diff --git a/packages/twenty-front/src/modules/command-menu-item/hooks/__tests__/useRegisteredCommandMenuItems.test.tsx b/packages/twenty-front/src/modules/command-menu-item/hooks/__tests__/useRegisteredCommandMenuItems.test.tsx
new file mode 100644
index 0000000000..7c677c859b
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/hooks/__tests__/useRegisteredCommandMenuItems.test.tsx
@@ -0,0 +1,186 @@
+import { useRegisteredCommandMenuItems } from '@/command-menu-item/hooks/useRegisteredCommandMenuItems';
+import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
+import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
+import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
+import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
+import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
+import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
+import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
+import { act, renderHook } from '@testing-library/react';
+import { createStore, Provider as JotaiProvider } from 'jotai';
+import { type ReactNode } from 'react';
+import { CommandMenuItemViewType } from 'twenty-shared/types';
+import { Icon123 } from 'twenty-ui/display';
+
+jest.mock('@/command-menu-item/utils/getCommandMenuItemConfig', () => ({
+ getCommandMenuItemConfig: () => ({}),
+}));
+
+jest.mock(
+ '@/command-menu-item/record-agnostic/hooks/useRelatedRecordCommands',
+ () => ({
+ useRelatedRecordCommands: () => ({}),
+ }),
+);
+
+jest.mock('@/settings/roles/hooks/usePermissionFlagMap', () => ({
+ usePermissionFlagMap: () => ({}),
+}));
+
+jest.mock(
+ '@/command-menu-item/record-agnostic/hooks/useRecordAgnosticCommands',
+ () => ({
+ useRecordAgnosticCommands: () => ({
+ pageEditItem: {
+ type: CommandMenuItemType.Standard,
+ scope: CommandMenuItemScope.Global,
+ key: 'page-edit-item',
+ label: 'Page Edit Item',
+ position: 0,
+ Icon: Icon123,
+ availableOn: [CommandMenuItemViewType.PAGE_EDIT_MODE],
+ shouldBeRegistered: () => true,
+ component: null,
+ },
+ showItem: {
+ type: CommandMenuItemType.Standard,
+ scope: CommandMenuItemScope.Global,
+ key: 'show-item',
+ label: 'Show Item',
+ position: 1,
+ Icon: Icon123,
+ availableOn: [CommandMenuItemViewType.SHOW_PAGE],
+ shouldBeRegistered: () => true,
+ component: null,
+ },
+ globalItem: {
+ type: CommandMenuItemType.Standard,
+ scope: CommandMenuItemScope.Global,
+ key: 'global-item',
+ label: 'Global Item',
+ position: 2,
+ Icon: Icon123,
+ availableOn: [CommandMenuItemViewType.GLOBAL],
+ shouldBeRegistered: () => true,
+ component: null,
+ },
+ }),
+ }),
+);
+
+const CONTEXT_STORE_INSTANCE_ID = 'test-context-store-instance-id';
+
+const getWrapper = (store = createStore()) => {
+ return ({ children }: { children: ReactNode }) => (
+
+
+ {children}
+
+
+ );
+};
+
+const shouldBeRegisteredParams = {
+ objectPermissions: {
+ canReadObjectRecords: true,
+ canUpdateObjectRecords: true,
+ canSoftDeleteObjectRecords: true,
+ canDestroyObjectRecords: true,
+ restrictedFields: {},
+ objectMetadataId: '',
+ rowLevelPermissionPredicates: [],
+ rowLevelPermissionPredicateGroups: [],
+ },
+ getTargetObjectReadPermission: () => true,
+ getTargetObjectWritePermission: () => true,
+ isFeatureFlagEnabled: () => true,
+};
+
+describe('useRegisteredCommandMenuItems', () => {
+ it('should register SHOW_PAGE and GLOBAL commands when page is not in edit mode', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(
+ contextStoreCurrentViewTypeComponentState.atomFamily({
+ instanceId: CONTEXT_STORE_INSTANCE_ID,
+ }),
+ ContextStoreViewType.ShowPage,
+ );
+ store.set(
+ contextStoreTargetedRecordsRuleComponentState.atomFamily({
+ instanceId: CONTEXT_STORE_INSTANCE_ID,
+ }),
+ { mode: 'selection', selectedRecordIds: [] },
+ );
+
+ act(() => {
+ store.set(
+ contextStoreIsPageInEditModeComponentState.atomFamily({
+ instanceId: CONTEXT_STORE_INSTANCE_ID,
+ }),
+ false,
+ );
+ });
+
+ const { result } = renderHook(
+ () =>
+ useRegisteredCommandMenuItems(
+ shouldBeRegisteredParams as Parameters<
+ typeof useRegisteredCommandMenuItems
+ >[0],
+ ),
+ {
+ wrapper,
+ },
+ );
+
+ expect(result.current.map((item) => item.key)).toEqual([
+ 'show-item',
+ 'global-item',
+ ]);
+ });
+
+ it('should register PAGE_EDIT_MODE commands when page is in edit mode', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(
+ contextStoreCurrentViewTypeComponentState.atomFamily({
+ instanceId: CONTEXT_STORE_INSTANCE_ID,
+ }),
+ ContextStoreViewType.ShowPage,
+ );
+ store.set(
+ contextStoreTargetedRecordsRuleComponentState.atomFamily({
+ instanceId: CONTEXT_STORE_INSTANCE_ID,
+ }),
+ { mode: 'selection', selectedRecordIds: [] },
+ );
+
+ act(() => {
+ store.set(
+ contextStoreIsPageInEditModeComponentState.atomFamily({
+ instanceId: CONTEXT_STORE_INSTANCE_ID,
+ }),
+ true,
+ );
+ });
+
+ const { result } = renderHook(
+ () =>
+ useRegisteredCommandMenuItems(
+ shouldBeRegisteredParams as Parameters<
+ typeof useRegisteredCommandMenuItems
+ >[0],
+ ),
+ {
+ wrapper,
+ },
+ );
+
+ expect(result.current.map((item) => item.key)).toEqual(['page-edit-item']);
+ });
+});
diff --git a/packages/twenty-front/src/modules/command-menu-item/hooks/useIsCommandBlockedByGlobalLayoutCustomization.ts b/packages/twenty-front/src/modules/command-menu-item/hooks/useIsCommandBlockedByGlobalLayoutCustomization.ts
new file mode 100644
index 0000000000..62462ccf56
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/hooks/useIsCommandBlockedByGlobalLayoutCustomization.ts
@@ -0,0 +1,17 @@
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+
+export const useIsCommandBlockedByGlobalLayoutCustomization = (
+ commandMenuItemConfig: CommandMenuItemConfig | null,
+) => {
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
+ );
+
+ if (!isLayoutCustomizationModeEnabled) {
+ return false;
+ }
+
+ return !commandMenuItemConfig?.isAllowedDuringGlobalLayoutCustomization;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/record/constants/DefaultRecordCommandMenuItemsConfig.tsx b/packages/twenty-front/src/modules/command-menu-item/record/constants/DefaultRecordCommandMenuItemsConfig.tsx
index 248807da8c..ea14c16f6c 100644
--- a/packages/twenty-front/src/modules/command-menu-item/record/constants/DefaultRecordCommandMenuItemsConfig.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/record/constants/DefaultRecordCommandMenuItemsConfig.tsx
@@ -21,9 +21,7 @@ import { NavigateToNextRecordSingleRecordCommand } from '@/command-menu-item/rec
import { NavigateToPreviousRecordSingleRecordCommand } from '@/command-menu-item/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand';
import { RemoveFromFavoritesSingleRecordCommand } from '@/command-menu-item/record/single-record/components/RemoveFromFavoritesSingleRecordCommand';
import { RestoreSingleRecordCommand } from '@/command-menu-item/record/single-record/components/RestoreSingleRecordCommand';
-import { CancelRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/record/single-record/record-page-layout/components/CancelRecordPageLayoutSingleRecordCommand';
import { EditRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand';
-import { SaveRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/record/single-record/record-page-layout/components/SaveRecordPageLayoutSingleRecordCommand';
import { RecordPageLayoutSingleRecordCommandKeys } from '@/command-menu-item/record/single-record/record-page-layout/types/RecordPageLayoutSingleRecordCommandKeys';
import { SingleRecordCommandKeys } from '@/command-menu-item/record/single-record/types/SingleRecordCommandKeys';
import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
@@ -45,11 +43,9 @@ import {
import {
IconArrowMerge,
IconBuildingSkyscraper,
- IconCancel,
IconCheckbox,
IconChevronDown,
IconChevronUp,
- IconDeviceFloppy,
IconEdit,
IconEyeOff,
IconFileExport,
@@ -814,59 +810,4 @@ export const DEFAULT_RECORD_COMMAND_MENU_ITEMS_CONFIG: Record<
availableOn: [CommandMenuItemViewType.SHOW_PAGE],
component: ,
},
- [RecordPageLayoutSingleRecordCommandKeys.SAVE_RECORD_PAGE_LAYOUT]: {
- key: RecordPageLayoutSingleRecordCommandKeys.SAVE_RECORD_PAGE_LAYOUT,
- label: msg`Save Page Layout`,
- shortLabel: msg`Save`,
- isPinned: true,
- isPrimaryCTA: true,
- position: 31,
- Icon: IconDeviceFloppy,
- type: CommandMenuItemType.Standard,
- scope: CommandMenuItemScope.RecordSelection,
- requiredPermissionFlag: PermissionFlagType.LAYOUTS,
- shouldBeRegistered: ({
- selectedRecord,
- objectPermissions,
- objectMetadataItem,
- isFeatureFlagEnabled,
- }) =>
- isFeatureFlagEnabled(
- FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
- ) &&
- isDefined(selectedRecord) &&
- !selectedRecord?.isRemote &&
- !isDefined(selectedRecord?.deletedAt) &&
- objectPermissions.canUpdateObjectRecords &&
- objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Dashboard,
- availableOn: [CommandMenuItemViewType.PAGE_EDIT_MODE],
- component: ,
- },
- [RecordPageLayoutSingleRecordCommandKeys.CANCEL_RECORD_PAGE_LAYOUT_EDITION]: {
- key: RecordPageLayoutSingleRecordCommandKeys.CANCEL_RECORD_PAGE_LAYOUT_EDITION,
- label: msg`Cancel Edition`,
- shortLabel: msg`Cancel`,
- isPinned: true,
- position: 32,
- Icon: IconCancel,
- type: CommandMenuItemType.Standard,
- scope: CommandMenuItemScope.RecordSelection,
- requiredPermissionFlag: PermissionFlagType.LAYOUTS,
- shouldBeRegistered: ({
- selectedRecord,
- objectPermissions,
- objectMetadataItem,
- isFeatureFlagEnabled,
- }) =>
- isFeatureFlagEnabled(
- FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
- ) &&
- isDefined(selectedRecord) &&
- !selectedRecord?.isRemote &&
- !isDefined(selectedRecord?.deletedAt) &&
- objectPermissions.canUpdateObjectRecords &&
- objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Dashboard,
- availableOn: [CommandMenuItemViewType.PAGE_EDIT_MODE],
- component: ,
- },
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/record/no-selection/components/EditNavigationSidebarNoSelectionRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/record/no-selection/components/EditNavigationSidebarNoSelectionRecordCommand.tsx
index aa7635d0e2..023fb65d09 100644
--- a/packages/twenty-front/src/modules/command-menu-item/record/no-selection/components/EditNavigationSidebarNoSelectionRecordCommand.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/record/no-selection/components/EditNavigationSidebarNoSelectionRecordCommand.tsx
@@ -1,15 +1,12 @@
+import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
import { Command } from '@/command-menu-item/display/components/Command';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
-import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const EditNavigationSidebarNoSelectionRecordCommand = () => {
- const setIsNavigationMenuInEditMode = useSetAtomState(
- isNavigationMenuInEditModeState,
- );
+ const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode();
return (
setIsNavigationMenuInEditMode(true)}
+ onClick={() => enterLayoutCustomizationMode()}
closeSidePanelOnCommandMenuListExecution
/>
);
diff --git a/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/CancelRecordPageLayoutSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/CancelRecordPageLayoutSingleRecordCommand.tsx
deleted file mode 100644
index f5762b8340..0000000000
--- a/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/CancelRecordPageLayoutSingleRecordCommand.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Command } from '@/command-menu-item/display/components/Command';
-import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
-import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
-import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
-import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
-import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
-
-export const CancelRecordPageLayoutSingleRecordCommand = () => {
- const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
-
- const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
- targetObjectNameSingular: objectMetadataItem.nameSingular,
- });
-
- const { closeSidePanelMenu } = useSidePanelMenu();
-
- const { setIsPageLayoutInEditMode } =
- useSetIsPageLayoutInEditMode(pageLayoutId);
-
- const { resetDraftPageLayoutToPersistedPageLayout } =
- useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
-
- const handleClick = () => {
- closeSidePanelMenu();
-
- resetDraftPageLayoutToPersistedPageLayout();
- setIsPageLayoutInEditMode(false);
- };
-
- return ;
-};
diff --git a/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx
index 2ed6606bfe..11861b1af5 100644
--- a/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx
@@ -1,23 +1,14 @@
+import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
import { Command } from '@/command-menu-item/display/components/Command';
-import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
-import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
-import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { useResetLocationHash } from 'twenty-ui/utilities';
export const EditRecordPageLayoutSingleRecordCommand = () => {
- const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
-
- const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
- targetObjectNameSingular: objectMetadataItem.nameSingular,
- });
-
- const { setIsPageLayoutInEditMode } =
- useSetIsPageLayoutInEditMode(pageLayoutId);
+ const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode();
const { resetLocationHash } = useResetLocationHash();
const handleClick = () => {
- setIsPageLayoutInEditMode(true);
+ enterLayoutCustomizationMode();
resetLocationHash();
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/SaveRecordPageLayoutSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/SaveRecordPageLayoutSingleRecordCommand.tsx
deleted file mode 100644
index 689978c381..0000000000
--- a/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/components/SaveRecordPageLayoutSingleRecordCommand.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Command } from '@/command-menu-item/display/components/Command';
-import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
-import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
-import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
-import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
-import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
-import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
-
-export const SaveRecordPageLayoutSingleRecordCommand = () => {
- const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
-
- const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
- targetObjectNameSingular: objectMetadataItem.nameSingular,
- });
-
- const { savePageLayout } = useSavePageLayout(pageLayoutId);
- const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups({
- pageLayoutId,
- });
-
- const { setIsPageLayoutInEditMode } =
- useSetIsPageLayoutInEditMode(pageLayoutId);
-
- const { closeSidePanelMenu } = useSidePanelMenu();
-
- const handleClick = async () => {
- const result = await savePageLayout();
-
- if (result.status === 'successful') {
- await saveFieldsWidgetGroups();
-
- closeSidePanelMenu();
- setIsPageLayoutInEditMode(false);
- }
- };
-
- return ;
-};
diff --git a/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/types/RecordPageLayoutSingleRecordCommandKeys.ts b/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/types/RecordPageLayoutSingleRecordCommandKeys.ts
index 8869732e61..eadab1d383 100644
--- a/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/types/RecordPageLayoutSingleRecordCommandKeys.ts
+++ b/packages/twenty-front/src/modules/command-menu-item/record/single-record/record-page-layout/types/RecordPageLayoutSingleRecordCommandKeys.ts
@@ -1,5 +1,3 @@
export enum RecordPageLayoutSingleRecordCommandKeys {
EDIT_RECORD_PAGE_LAYOUT = 'edit-record-page-layout-single-record',
- SAVE_RECORD_PAGE_LAYOUT = 'save-record-page-layout-single-record',
- CANCEL_RECORD_PAGE_LAYOUT_EDITION = 'cancel-record-page-layout-edition-single-record',
}
diff --git a/packages/twenty-front/src/modules/command-menu-item/types/CommandMenuItemConfig.ts b/packages/twenty-front/src/modules/command-menu-item/types/CommandMenuItemConfig.ts
index 85868d6642..ff1ef7028c 100644
--- a/packages/twenty-front/src/modules/command-menu-item/types/CommandMenuItemConfig.ts
+++ b/packages/twenty-front/src/modules/command-menu-item/types/CommandMenuItemConfig.ts
@@ -27,4 +27,5 @@ export type CommandMenuItemConfig = {
component: React.ReactNode;
hotKeys?: Nullable;
requiredPermissionFlag?: PermissionFlagType;
+ isAllowedDuringGlobalLayoutCustomization?: boolean;
};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuButton.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuButton.tsx
index 6672775c8a..cb77181d3d 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuButton.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuButton.tsx
@@ -27,6 +27,7 @@ export type CommandMenuButtonProps = {
};
onClick?: (event?: MouseEvent) => void;
to?: string;
+ disabled?: boolean;
};
const getCommandMenuButtonLabel = (
@@ -39,6 +40,7 @@ export const CommandMenuButton = ({
command,
onClick,
to,
+ disabled = false,
}: CommandMenuButtonProps) => {
const resolvedLabel = getCommandMenuButtonLabel(command.label);
@@ -58,6 +60,7 @@ export const CommandMenuButton = ({
accent={buttonAccent}
to={to}
onClick={onClick}
+ disabled={disabled}
title={resolvedShortLabel}
ariaLabel={resolvedLabel}
/>
@@ -70,6 +73,7 @@ export const CommandMenuButton = ({
accent={buttonAccent}
to={to}
onClick={onClick}
+ disabled={disabled}
ariaLabel={resolvedLabel}
/>
diff --git a/packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx b/packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx
new file mode 100644
index 0000000000..1d143b7051
--- /dev/null
+++ b/packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx
@@ -0,0 +1,78 @@
+import { useCancelLayoutCustomization } from '@/layout-customization/hooks/useCancelLayoutCustomization';
+import { useIsLayoutCustomizationDirty } from '@/layout-customization/hooks/useIsLayoutCustomizationDirty';
+import { useSaveLayoutCustomization } from '@/layout-customization/hooks/useSaveLayoutCustomization';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { styled } from '@linaria/react';
+import { useLingui } from '@lingui/react/macro';
+import { AnimatePresence, motion } from 'framer-motion';
+import { useContext } from 'react';
+import { IconCheck, IconPaint } from 'twenty-ui/display';
+import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledContainer = styled.div`
+ align-items: center;
+ background: ${themeCssVariables.color.blue};
+ box-sizing: border-box;
+ color: ${themeCssVariables.font.color.inverted};
+ display: flex;
+ justify-content: space-between;
+ padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[3]};
+ width: 100%;
+`;
+
+const StyledTitle = styled.span`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[2]};
+`;
+
+const LayoutCustomizationBarContent = () => {
+ const { theme } = useContext(ThemeContext);
+ const { t } = useLingui();
+
+ const { save, isSaving } = useSaveLayoutCustomization();
+ const { cancel } = useCancelLayoutCustomization();
+ const { isDirty } = useIsLayoutCustomizationDirty();
+
+ return (
+
+
+
+
+ {t`Layout customization`}
+
+
+
+
+ );
+};
+
+export const LayoutCustomizationBar = () => {
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
+ );
+
+ return (
+
+ {isLayoutCustomizationModeEnabled && }
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/__tests__/useIsLayoutCustomizationDirty.test.tsx b/packages/twenty-front/src/modules/layout-customization/hooks/__tests__/useIsLayoutCustomizationDirty.test.tsx
new file mode 100644
index 0000000000..73fb86526b
--- /dev/null
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/__tests__/useIsLayoutCustomizationDirty.test.tsx
@@ -0,0 +1,224 @@
+import { useIsLayoutCustomizationDirty } from '@/layout-customization/hooks/useIsLayoutCustomizationDirty';
+import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
+import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
+import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
+import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
+import { type PageLayout } from '@/page-layout/types/PageLayout';
+import { renderHook } from '@testing-library/react';
+import { createStore, Provider as JotaiProvider } from 'jotai';
+import { type ReactNode } from 'react';
+import {
+ type NavigationMenuItem,
+ NavigationMenuItemType,
+ PageLayoutType,
+} from '~/generated-metadata/graphql';
+
+const PAGE_LAYOUT_ID_1 = 'page-layout-1';
+const PAGE_LAYOUT_ID_2 = 'page-layout-2';
+
+const MOCK_PAGE_LAYOUT: PageLayout = {
+ __typename: 'PageLayout',
+ id: PAGE_LAYOUT_ID_1,
+ name: 'Test Layout',
+ type: PageLayoutType.RECORD_PAGE,
+ objectMetadataId: 'obj-1',
+ tabs: [],
+ createdAt: '2024-01-01',
+ updatedAt: '2024-01-01',
+ deletedAt: null,
+ defaultTabToFocusOnMobileAndSidePanelId: null,
+};
+
+const MOCK_DRAFT_PAGE_LAYOUT = {
+ id: PAGE_LAYOUT_ID_1,
+ name: 'Test Layout',
+ type: PageLayoutType.RECORD_PAGE,
+ objectMetadataId: 'obj-1',
+ tabs: [] as PageLayout['tabs'],
+ defaultTabToFocusOnMobileAndSidePanelId: null,
+};
+
+const getWrapper =
+ (store = createStore()) =>
+ ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+
+describe('useIsLayoutCustomizationDirty', () => {
+ it('should return not dirty when no layouts are touched and nav is clean', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(metadataStoreState.atomFamily('navigationMenuItems'), {
+ current: [],
+ draft: [],
+ status: 'up-to-date',
+ });
+ store.set(isLayoutCustomizationModeEnabledState.atom, false);
+
+ const { result } = renderHook(() => useIsLayoutCustomizationDirty(), {
+ wrapper,
+ });
+
+ expect(result.current.isDirty).toBe(false);
+ });
+
+ it('should return dirty when a touched page layout draft differs from persisted', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(metadataStoreState.atomFamily('navigationMenuItems'), {
+ current: [],
+ draft: [],
+ status: 'up-to-date',
+ });
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+ store.set(activeCustomizationPageLayoutIdsState.atom, [PAGE_LAYOUT_ID_1]);
+
+ store.set(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID_1,
+ }),
+ MOCK_PAGE_LAYOUT,
+ );
+ store.set(
+ pageLayoutDraftComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID_1,
+ }),
+ { ...MOCK_DRAFT_PAGE_LAYOUT, name: 'Modified Layout' },
+ );
+
+ const { result } = renderHook(() => useIsLayoutCustomizationDirty(), {
+ wrapper,
+ });
+
+ expect(result.current.isDirty).toBe(true);
+ });
+
+ it('should return not dirty when all touched layouts match persisted', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(metadataStoreState.atomFamily('navigationMenuItems'), {
+ current: [],
+ draft: [],
+ status: 'up-to-date',
+ });
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+ store.set(activeCustomizationPageLayoutIdsState.atom, [PAGE_LAYOUT_ID_1]);
+
+ store.set(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID_1,
+ }),
+ MOCK_PAGE_LAYOUT,
+ );
+ store.set(
+ pageLayoutDraftComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID_1,
+ }),
+ MOCK_DRAFT_PAGE_LAYOUT,
+ );
+
+ const { result } = renderHook(() => useIsLayoutCustomizationDirty(), {
+ wrapper,
+ });
+
+ expect(result.current.isDirty).toBe(false);
+ });
+
+ it('should return dirty when nav is dirty even if page layouts are clean', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ // userWorkspaceId must be null so item passes workspace filter
+ const mockNavItem: NavigationMenuItem = {
+ id: 'nav-1',
+ position: 0,
+ type: NavigationMenuItemType.OBJECT,
+ viewId: null,
+ targetObjectMetadataId: null,
+ folderId: null,
+ name: null,
+ link: null,
+ icon: null,
+ color: null,
+ targetRecordId: null,
+ createdAt: '2024-01-01',
+ updatedAt: '2024-01-01',
+ userWorkspaceId: null,
+ };
+
+ store.set(metadataStoreState.atomFamily('navigationMenuItems'), {
+ current: [mockNavItem],
+ draft: [],
+ status: 'up-to-date',
+ });
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+ // Nav draft differs from prefetch
+ store.set(navigationMenuItemsDraftState.atom, []);
+
+ const { result } = renderHook(() => useIsLayoutCustomizationDirty(), {
+ wrapper,
+ });
+
+ expect(result.current.isDirty).toBe(true);
+ });
+
+ it('should check multiple touched layouts', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(metadataStoreState.atomFamily('navigationMenuItems'), {
+ current: [],
+ draft: [],
+ status: 'up-to-date',
+ });
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+ store.set(activeCustomizationPageLayoutIdsState.atom, [
+ PAGE_LAYOUT_ID_1,
+ PAGE_LAYOUT_ID_2,
+ ]);
+
+ // First layout is clean
+ store.set(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID_1,
+ }),
+ MOCK_PAGE_LAYOUT,
+ );
+ store.set(
+ pageLayoutDraftComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID_1,
+ }),
+ MOCK_PAGE_LAYOUT,
+ );
+
+ // Second layout is dirty
+ const secondLayout: PageLayout = {
+ ...MOCK_PAGE_LAYOUT,
+ id: PAGE_LAYOUT_ID_2,
+ name: 'Second Layout',
+ };
+ store.set(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID_2,
+ }),
+ secondLayout,
+ );
+ store.set(
+ pageLayoutDraftComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_ID_2,
+ }),
+ { ...secondLayout, name: 'Modified Second' },
+ );
+
+ const { result } = renderHook(() => useIsLayoutCustomizationDirty(), {
+ wrapper,
+ });
+
+ expect(result.current.isDirty).toBe(true);
+ });
+});
diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useCancelLayoutCustomization.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useCancelLayoutCustomization.ts
new file mode 100644
index 0000000000..f1ff5093ca
--- /dev/null
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/useCancelLayoutCustomization.ts
@@ -0,0 +1,99 @@
+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';
+import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
+import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
+import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
+import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
+import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
+import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
+import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
+import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
+import { useStore } from 'jotai';
+import { useCallback } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+
+export const useCancelLayoutCustomization = () => {
+ const store = useStore();
+ const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
+
+ const cancel = useCallback(() => {
+ const activePageLayoutIds = store.get(
+ activeCustomizationPageLayoutIdsState.atom,
+ );
+
+ for (const pageLayoutId of activePageLayoutIds) {
+ const persisted = store.get(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+
+ if (isDefined(persisted)) {
+ store.set(
+ pageLayoutDraftComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ {
+ id: persisted.id,
+ name: persisted.name,
+ type: persisted.type,
+ objectMetadataId: persisted.objectMetadataId,
+ tabs: persisted.tabs,
+ defaultTabToFocusOnMobileAndSidePanelId:
+ persisted.defaultTabToFocusOnMobileAndSidePanelId,
+ } satisfies DraftPageLayout,
+ );
+
+ store.set(
+ pageLayoutCurrentLayoutsComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ convertPageLayoutToTabLayouts(persisted),
+ );
+ }
+
+ const fieldsWidgetGroupsPersisted = store.get(
+ fieldsWidgetGroupsPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+ store.set(
+ fieldsWidgetGroupsDraftComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ fieldsWidgetGroupsPersisted,
+ );
+
+ const fieldsWidgetUngroupedFieldsPersisted = store.get(
+ fieldsWidgetUngroupedFieldsPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+ store.set(
+ fieldsWidgetUngroupedFieldsDraftComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ fieldsWidgetUngroupedFieldsPersisted,
+ );
+
+ const fieldsWidgetEditorModePersisted = store.get(
+ fieldsWidgetEditorModePersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+ store.set(
+ fieldsWidgetEditorModeDraftComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ fieldsWidgetEditorModePersisted,
+ );
+ }
+
+ exitLayoutCustomizationMode();
+ }, [store, exitLayoutCustomizationMode]);
+
+ return { cancel };
+};
diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
new file mode 100644
index 0000000000..ee013c4721
--- /dev/null
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
@@ -0,0 +1,35 @@
+import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
+import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
+import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
+import { useStore } from 'jotai';
+import { useCallback } from 'react';
+
+export const useEnterLayoutCustomizationMode = () => {
+ const store = useStore();
+
+ const enterLayoutCustomizationMode = useCallback(() => {
+ const isLayoutCustomizationModeAlreadyEnabled = store.get(
+ isLayoutCustomizationModeEnabledState.atom,
+ );
+
+ if (isLayoutCustomizationModeAlreadyEnabled) {
+ return;
+ }
+
+ const prefetchNavigationMenuItems = store.get(
+ navigationMenuItemsSelector.atom,
+ );
+ const workspaceNavigationMenuItems = filterWorkspaceNavigationMenuItems(
+ prefetchNavigationMenuItems,
+ );
+ store.set(navigationMenuItemsDraftState.atom, workspaceNavigationMenuItems);
+
+ store.set(activeCustomizationPageLayoutIdsState.atom, []);
+
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+ }, [store]);
+
+ return { enterLayoutCustomizationMode };
+};
diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts
new file mode 100644
index 0000000000..d38c12c8eb
--- /dev/null
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts
@@ -0,0 +1,42 @@
+import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
+import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
+import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
+import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
+import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
+import { useStore } from 'jotai';
+import { useCallback } from 'react';
+
+export const useExitLayoutCustomizationMode = () => {
+ const store = useStore();
+ const { closeSidePanelMenu } = useSidePanelMenu();
+
+ const setNavigationMenuItemsDraft = useSetAtomState(
+ navigationMenuItemsDraftState,
+ );
+ const setSelectedNavigationMenuItemInEditMode = useSetAtomState(
+ selectedNavigationMenuItemInEditModeState,
+ );
+ const setIsLayoutCustomizationModeEnabled = useSetAtomState(
+ isLayoutCustomizationModeEnabledState,
+ );
+
+ const exitLayoutCustomizationMode = useCallback(() => {
+ setNavigationMenuItemsDraft(null);
+ setSelectedNavigationMenuItemInEditMode(null);
+
+ store.set(currentPageLayoutIdState.atom, null);
+ store.set(activeCustomizationPageLayoutIdsState.atom, []);
+ setIsLayoutCustomizationModeEnabled(false);
+ closeSidePanelMenu();
+ }, [
+ setNavigationMenuItemsDraft,
+ setSelectedNavigationMenuItemInEditMode,
+ setIsLayoutCustomizationModeEnabled,
+ closeSidePanelMenu,
+ store,
+ ]);
+
+ return { 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
new file mode 100644
index 0000000000..82645a7799
--- /dev/null
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/useIsLayoutCustomizationDirty.ts
@@ -0,0 +1,97 @@
+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';
+import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
+import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
+import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
+import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
+import { atom, useAtomValue } from 'jotai';
+import { useMemo } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
+
+export const useIsLayoutCustomizationDirty = () => {
+ const { isDirty: isNavigationDirty } = useNavigationMenuItemsDraftState();
+
+ const isAnyPageLayoutDirtyAtom = useMemo(
+ () =>
+ atom((get) => {
+ const activePageLayoutIds = get(
+ activeCustomizationPageLayoutIdsState.atom,
+ );
+
+ for (const pageLayoutId of activePageLayoutIds) {
+ const draft = get(
+ pageLayoutDraftComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+
+ const persisted = get(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+
+ if (!isDefined(draft) || !isDefined(persisted)) {
+ continue;
+ }
+
+ const persistedAsDraft: DraftPageLayout = {
+ id: persisted.id,
+ name: persisted.name,
+ type: persisted.type,
+ objectMetadataId: persisted.objectMetadataId,
+ tabs: persisted.tabs,
+ defaultTabToFocusOnMobileAndSidePanelId:
+ persisted.defaultTabToFocusOnMobileAndSidePanelId,
+ };
+
+ if (!isDeeplyEqual(draft, persistedAsDraft)) {
+ return true;
+ }
+
+ const fieldsWidgetGroupsDraft = get(
+ fieldsWidgetGroupsDraftComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+ const fieldsWidgetGroupsPersisted = get(
+ fieldsWidgetGroupsPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+
+ if (
+ !isDeeplyEqual(fieldsWidgetGroupsDraft, fieldsWidgetGroupsPersisted)
+ ) {
+ return true;
+ }
+
+ const ungroupedFieldsDraft = get(
+ fieldsWidgetUngroupedFieldsDraftComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+ const ungroupedFieldsPersisted = get(
+ fieldsWidgetUngroupedFieldsPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+
+ if (!isDeeplyEqual(ungroupedFieldsDraft, ungroupedFieldsPersisted)) {
+ return true;
+ }
+ }
+
+ return false;
+ }),
+ [],
+ );
+
+ const isAnyPageLayoutDirty = useAtomValue(isAnyPageLayoutDirtyAtom);
+
+ return { isDirty: isNavigationDirty || isAnyPageLayoutDirty };
+};
diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts
new file mode 100644
index 0000000000..13d1613bc9
--- /dev/null
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/useSaveLayoutCustomization.ts
@@ -0,0 +1,167 @@
+import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/useExitLayoutCustomizationMode';
+import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
+import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft';
+import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
+import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
+import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
+import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
+import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
+import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
+import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
+import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
+import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
+import { type PageLayout } from '@/page-layout/types/PageLayout';
+import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput';
+import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
+import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft';
+import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { useLingui } from '@lingui/react/macro';
+import { useStore } from 'jotai';
+import { useCallback, useState } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+import { PageLayoutType } from '~/generated-metadata/graphql';
+import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
+import { logError } from '~/utils/logError';
+
+export const useSaveLayoutCustomization = () => {
+ const [isSaving, setIsSaving] = useState(false);
+ const store = useStore();
+ const { t } = useLingui();
+
+ const { saveDraft } = useSaveNavigationMenuItemsDraft();
+ const { enqueueErrorSnackBar } = useSnackBar();
+ const { updatePageLayoutWithTabsAndWidgets } =
+ useUpdatePageLayoutWithTabsAndWidgets();
+ const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
+ const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
+
+ const save = useCallback(async () => {
+ setIsSaving(true);
+ try {
+ const navigationDraft = store.get(navigationMenuItemsDraftState.atom);
+ const prefetchItems = store.get(navigationMenuItemsSelector.atom);
+ const workspaceItems = filterWorkspaceNavigationMenuItems(prefetchItems);
+ const isNavigationDirty =
+ isDefined(navigationDraft) &&
+ !isDeeplyEqual(navigationDraft, workspaceItems);
+
+ // TODO: consider a single server mutation (e.g. saveLayoutCustomization)
+ // that saves navigation + page layouts + field widgets in one transaction.
+ // Currently, partial failure leaves mixed state — navigation may commit
+ // while page layouts fail.
+ if (isNavigationDirty) {
+ await saveDraft();
+ }
+
+ const activePageLayoutIds = store.get(
+ activeCustomizationPageLayoutIdsState.atom,
+ );
+ let hasAnyFailure = false;
+
+ for (const pageLayoutId of activePageLayoutIds) {
+ const draft = store.get(
+ pageLayoutDraftComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+
+ const persisted = store.get(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+
+ if (!isDefined(draft) || !isDefined(persisted)) {
+ continue;
+ }
+
+ const persistedAsDraft: DraftPageLayout = {
+ id: persisted.id,
+ name: persisted.name,
+ type: persisted.type,
+ objectMetadataId: persisted.objectMetadataId,
+ tabs: persisted.tabs,
+ defaultTabToFocusOnMobileAndSidePanelId:
+ persisted.defaultTabToFocusOnMobileAndSidePanelId,
+ };
+
+ const isPageLayoutStructureDirty = !isDeeplyEqual(
+ draft,
+ persistedAsDraft,
+ );
+
+ if (isPageLayoutStructureDirty) {
+ const updateInput = convertPageLayoutDraftToUpdateInput(draft);
+ const result = await updatePageLayoutWithTabsAndWidgets(
+ pageLayoutId,
+ updateInput,
+ );
+
+ if (result.status === 'successful') {
+ const updatedPageLayout =
+ result.response.data?.updatePageLayoutWithTabsAndWidgets;
+
+ if (isDefined(updatedPageLayout)) {
+ const persistedLayout: PageLayout =
+ transformPageLayout(updatedPageLayout);
+
+ const pageLayoutToPersist =
+ persistedLayout.type === PageLayoutType.RECORD_PAGE
+ ? reInjectDynamicRelationWidgetsFromDraft(
+ persistedLayout,
+ draft,
+ )
+ : persistedLayout;
+
+ store.set(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ pageLayoutToPersist,
+ );
+ store.set(
+ pageLayoutCurrentLayoutsComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ convertPageLayoutToTabLayouts(pageLayoutToPersist),
+ );
+ }
+ } else {
+ // goes away with a single server mutation (see TODO above)
+ hasAnyFailure = true;
+ continue;
+ }
+ }
+
+ await saveFieldsWidgetGroups(pageLayoutId);
+ }
+
+ if (hasAnyFailure) {
+ enqueueErrorSnackBar({
+ message: t`Some layout changes could not be saved`,
+ });
+ return;
+ }
+
+ exitLayoutCustomizationMode();
+ } catch (error) {
+ logError(error);
+ enqueueErrorSnackBar({
+ message: t`Failed to save layout customization`,
+ });
+ } finally {
+ setIsSaving(false);
+ }
+ }, [
+ saveDraft,
+ updatePageLayoutWithTabsAndWidgets,
+ saveFieldsWidgetGroups,
+ exitLayoutCustomizationMode,
+ enqueueErrorSnackBar,
+ store,
+ t,
+ ]);
+
+ return { save, isSaving };
+};
diff --git a/packages/twenty-front/src/modules/layout-customization/states/activeCustomizationPageLayoutIdsState.ts b/packages/twenty-front/src/modules/layout-customization/states/activeCustomizationPageLayoutIdsState.ts
new file mode 100644
index 0000000000..f716f2fc92
--- /dev/null
+++ b/packages/twenty-front/src/modules/layout-customization/states/activeCustomizationPageLayoutIdsState.ts
@@ -0,0 +1,6 @@
+import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
+
+export const activeCustomizationPageLayoutIdsState = createAtomState({
+ key: 'activeCustomizationPageLayoutIdsState',
+ defaultValue: [],
+});
diff --git a/packages/twenty-front/src/modules/layout-customization/states/isLayoutCustomizationModeEnabledState.ts b/packages/twenty-front/src/modules/layout-customization/states/isLayoutCustomizationModeEnabledState.ts
new file mode 100644
index 0000000000..2d68320922
--- /dev/null
+++ b/packages/twenty-front/src/modules/layout-customization/states/isLayoutCustomizationModeEnabledState.ts
@@ -0,0 +1,6 @@
+import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
+
+export const isLayoutCustomizationModeEnabledState = createAtomState({
+ key: 'isLayoutCustomizationModeEnabledState',
+ defaultValue: false,
+});
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/states/isNavigationMenuInEditModeState.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/states/isNavigationMenuInEditModeState.ts
deleted file mode 100644
index fb01dff947..0000000000
--- a/packages/twenty-front/src/modules/navigation-menu-item/common/states/isNavigationMenuInEditModeState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
-
-export const isNavigationMenuInEditModeState = createAtomState({
- key: 'isNavigationMenuInEditModeState',
- defaultValue: false,
-});
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts
index 50d3840ae5..3cee4c9434 100644
--- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts
+++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts
@@ -4,6 +4,7 @@ import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconFolder, IconLink, useIcons } from 'twenty-ui/display';
+import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/edit/folder/hooks/useAddFolderToNavigationMenuDraft';
@@ -14,7 +15,6 @@ import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/edit/vie
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
import { getObjectMetadataIdsInDraft } from '@/navigation-menu-item/common/utils/getObjectMetadataIdsInDraft';
@@ -43,9 +43,7 @@ export const useHandleAddToNavigationDrop = () => {
const { objectMetadataItems } = useObjectMetadataItems();
const views = useAtomStateValue(viewsSelector);
const { getIcon } = useIcons();
- const setIsNavigationMenuInEditMode = useSetAtomState(
- isNavigationMenuInEditModeState,
- );
+ const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode();
const setOpenNavigationMenuItemFolderIds = useSetAtomState(
openNavigationMenuItemFolderIdsState,
);
@@ -92,7 +90,7 @@ export const useHandleAddToNavigationDrop = () => {
'itemId'
>,
) => {
- setIsNavigationMenuInEditMode(true);
+ enterLayoutCustomizationMode();
openNavigationMenuItemInSidePanel({ ...options, itemId: newItemId });
};
@@ -216,7 +214,7 @@ export const useHandleAddToNavigationDrop = () => {
objectMetadataItems,
openNavigationMenuItemInSidePanel,
setOpenNavigationMenuItemFolderIds,
- setIsNavigationMenuInEditMode,
+ enterLayoutCustomizationMode,
workspaceNavigationMenuItems,
store,
],
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop.ts
index 4b062cac21..7782d899fc 100644
--- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop.ts
+++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop.ts
@@ -2,8 +2,8 @@ import { type OnDragEndResponder } from '@hello-pangea/dnd';
import { useStore } from 'jotai';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
import { getPositionBetween } from '@/navigation-menu-item/common/utils/getPositionBetween';
@@ -69,10 +69,10 @@ export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
const navigationMenuItemsDraft = store.get(
navigationMenuItemsDraftState.atom,
);
- const isNavigationMenuInEditMode = store.get(
- isNavigationMenuInEditModeState.atom,
+ const isLayoutCustomizationModeEnabled = store.get(
+ isLayoutCustomizationModeEnabledState.atom,
);
- if (!isNavigationMenuInEditMode || !navigationMenuItemsDraft) {
+ if (!isLayoutCustomizationModeEnabled || !navigationMenuItemsDraft) {
return;
}
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemFolderSubItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemFolderSubItem.tsx
index a921d66d93..95f7ef67a4 100644
--- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemFolderSubItem.tsx
+++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemFolderSubItem.tsx
@@ -2,9 +2,9 @@ import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/display/object/utils/getNavigationMenuItemObjectNameSingular';
@@ -36,8 +36,8 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
selectedNavigationMenuItemId,
isContextDragging,
}: WorkspaceNavigationMenuItemFolderSubItemProps) => {
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
@@ -54,7 +54,7 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
: null;
const isEditableInEditMode =
- isNavigationMenuInEditMode &&
+ isLayoutCustomizationModeEnabled &&
isDefined(onNavigationMenuItemClick) &&
(navigationMenuItem.type === NavigationMenuItemType.LINK ||
isDefined(objectMetadataItem));
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemsFolder.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemsFolder.tsx
index 17d8150a48..cdab564c6c 100644
--- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemsFolder.tsx
+++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemsFolder.tsx
@@ -34,7 +34,7 @@ import { NavigationDropTargetContext } from '@/navigation-menu-item/common/conte
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
import { SortableDropTargetRefContext } from '@/navigation-menu-item/common/contexts/SortableDropTargetRefContext';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
@@ -98,8 +98,8 @@ export const WorkspaceNavigationMenuItemsFolder = ({
selectedNavigationMenuItemId = null,
isDragging = false,
}: WorkspaceNavigationMenuItemsFolderProps) => {
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const { theme } = useContext(ThemeContext);
const { getIcon } = useIcons();
@@ -114,7 +114,7 @@ export const WorkspaceNavigationMenuItemsFolder = ({
addMenuItemInsertionContextState,
);
- const folderContentLengthForTree = isNavigationMenuInEditMode
+ const folderContentLengthForTree = isLayoutCustomizationModeEnabled
? navigationMenuItems.length + 1
: navigationMenuItems.length;
@@ -127,7 +127,7 @@ export const WorkspaceNavigationMenuItemsFolder = ({
};
const shouldUseEditModeClick =
- isNavigationMenuInEditMode && isDefined(onEditModeClick);
+ isLayoutCustomizationModeEnabled && isDefined(onEditModeClick);
const handleClick = shouldUseEditModeClick
? (e?: React.MouseEvent) => {
e?.stopPropagation();
@@ -160,7 +160,7 @@ export const WorkspaceNavigationMenuItemsFolder = ({
const isDragOverFolderHeader =
!isForbiddenDropTarget && activeDropTargetId === folderHeaderSlotId;
const isCompact =
- isNavigationMenuInEditMode || navigationMenuItems.length === 0;
+ isLayoutCustomizationModeEnabled || navigationMenuItems.length === 0;
const headerItem = (
- {isNavigationMenuInEditMode && (
+ {isLayoutCustomizationModeEnabled && (
{
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector);
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const navigationMenuItemsDraft = useAtomStateValue(
navigationMenuItemsDraftState,
@@ -33,7 +33,7 @@ export const useNavigationMenuItemsData = (): NavigationMenuItemsData => {
filterWorkspaceNavigationMenuItems(navigationMenuItems);
const workspaceNavigationMenuItems =
- isNavigationMenuInEditMode && isDefined(navigationMenuItemsDraft)
+ isLayoutCustomizationModeEnabled && isDefined(navigationMenuItemsDraft)
? navigationMenuItemsDraft
: workspaceNavigationMenuItemsFromState;
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/link/components/NavigationMenuItemLinkDisplay.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/link/components/NavigationMenuItemLinkDisplay.tsx
index 32394406c6..7a9aca9a45 100644
--- a/packages/twenty-front/src/modules/navigation-menu-item/display/link/components/NavigationMenuItemLinkDisplay.tsx
+++ b/packages/twenty-front/src/modules/navigation-menu-item/display/link/components/NavigationMenuItemLinkDisplay.tsx
@@ -1,5 +1,5 @@
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
import { getLinkNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/link/utils/getLinkNavigationMenuItemComputedLink';
import { getLinkNavigationMenuItemLabel } from '@/navigation-menu-item/display/link/utils/getLinkNavigationMenuItemLabel';
import type { WorkspaceSectionItemContentProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps';
@@ -15,8 +15,8 @@ export const NavigationMenuItemLinkDisplay = ({
editModeProps,
isDragging,
}: NavigationMenuItemLinkDisplayProps) => {
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const label = getLinkNavigationMenuItemLabel(item);
@@ -25,9 +25,15 @@ export const NavigationMenuItemLinkDisplay = ({
return (
}
active={false}
@@ -35,7 +41,7 @@ export const NavigationMenuItemLinkDisplay = ({
isDragging={isDragging}
triggerEvent="CLICK"
rightOptions={
- !isNavigationMenuInEditMode && (
+ !isLayoutCustomizationModeEnabled && (
{
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
lastVisitedViewPerObjectMetadataItemState,
@@ -91,9 +91,11 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
}) + '/',
);
- const handleClick = isNavigationMenuInEditMode ? onEditModeClick : undefined;
+ const handleClick = isLayoutCustomizationModeEnabled
+ ? onEditModeClick
+ : undefined;
- const shouldNavigate = !isNavigationMenuInEditMode;
+ const shouldNavigate = !isLayoutCustomizationModeEnabled;
const view = isDefined(navigationMenuItem?.viewId)
? views.find((view) => view.id === navigationMenuItem!.viewId)
@@ -156,7 +158,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
label={label}
secondaryLabel={secondaryLabel}
to={
- isNavigationMenuInEditMode || isDragging
+ isLayoutCustomizationModeEnabled || isDragging
? undefined
: shouldNavigate
? navigationPath
@@ -168,7 +170,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
active={isActive}
isSelectedInEditMode={isSelectedInEditMode}
isDragging={isDragging}
- triggerEvent={isNavigationMenuInEditMode ? 'CLICK' : undefined}
+ triggerEvent={isLayoutCustomizationModeEnabled ? 'CLICK' : undefined}
/>
);
};
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItems.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItems.tsx
index 7b07d672b9..bd07705873 100644
--- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItems.tsx
+++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItems.tsx
@@ -5,9 +5,9 @@ import { isDefined } from 'twenty-shared/utils';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly';
@@ -55,8 +55,8 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
onNavigationMenuItemClick,
onActiveObjectMetadataItemClick,
}: NavigationDrawerSectionForWorkspaceItemsProps) => {
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const { toggleNavigationSection, isNavigationSectionOpen } =
useNavigationSection('Workspace');
@@ -151,7 +151,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
label={sectionTitle}
onClick={() => toggleNavigationSection()}
rightIcon={rightIcon}
- alwaysShowRightIcon={isNavigationMenuInEditMode}
+ alwaysShowRightIcon={isLayoutCustomizationModeEnabled}
isOpen={isNavigationSectionOpen}
/>
@@ -165,7 +165,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
containAnimation
initial={false}
>
- {isNavigationMenuInEditMode ? (
+ {isLayoutCustomizationModeEnabled ? (
{
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const workspaceDropDisabled = useIsDropDisabledForSection(true);
const { isDragging } = useContext(NavigationMenuItemDragContext);
@@ -48,7 +48,7 @@ export const WorkspaceSectionListDndKit = ({
const folderCount = filteredItems.filter(
(item) => item.type === NavigationMenuItemType.FOLDER,
).length;
- const isAddMenuItemButtonVisible = isNavigationMenuInEditMode;
+ const isAddMenuItemButtonVisible = isLayoutCustomizationModeEnabled;
return (
{filteredItems.map((item, index) => (
@@ -60,7 +60,9 @@ export const WorkspaceSectionListDndKit = ({
group={
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
}
- disabled={!isNavigationMenuInEditMode || workspaceDropDisabled}
+ disabled={
+ !isLayoutCustomizationModeEnabled || workspaceDropDisabled
+ }
>
{
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
return (
-
+
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItems.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItems.tsx
index 53845afcfd..447658a085 100644
--- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItems.tsx
+++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItems.tsx
@@ -12,6 +12,8 @@ import {
import { LightIconButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
+import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault';
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
@@ -20,24 +22,19 @@ import {
type NavigationMenuItemClickParams,
useWorkspaceSectionItems,
} from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
-import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
-import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
import { preloadWorkspaceDndKit } from '@/navigation/preloadWorkspaceDndKit';
import { NavigationDrawerSectionForWorkspaceItems } from '@/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItems';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
-import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
-import { useStore } from 'jotai';
const StyledRightIconsContainer = styled.div`
align-items: center;
@@ -50,19 +47,9 @@ export const WorkspaceNavigationMenuItems = () => {
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
- const store = useStore();
- const enterEditMode = () => {
- const currentNavigationMenuItems = store.get(
- navigationMenuItemsSelector.atom,
- );
- const workspaceNavigationMenuItems = filterWorkspaceNavigationMenuItems(
- currentNavigationMenuItems,
- );
- store.set(navigationMenuItemsDraftState.atom, workspaceNavigationMenuItems);
- store.set(isNavigationMenuInEditModeState.atom, true);
- };
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode();
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const [
selectedNavigationMenuItemInEditMode,
@@ -81,7 +68,7 @@ export const WorkspaceNavigationMenuItems = () => {
const handleEditClick = (event: React.MouseEvent) => {
event.stopPropagation();
- enterEditMode();
+ enterLayoutCustomizationMode();
};
const openFolderAndNavigateToFirstChild = (
@@ -167,7 +154,7 @@ export const WorkspaceNavigationMenuItems = () => {
objectMetadataItem: ObjectMetadataItem,
navigationMenuItemId: string,
) => {
- enterEditMode();
+ enterLayoutCustomizationMode();
setSelectedNavigationMenuItemInEditMode(navigationMenuItemId);
openNavigationMenuItemInSidePanel({
pageTitle: objectMetadataItem.labelSingular,
@@ -191,7 +178,7 @@ export const WorkspaceNavigationMenuItems = () => {
items={items}
rightIcon={
- {isNavigationMenuInEditMode ? (
+ {isLayoutCustomizationModeEnabled ? (
{
}
selectedNavigationMenuItemId={selectedNavigationMenuItemInEditMode}
onNavigationMenuItemClick={
- isNavigationMenuInEditMode ? handleNavigationMenuItemClick : undefined
+ isLayoutCustomizationModeEnabled
+ ? handleNavigationMenuItemClick
+ : undefined
}
onActiveObjectMetadataItemClick={handleActiveObjectMetadataItemClick}
/>
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/components/NavigationMenuEditModeBar.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/components/NavigationMenuEditModeBar.tsx
deleted file mode 100644
index 98ca84ffc3..0000000000
--- a/packages/twenty-front/src/modules/navigation-menu-item/edit/components/NavigationMenuEditModeBar.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
-import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
-import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
-import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
-import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
-import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
-import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
-import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
-import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
-import { styled } from '@linaria/react';
-import { useLingui } from '@lingui/react/macro';
-import { AnimatePresence, motion } from 'framer-motion';
-import { useContext, useState } from 'react';
-import { SidePanelPages } from 'twenty-shared/types';
-import { IconCheck, useIcons } from 'twenty-ui/display';
-import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
-
-const StyledContainer = styled.div`
- align-items: center;
- background: ${themeCssVariables.color.blue};
- box-sizing: border-box;
- color: ${themeCssVariables.font.color.inverted};
- display: flex;
- justify-content: space-between;
- padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[3]};
- width: 100%;
-`;
-
-const StyledTitle = styled.span`
- align-items: center;
- display: flex;
- gap: ${themeCssVariables.spacing[2]};
-`;
-
-export const NavigationMenuEditModeBar = () => {
- const { theme } = useContext(ThemeContext);
- const { t } = useLingui();
- const { getIcon } = useIcons();
- const [isSaving, setIsSaving] = useState(false);
- const { closeSidePanelMenu } = useSidePanelMenu();
- const sidePanelPage = useAtomStateValue(sidePanelPageState);
- const { enqueueErrorSnackBar } = useSnackBar();
- const setNavigationMenuItemsDraft = useSetAtomState(
- navigationMenuItemsDraftState,
- );
- const setSelectedNavigationMenuItemInEditMode = useSetAtomState(
- selectedNavigationMenuItemInEditModeState,
- );
- const setIsNavigationMenuInEditMode = useSetAtomState(
- isNavigationMenuInEditModeState,
- );
- const { saveDraft } = useSaveNavigationMenuItemsDraft();
- const { isDirty } = useNavigationMenuItemsDraftState();
-
- const cancelEditMode = () => {
- setNavigationMenuItemsDraft(null);
- setSelectedNavigationMenuItemInEditMode(null);
- setIsNavigationMenuInEditMode(false);
- const isNavItemPageOpen =
- sidePanelPage === SidePanelPages.NavigationMenuAddItem ||
- sidePanelPage === SidePanelPages.NavigationMenuItemEdit;
- if (isNavItemPageOpen) {
- closeSidePanelMenu();
- }
- };
-
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
- );
-
- const showNavigationMenuEditModeBar = isNavigationMenuInEditMode;
-
- const handleSave = async () => {
- if (!isDirty) return;
-
- setIsSaving(true);
- try {
- await saveDraft();
- cancelEditMode();
- closeSidePanelMenu();
- } catch {
- enqueueErrorSnackBar({
- message: t`Failed to save navigation layout`,
- });
- } finally {
- setIsSaving(false);
- }
- };
-
- const IconPaint = getIcon('IconPaint');
-
- return (
-
- {showNavigationMenuEditModeBar && (
-
-
-
-
- {t`Layout customization`}
-
-
-
-
- )}
-
- );
-};
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState.ts
index cf5cbe7020..a1cefeb84f 100644
--- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState.ts
+++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState.ts
@@ -1,15 +1,15 @@
import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const useNavigationMenuItemsDraftState = () => {
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector);
const navigationMenuItemsDraft = useAtomStateValue(
@@ -20,12 +20,12 @@ export const useNavigationMenuItemsDraftState = () => {
filterWorkspaceNavigationMenuItems(navigationMenuItems);
const workspaceNavigationMenuItems =
- isNavigationMenuInEditMode && isDefined(navigationMenuItemsDraft)
+ isLayoutCustomizationModeEnabled && isDefined(navigationMenuItemsDraft)
? navigationMenuItemsDraft
: workspaceNavigationMenuItemsFromState;
const isDirty =
- isNavigationMenuInEditMode &&
+ isLayoutCustomizationModeEnabled &&
isDefined(navigationMenuItemsDraft) &&
!isDeeplyEqual(
navigationMenuItemsDraft,
diff --git a/packages/twenty-front/src/modules/navigation/components/PageDragDropProviderMountEffect.tsx b/packages/twenty-front/src/modules/navigation/components/PageDragDropProviderMountEffect.tsx
index 329ef4d1a4..078f0e0581 100644
--- a/packages/twenty-front/src/modules/navigation/components/PageDragDropProviderMountEffect.tsx
+++ b/packages/twenty-front/src/modules/navigation/components/PageDragDropProviderMountEffect.tsx
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
type PageDragDropProviderMountEffectProps = {
@@ -10,15 +10,15 @@ type PageDragDropProviderMountEffectProps = {
export const PageDragDropProviderMountEffect = ({
onEnterEditMode,
}: PageDragDropProviderMountEffectProps) => {
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
useEffect(() => {
- if (isNavigationMenuInEditMode) {
+ if (isLayoutCustomizationModeEnabled) {
onEnterEditMode();
}
- }, [isNavigationMenuInEditMode, onEnterEditMode]);
+ }, [isLayoutCustomizationModeEnabled, onEnterEditMode]);
return null;
};
diff --git a/packages/twenty-front/src/modules/object-record/read-only/hooks/useIsRecordReadOnly.ts b/packages/twenty-front/src/modules/object-record/read-only/hooks/useIsRecordReadOnly.ts
index 59719080b1..aa8af14c2f 100644
--- a/packages/twenty-front/src/modules/object-record/read-only/hooks/useIsRecordReadOnly.ts
+++ b/packages/twenty-front/src/modules/object-record/read-only/hooks/useIsRecordReadOnly.ts
@@ -1,6 +1,6 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { isRecordReadOnly } from '@/object-record/read-only/utils/isRecordReadOnly';
import { useIsRecordDeleted } from '@/object-record/record-field/ui/hooks/useIsRecordDeleted';
@@ -15,8 +15,8 @@ export const useIsRecordReadOnly = ({
recordId,
objectMetadataId,
}: UseIsRecordReadOnlyParams) => {
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const { objectMetadataItem } = useObjectMetadataItemById({
@@ -33,7 +33,7 @@ export const useIsRecordReadOnly = ({
const isRecordDeleted = useIsRecordDeleted({ recordId });
return (
- isNavigationMenuInEditMode ||
+ isLayoutCustomizationModeEnabled ||
isRecordReadOnly({
objectPermissions,
isRecordDeleted,
diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx
index 7f1afbca2b..8325cd9eef 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx
@@ -1,4 +1,4 @@
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
@@ -51,11 +51,11 @@ export const RecordTableEmptyStateDisplay = (
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const isReadOnly =
- isNavigationMenuInEditMode ||
+ isLayoutCustomizationModeEnabled ||
isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx
index 32685a8866..b534f34b3b 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx
@@ -1,4 +1,4 @@
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
@@ -22,8 +22,8 @@ export const RecordTableHeaderLabelIdentifierCellPlusButton = () => {
useRecordTableContextOrThrow();
const isMobile = useIsMobile();
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const { createNewIndexRecord } = useCreateNewIndexRecord({
@@ -37,7 +37,7 @@ export const RecordTableHeaderLabelIdentifierCellPlusButton = () => {
};
const isReadOnly =
- isNavigationMenuInEditMode ||
+ isLayoutCustomizationModeEnabled ||
isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
diff --git a/packages/twenty-front/src/modules/page-layout/components/DashboardPageLayoutEditModeProvider.tsx b/packages/twenty-front/src/modules/page-layout/components/DashboardPageLayoutEditModeProvider.tsx
new file mode 100644
index 0000000000..89fe017774
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/components/DashboardPageLayoutEditModeProvider.tsx
@@ -0,0 +1,21 @@
+import { PageLayoutEditModeProviderContext } from '@/page-layout/contexts/PageLayoutEditModeContext';
+import { useIsDashboardPageLayoutInEditMode } from '@/page-layout/hooks/useIsDashboardPageLayoutInEditMode';
+import { type ReactNode } from 'react';
+
+type DashboardPageLayoutEditModeProviderProps = {
+ pageLayoutId: string;
+ children: ReactNode;
+};
+
+export const DashboardPageLayoutEditModeProvider = ({
+ pageLayoutId,
+ children,
+}: DashboardPageLayoutEditModeProviderProps) => {
+ const isInEditMode = useIsDashboardPageLayoutInEditMode(pageLayoutId);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx
index 3d51a225b7..0c8cd86dd9 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx
@@ -4,19 +4,16 @@ import { PageLayoutVerticalListEditor } from '@/page-layout/components/PageLayou
import { PageLayoutVerticalListViewer } from '@/page-layout/components/PageLayoutVerticalListViewer';
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow';
import { useReorderPageLayoutWidgets } from '@/page-layout/hooks/useReorderPageLayoutWidgets';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import {
PageLayoutTabLayoutMode,
PageLayoutType,
} from '~/generated-metadata/graphql';
export const PageLayoutContent = () => {
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const { tabId } = usePageLayoutContentContext();
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutEditModeProvider.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutEditModeProvider.tsx
new file mode 100644
index 0000000000..a9104d215a
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutEditModeProvider.tsx
@@ -0,0 +1,30 @@
+import { DashboardPageLayoutEditModeProvider } from '@/page-layout/components/DashboardPageLayoutEditModeProvider';
+import { RecordPageLayoutEditModeProvider } from '@/page-layout/components/RecordPageLayoutEditModeProvider';
+import { type ReactNode } from 'react';
+import { PageLayoutType } from '~/generated-metadata/graphql';
+
+type PageLayoutEditModeProviderProps = {
+ layoutType: PageLayoutType;
+ pageLayoutId: string;
+ children: ReactNode;
+};
+
+export const PageLayoutEditModeProvider = ({
+ layoutType,
+ pageLayoutId,
+ children,
+}: PageLayoutEditModeProviderProps) => {
+ if (layoutType === PageLayoutType.RECORD_PAGE) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx
index c70f822ba9..1c08a329f5 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx
@@ -13,7 +13,7 @@ import { PAGE_LAYOUT_GRID_MARGIN } from '@/page-layout/constants/PageLayoutGridM
import { PAGE_LAYOUT_GRID_ROW_HEIGHT } from '@/page-layout/constants/PageLayoutGridRowHeight';
import { usePageLayoutHandleLayoutChange } from '@/page-layout/hooks/usePageLayoutHandleLayoutChange';
import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { pageLayoutCurrentBreakpointComponentState } from '@/page-layout/states/pageLayoutCurrentBreakpointComponentState';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
@@ -125,9 +125,7 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
const gridContainerRef = useRef(null);
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const pageLayoutCurrentLayouts = useAtomComponentStateValue(
pageLayoutCurrentLayoutsComponentState,
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx
index f9affeb06e..5888b2a0d2 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx
@@ -8,11 +8,12 @@ import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayo
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
import { isPageLayoutEmpty } from '@/page-layout/utils/isPageLayoutEmpty';
-import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
+import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback, useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
+import { PageLayoutType } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type PageLayoutInitializationQueryEffectProps = {
@@ -45,6 +46,8 @@ export const PageLayoutInitializationQueryEffect = ({
const initializePageLayout = useCallback(
(layout: PageLayout) => {
+ const isRecordPageLayout = layout.type === PageLayoutType.RECORD_PAGE;
+
const currentPersisted = store.get(
pageLayoutPersistedComponentCallbackState,
);
@@ -59,12 +62,17 @@ export const PageLayoutInitializationQueryEffect = ({
type: layout.type,
objectMetadataId: layout.objectMetadataId,
tabs: layout.tabs,
+ defaultTabToFocusOnMobileAndSidePanelId:
+ layout.defaultTabToFocusOnMobileAndSidePanelId,
});
const tabLayouts = convertPageLayoutToTabLayouts(layout);
store.set(pageLayoutCurrentLayoutsComponentCallbackState, tabLayouts);
- setIsPageLayoutInEditMode(isPageLayoutEmpty(layout));
+ if (!isRecordPageLayout) {
+ const shouldEnterDashboardEditMode = isPageLayoutEmpty(layout);
+ setIsPageLayoutInEditMode(shouldEnterDashboardEditMode);
+ }
},
[
pageLayoutCurrentLayoutsComponentCallbackState,
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRecordPageCustomizationSessionRegistrationEffect.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRecordPageCustomizationSessionRegistrationEffect.tsx
new file mode 100644
index 0000000000..b3b650764c
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRecordPageCustomizationSessionRegistrationEffect.tsx
@@ -0,0 +1,42 @@
+import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useStore } from 'jotai';
+import { useEffect } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+import { PageLayoutType } from '~/generated-metadata/graphql';
+
+export const PageLayoutRecordPageCustomizationSessionRegistrationEffect =
+ () => {
+ const store = useStore();
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
+ );
+ const pageLayoutPersisted = useAtomComponentStateValue(
+ pageLayoutPersistedComponentState,
+ );
+
+ useEffect(() => {
+ if (!isLayoutCustomizationModeEnabled) {
+ return;
+ }
+
+ if (!isDefined(pageLayoutPersisted)) {
+ return;
+ }
+
+ if (pageLayoutPersisted.type !== PageLayoutType.RECORD_PAGE) {
+ return;
+ }
+
+ store.set(activeCustomizationPageLayoutIdsState.atom, (activeIds) =>
+ activeIds.includes(pageLayoutPersisted.id)
+ ? activeIds
+ : [...activeIds, pageLayoutPersisted.id],
+ );
+ }, [isLayoutCustomizationModeEnabled, pageLayoutPersisted, store]);
+
+ return null;
+ };
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRelationWidgetsSyncEffect.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRelationWidgetsSyncEffect.tsx
index 9119a0f9c7..a2ef1bf740 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRelationWidgetsSyncEffect.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRelationWidgetsSyncEffect.tsx
@@ -1,19 +1,23 @@
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
import { useBasePageLayout } from '@/page-layout/hooks/useBasePageLayout';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
+import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
import { injectRelationWidgetsIntoLayout } from '@/page-layout/utils/injectRelationWidgetsIntoLayout';
+import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWidget';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useStore } from 'jotai';
import { useCallback, useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
-import { PageLayoutType } from '~/generated-metadata/graphql';
+import { PageLayoutType, WidgetType } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type PageLayoutRelationWidgetsSyncEffectProps = {
@@ -45,6 +49,73 @@ export const PageLayoutRelationWidgetsSyncEffect = ({
useAtomComponentStateCallbackState(pageLayoutCurrentLayoutsComponentState);
const store = useStore();
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
+ );
+
+ const getDraftWithSyncedDynamicRelationWidgets = useCallback(
+ (
+ currentDraft: DraftPageLayout,
+ layoutWithRelationWidgets: PageLayout,
+ ): DraftPageLayout => {
+ return {
+ ...currentDraft,
+ tabs: currentDraft.tabs.map((draftTab) => {
+ const persistedTab = layoutWithRelationWidgets.tabs.find(
+ (tab) => tab.id === draftTab.id,
+ );
+
+ if (!isDefined(persistedTab)) {
+ return draftTab;
+ }
+
+ const dynamicRelationWidgets = persistedTab.widgets.filter(
+ isDynamicRelationWidget,
+ );
+
+ const nonDynamicWidgets = draftTab.widgets.filter(
+ (widget) => !isDynamicRelationWidget(widget),
+ );
+
+ if (dynamicRelationWidgets.length === 0) {
+ return {
+ ...draftTab,
+ widgets: nonDynamicWidgets,
+ };
+ }
+
+ const firstFieldsWidgetIndex = nonDynamicWidgets.findIndex(
+ (widget) => widget.type === WidgetType.FIELDS,
+ );
+
+ if (firstFieldsWidgetIndex === -1) {
+ return {
+ ...draftTab,
+ widgets: [...nonDynamicWidgets, ...dynamicRelationWidgets],
+ };
+ }
+
+ const widgetsBeforeFields = nonDynamicWidgets.slice(
+ 0,
+ firstFieldsWidgetIndex + 1,
+ );
+ const widgetsAfterFields = nonDynamicWidgets.slice(
+ firstFieldsWidgetIndex + 1,
+ );
+
+ return {
+ ...draftTab,
+ widgets: [
+ ...widgetsBeforeFields,
+ ...dynamicRelationWidgets,
+ ...widgetsAfterFields,
+ ],
+ };
+ }),
+ };
+ },
+ [],
+ );
const syncPageLayoutWithRelationWidgets = useCallback(
(layout: PageLayout) => {
@@ -54,19 +125,35 @@ export const PageLayoutRelationWidgetsSyncEffect = ({
if (!isDeeplyEqual(layout, currentPersisted)) {
store.set(pageLayoutPersistedComponentCallbackState, layout);
- store.set(pageLayoutDraftComponentCallbackState, {
- id: layout.id,
- name: layout.name,
- type: layout.type,
- objectMetadataId: layout.objectMetadataId,
- tabs: layout.tabs,
- });
- const tabLayouts = convertPageLayoutToTabLayouts(layout);
+ const currentDraft = store.get(pageLayoutDraftComponentCallbackState);
+
+ const nextDraft = isLayoutCustomizationModeEnabled
+ ? getDraftWithSyncedDynamicRelationWidgets(currentDraft, layout)
+ : {
+ id: layout.id,
+ name: layout.name,
+ type: layout.type,
+ objectMetadataId: layout.objectMetadataId,
+ tabs: layout.tabs,
+ defaultTabToFocusOnMobileAndSidePanelId:
+ layout.defaultTabToFocusOnMobileAndSidePanelId,
+ };
+
+ if (!isDeeplyEqual(nextDraft, currentDraft)) {
+ store.set(pageLayoutDraftComponentCallbackState, nextDraft);
+ }
+
+ const tabLayouts = convertPageLayoutToTabLayouts({
+ ...layout,
+ tabs: nextDraft.tabs,
+ });
store.set(pageLayoutCurrentLayoutsComponentCallbackState, tabLayouts);
}
},
[
+ getDraftWithSyncedDynamicRelationWidgets,
+ isLayoutCustomizationModeEnabled,
pageLayoutCurrentLayoutsComponentCallbackState,
pageLayoutDraftComponentCallbackState,
pageLayoutPersistedComponentCallbackState,
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx
index 281997f3f2..83ff2ce452 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx
@@ -1,4 +1,6 @@
+import { PageLayoutEditModeProvider } from '@/page-layout/components/PageLayoutEditModeProvider';
import { PageLayoutInitializationQueryEffect } from '@/page-layout/components/PageLayoutInitializationQueryEffect';
+import { PageLayoutRecordPageCustomizationSessionRegistrationEffect } from '@/page-layout/components/PageLayoutRecordPageCustomizationSessionRegistrationEffect';
import { PageLayoutRelationWidgetsSyncEffect } from '@/page-layout/components/PageLayoutRelationWidgetsSyncEffect';
import { PageLayoutRendererContent } from '@/page-layout/components/PageLayoutRendererContent';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
@@ -34,9 +36,15 @@ export const PageLayoutRenderer = ({
instanceId: tabListInstanceId,
}}
>
-
-
-
+
+
+
+
+
+
);
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx
index 58b5d8dcec..736c0eaf43 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx
@@ -8,7 +8,7 @@ import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutT
import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout';
import { useReorderPageLayoutTabs } from '@/page-layout/hooks/useReorderPageLayoutTabs';
import { PageLayoutMainContent } from '@/page-layout/PageLayoutMainContent';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { getScrollWrapperInstanceIdFromPageLayoutId } from '@/page-layout/utils/getScrollWrapperInstanceIdFromPageLayoutId';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
@@ -60,9 +60,7 @@ export const PageLayoutRendererContent = () => {
const { isInSidePanel, layoutType, targetRecordIdentifier } =
useLayoutRenderingContext();
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const activeTabId = useAtomComponentStateValue(activeTabIdComponentState);
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabList.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabList.tsx
index d7e09c53d8..3932cfd74f 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabList.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabList.tsx
@@ -36,7 +36,7 @@ import { PageLayoutTabListVisibleTabs } from '@/page-layout/components/PageLayou
import { STANDARD_PAGE_LAYOUT_TAB_TITLE_TRANSLATIONS } from '@/page-layout/constants/StandardPageLayoutTabTitleTranslations';
import { useIsCurrentObjectCustom } from '@/page-layout/hooks/useIsCurrentObjectCustom';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { pageLayoutTabListCurrentDragDroppableIdComponentState } from '@/page-layout/states/pageLayoutTabListCurrentDragDroppableIdComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
@@ -232,10 +232,7 @@ export const PageLayoutTabList = ({
],
);
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- pageLayoutId,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const pageLayoutTabSettingsOpenTabId = useAtomComponentStateValue(
pageLayoutTabSettingsOpenTabIdComponentState,
pageLayoutId,
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableOverflowDropdown.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableOverflowDropdown.tsx
index b386308bc2..825c9d231d 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableOverflowDropdown.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableOverflowDropdown.tsx
@@ -13,7 +13,7 @@ import { PageLayoutTabListDroppableMoreButton } from '@/page-layout/components/P
import { PageLayoutTabMenuItemSelectAvatar } from '@/page-layout/components/PageLayoutTabMenuItemSelectAvatar';
import { PageLayoutTabRenderClone } from '@/page-layout/components/PageLayoutTabRenderClone';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { isPageLayoutTabDraggingComponentState } from '@/page-layout/states/isPageLayoutTabDraggingComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { shouldEnableTabEditingFeatures } from '@/page-layout/utils/shouldEnableTabEditingFeatures';
@@ -73,10 +73,7 @@ export const PageLayoutTabListReorderableOverflowDropdown = ({
PageLayoutComponentInstanceContext,
);
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- pageLayoutId,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const shouldShowEditButton =
isPageLayoutInEditMode && shouldEnableTabEditingFeatures(pageLayoutType);
diff --git a/packages/twenty-front/src/modules/page-layout/components/RecordPageLayoutEditModeProvider.tsx b/packages/twenty-front/src/modules/page-layout/components/RecordPageLayoutEditModeProvider.tsx
new file mode 100644
index 0000000000..1e0af631bd
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/components/RecordPageLayoutEditModeProvider.tsx
@@ -0,0 +1,24 @@
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { PageLayoutEditModeProviderContext } from '@/page-layout/contexts/PageLayoutEditModeContext';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { type ReactNode } from 'react';
+
+type RecordPageLayoutEditModeProviderProps = {
+ children: ReactNode;
+};
+
+export const RecordPageLayoutEditModeProvider = ({
+ children,
+}: RecordPageLayoutEditModeProviderProps) => {
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx
index 701ae5aab0..61f86a6785 100644
--- a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx
@@ -7,6 +7,7 @@ import { ComponentWithRouterDecorator } from 'twenty-ui/testing';
import { PageLayoutTabList } from '@/page-layout/components/PageLayoutTabList';
import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/PageLayoutTabListDroppableIds';
import { PageLayoutTabListEffect } from '@/page-layout/components/PageLayoutTabListEffect';
+import { PageLayoutEditModeProviderContext } from '@/page-layout/contexts/PageLayoutEditModeContext';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition';
@@ -187,11 +188,13 @@ const meta: Meta = {
decorators: [
ComponentWithRouterDecorator,
(Story) => (
-
-
-
+
+
+
+
+
),
],
};
diff --git a/packages/twenty-front/src/modules/page-layout/contexts/PageLayoutEditModeContext.ts b/packages/twenty-front/src/modules/page-layout/contexts/PageLayoutEditModeContext.ts
new file mode 100644
index 0000000000..cf575e9ce2
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/contexts/PageLayoutEditModeContext.ts
@@ -0,0 +1,10 @@
+import { createRequiredContext } from '~/utils/createRequiredContext';
+
+export type PageLayoutEditModeContextType = {
+ isInEditMode: boolean;
+};
+
+export const [PageLayoutEditModeProviderContext, usePageLayoutEditModeContext] =
+ createRequiredContext(
+ 'PageLayoutEditModeContext',
+ );
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/PageLayoutTestWrapper.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/PageLayoutTestWrapper.tsx
index c17fc28650..96ec9a3990 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/PageLayoutTestWrapper.tsx
+++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/PageLayoutTestWrapper.tsx
@@ -1,24 +1,57 @@
+import { PageLayoutEditModeProvider } from '@/page-layout/components/PageLayoutEditModeProvider';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
+import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import {
createStore,
type getDefaultStore,
Provider as JotaiProvider,
} from 'jotai';
import { type ReactNode, useState } from 'react';
+import { PageLayoutType } from '~/generated-metadata/graphql';
export const PAGE_LAYOUT_TEST_INSTANCE_ID =
'20202020-f244-4ae0-906b-78958aa07642';
+const PageLayoutTestEditModeProvider = ({
+ children,
+ instanceId,
+ layoutType,
+}: {
+ children: ReactNode;
+ instanceId: string;
+ layoutType?: PageLayoutType;
+}) => {
+ const pageLayoutPersisted = useAtomComponentStateValue(
+ pageLayoutPersistedComponentState,
+ instanceId,
+ );
+
+ const resolvedLayoutType =
+ layoutType ?? pageLayoutPersisted?.type ?? PageLayoutType.DASHBOARD;
+
+ return (
+
+ {children}
+
+ );
+};
+
export const PageLayoutTestWrapper = ({
children,
instanceId: instanceIdFromProps,
store: storeFromProps,
+ layoutType,
}: {
children: ReactNode;
instanceId?: string;
store?: ReturnType;
+ layoutType?: PageLayoutType;
}) => {
const instanceId = instanceIdFromProps ?? PAGE_LAYOUT_TEST_INSTANCE_ID;
const [defaultStore] = useState(() => createStore());
@@ -32,7 +65,12 @@ export const PageLayoutTestWrapper = ({
instanceId: getTabListInstanceIdFromPageLayoutId(instanceId),
}}
>
- {children}
+
+ {children}
+
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 ef864bc48c..72354fc50a 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
@@ -1,16 +1,40 @@
import { act, renderHook } from '@testing-library/react';
import {
- PageLayoutTestWrapper,
PAGE_LAYOUT_TEST_INSTANCE_ID,
+ PageLayoutTestWrapper,
} from './PageLayoutTestWrapper';
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
+import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
+import { createStore } from 'jotai';
+import { type ReactNode } from 'react';
describe('useDeletePageLayoutWidget', () => {
- it('should remove widget from all states', () => {
+ const getWrapper =
+ (store = createStore()) =>
+ ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+ );
+
+ it('should clear editing widget id when deleting the edited widget', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(
+ pageLayoutEditingWidgetIdComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ 'widget-1',
+ );
+
const { result } = renderHook(
() => useDeletePageLayoutWidget(PAGE_LAYOUT_TEST_INSTANCE_ID),
{
- wrapper: PageLayoutTestWrapper,
+ wrapper,
},
);
@@ -18,22 +42,44 @@ describe('useDeletePageLayoutWidget', () => {
result.current.deletePageLayoutWidget('widget-1');
});
- expect(typeof result.current.deletePageLayoutWidget).toBe('function');
+ expect(
+ store.get(
+ pageLayoutEditingWidgetIdComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ ),
+ ).toBeNull();
});
- it('should handle removing non-existent widget', () => {
+ it('should keep editing widget id when deleting a different widget', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(
+ pageLayoutEditingWidgetIdComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ 'edited-widget-id',
+ );
+
const { result } = renderHook(
() => useDeletePageLayoutWidget(PAGE_LAYOUT_TEST_INSTANCE_ID),
{
- wrapper: PageLayoutTestWrapper,
+ wrapper,
},
);
act(() => {
- result.current.deletePageLayoutWidget('non-existent-widget');
+ result.current.deletePageLayoutWidget('another-widget-id');
});
- expect(typeof result.current.deletePageLayoutWidget).toBe('function');
+ expect(
+ store.get(
+ pageLayoutEditingWidgetIdComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ ),
+ ).toBe('edited-widget-id');
});
it('should handle empty layouts', () => {
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useIsPageLayoutInEditMode.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useIsPageLayoutInEditMode.test.tsx
new file mode 100644
index 0000000000..3c3b03fb53
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useIsPageLayoutInEditMode.test.tsx
@@ -0,0 +1,111 @@
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
+import {
+ PAGE_LAYOUT_TEST_INSTANCE_ID,
+ PageLayoutTestWrapper,
+} from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
+import { isDashboardInEditModeComponentState } from '@/page-layout/states/isDashboardInEditModeComponentState';
+import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
+import { renderHook } from '@testing-library/react';
+import { createStore } from 'jotai';
+import { type ReactNode } from 'react';
+import { PageLayoutType } from '~/generated-metadata/graphql';
+
+const getWrapper =
+ (
+ store = createStore(),
+ layoutType: PageLayoutType = PageLayoutType.DASHBOARD,
+ ) =>
+ ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+ );
+
+describe('useIsPageLayoutInEditMode', () => {
+ it('should use global layout customization state for record pages', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store, PageLayoutType.RECORD_PAGE);
+
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+ store.set(
+ isDashboardInEditModeComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ false,
+ );
+
+ const { result } = renderHook(() => useIsPageLayoutInEditMode(), {
+ wrapper,
+ });
+
+ expect(result.current).toBe(true);
+ });
+
+ it('should infer record page layout type from persisted layout when layoutType is omitted', () => {
+ const store = createStore();
+
+ const wrapper = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+ );
+
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+ store.set(
+ isDashboardInEditModeComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ false,
+ );
+ store.set(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ {
+ __typename: 'PageLayout',
+ id: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ name: 'Record Page',
+ type: PageLayoutType.RECORD_PAGE,
+ objectMetadataId: 'company-id',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ deletedAt: null,
+ tabs: [],
+ defaultTabToFocusOnMobileAndSidePanelId: null,
+ },
+ );
+
+ const { result } = renderHook(() => useIsPageLayoutInEditMode(), {
+ wrapper,
+ });
+
+ expect(result.current).toBe(true);
+ });
+
+ it('should use dashboard edit mode state for dashboard pages', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store, PageLayoutType.DASHBOARD);
+
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+ store.set(
+ isDashboardInEditModeComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ false,
+ );
+
+ const { result } = renderHook(() => useIsPageLayoutInEditMode(), {
+ wrapper,
+ });
+
+ expect(result.current).toBe(false);
+ });
+});
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useSetIsPageLayoutInEditMode.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useSetIsPageLayoutInEditMode.test.tsx
new file mode 100644
index 0000000000..b32e4f8939
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useSetIsPageLayoutInEditMode.test.tsx
@@ -0,0 +1,104 @@
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
+import {
+ PAGE_LAYOUT_TEST_INSTANCE_ID,
+ PageLayoutTestWrapper,
+} from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
+import { isDashboardInEditModeComponentState } from '@/page-layout/states/isDashboardInEditModeComponentState';
+import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
+import { type PageLayout } from '@/page-layout/types/PageLayout';
+import { act, renderHook } from '@testing-library/react';
+import { createStore } from 'jotai';
+import { type ReactNode } from 'react';
+import { PageLayoutType } from '~/generated-metadata/graphql';
+
+const MOCK_DASHBOARD_LAYOUT: PageLayout = {
+ __typename: 'PageLayout',
+ id: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ name: 'Dashboard Layout',
+ type: PageLayoutType.DASHBOARD,
+ objectMetadataId: 'object-metadata-id',
+ tabs: [],
+ createdAt: '2024-01-01',
+ updatedAt: '2024-01-01',
+ deletedAt: null,
+ defaultTabToFocusOnMobileAndSidePanelId: null,
+};
+
+const getWrapper =
+ (store = createStore()) =>
+ ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+ );
+
+describe('useSetIsPageLayoutInEditMode', () => {
+ it('should block dashboard edit mode while global layout customization is active', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(isLayoutCustomizationModeEnabledState.atom, true);
+ store.set(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ MOCK_DASHBOARD_LAYOUT,
+ );
+
+ const { result } = renderHook(
+ () => useSetIsPageLayoutInEditMode(PAGE_LAYOUT_TEST_INSTANCE_ID),
+ {
+ wrapper,
+ },
+ );
+
+ act(() => {
+ result.current.setIsPageLayoutInEditMode(true);
+ });
+
+ expect(
+ store.get(
+ isDashboardInEditModeComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ ),
+ ).toBe(false);
+ });
+
+ it('should allow dashboard edit mode when global layout customization is inactive', () => {
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(isLayoutCustomizationModeEnabledState.atom, false);
+ store.set(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ MOCK_DASHBOARD_LAYOUT,
+ );
+
+ const { result } = renderHook(
+ () => useSetIsPageLayoutInEditMode(PAGE_LAYOUT_TEST_INSTANCE_ID),
+ {
+ wrapper,
+ },
+ );
+
+ act(() => {
+ result.current.setIsPageLayoutInEditMode(true);
+ });
+
+ expect(
+ store.get(
+ isDashboardInEditModeComponentState.atomFamily({
+ instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
+ }),
+ ),
+ ).toBe(true);
+ });
+});
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCurrentPageLayout.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCurrentPageLayout.ts
index 109239bb96..4b1c4ba210 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/useCurrentPageLayout.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useCurrentPageLayout.ts
@@ -1,4 +1,4 @@
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -13,9 +13,7 @@ export const useCurrentPageLayout = () => {
pageLayoutDraftComponentState,
);
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const isDraftInitialized = isNonEmptyString(pageLayoutDraft.id);
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts
index b0a3071399..3f7bee9b91 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useDeletePageLayoutWidget.ts
@@ -1,14 +1,15 @@
-import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
+import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
+import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
+import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
+import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
+import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
+import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
-import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
-import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
-import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
-import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -26,6 +27,11 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
pageLayoutId,
);
+ const pageLayoutEditingWidgetIdState = useAtomComponentStateCallbackState(
+ pageLayoutEditingWidgetIdComponentState,
+ pageLayoutId,
+ );
+
const { closeSidePanelMenu } = useSidePanelMenu();
const store = useStore();
@@ -55,11 +61,20 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
tabs: removeWidgetFromTab(prev.tabs, tabId, widgetId),
}));
}
+
+ const pageLayoutEditingWidgetId = store.get(
+ pageLayoutEditingWidgetIdState,
+ );
+
+ if (pageLayoutEditingWidgetId === widgetId) {
+ store.set(pageLayoutEditingWidgetIdState, null);
+ }
},
[
closeSidePanelMenu,
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
+ pageLayoutEditingWidgetIdState,
store,
],
);
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useIsDashboardPageLayoutInEditMode.ts b/packages/twenty-front/src/modules/page-layout/hooks/useIsDashboardPageLayoutInEditMode.ts
new file mode 100644
index 0000000000..30e3c2320a
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useIsDashboardPageLayoutInEditMode.ts
@@ -0,0 +1,13 @@
+import { isDashboardInEditModeComponentState } from '@/page-layout/states/isDashboardInEditModeComponentState';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+
+export const useIsDashboardPageLayoutInEditMode = (
+ pageLayoutIdFromProps?: string,
+) => {
+ const isDashboardInEditMode = useAtomComponentStateValue(
+ isDashboardInEditModeComponentState,
+ pageLayoutIdFromProps,
+ );
+
+ return isDashboardInEditMode;
+};
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useIsPageLayoutInEditMode.ts b/packages/twenty-front/src/modules/page-layout/hooks/useIsPageLayoutInEditMode.ts
new file mode 100644
index 0000000000..e79f94acfd
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useIsPageLayoutInEditMode.ts
@@ -0,0 +1,7 @@
+import { usePageLayoutEditModeContext } from '@/page-layout/contexts/PageLayoutEditModeContext';
+
+export const useIsPageLayoutInEditMode = () => {
+ const { isInEditMode } = usePageLayoutEditModeContext();
+
+ return isInEditMode;
+};
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow.ts
index ebf4199510..b05b0ff59a 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow.ts
@@ -1,11 +1,10 @@
import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { buildWidgetVisibilityContext } from '@/page-layout/utils/buildWidgetVisibilityContext';
import { filterVisibleWidgets } from '@/page-layout/utils/filterVisibleWidgets';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { isDefined } from 'twenty-shared/utils';
export const usePageLayoutTabWithVisibleWidgetsOrThrow = (
@@ -14,9 +13,7 @@ export const usePageLayoutTabWithVisibleWidgetsOrThrow = (
const { currentPageLayout } = useCurrentPageLayout();
const isMobile = useIsMobile();
const { isInSidePanel } = useLayoutRenderingContext();
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
if (!isDefined(currentPageLayout)) {
throw new Error('currentPageLayout is not defined');
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts b/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts
index 557d144892..66be40b944 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts
@@ -1,3 +1,4 @@
+import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
@@ -96,13 +97,16 @@ export const useResetDraftPageLayoutToPersistedPageLayout = (
store.set(activeTabId, pageLayoutPersisted.tabs[0].id);
}
- store.set(pageLayoutDraftState, {
+ const persistedAsDraft: DraftPageLayout = {
id: pageLayoutPersisted.id,
name: pageLayoutPersisted.name,
type: pageLayoutPersisted.type,
objectMetadataId: pageLayoutPersisted.objectMetadataId,
tabs: pageLayoutPersisted.tabs,
- });
+ defaultTabToFocusOnMobileAndSidePanelId:
+ pageLayoutPersisted.defaultTabToFocusOnMobileAndSidePanelId,
+ };
+ store.set(pageLayoutDraftState, persistedAsDraft);
const tabLayouts = convertPageLayoutToTabLayouts(pageLayoutPersisted);
store.set(pageLayoutCurrentLayoutsState, tabLayouts);
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useSaveFieldsWidgetGroups.ts b/packages/twenty-front/src/modules/page-layout/hooks/useSaveFieldsWidgetGroups.ts
index 13a5b7c1d8..470df749ef 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/useSaveFieldsWidgetGroups.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useSaveFieldsWidgetGroups.ts
@@ -5,114 +5,92 @@ import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fiel
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
-import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useMutation } from '@apollo/client/react';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
-import { type ViewFragmentFragment } from '~/generated-metadata/graphql';
-
-type UpsertFieldsWidgetInput = {
- widgetId: string;
- groups?: {
- id: string;
- name: string;
- position: number;
- isVisible: boolean;
- fields: {
- viewFieldId: string;
- isVisible: boolean;
- position: number;
- }[];
- }[];
- fields?: {
- viewFieldId: string;
- isVisible: boolean;
- position: number;
- }[];
-};
-
-type UpsertFieldsWidgetResult = {
- upsertFieldsWidget: ViewFragmentFragment;
-};
-
-type UseSaveFieldsWidgetGroupsParams = {
- pageLayoutId: string;
-};
-
-export const useSaveFieldsWidgetGroups = ({
- pageLayoutId,
-}: UseSaveFieldsWidgetGroupsParams) => {
- const fieldsWidgetGroupsDraftState = useAtomComponentStateCallbackState(
- fieldsWidgetGroupsDraftComponentState,
- pageLayoutId,
- );
-
- const fieldsWidgetGroupsPersistedState = useAtomComponentStateCallbackState(
- fieldsWidgetGroupsPersistedComponentState,
- pageLayoutId,
- );
-
- const fieldsWidgetUngroupedFieldsDraftState =
- useAtomComponentStateCallbackState(
- fieldsWidgetUngroupedFieldsDraftComponentState,
- pageLayoutId,
- );
-
- const fieldsWidgetUngroupedFieldsPersistedState =
- useAtomComponentStateCallbackState(
- fieldsWidgetUngroupedFieldsPersistedComponentState,
- pageLayoutId,
- );
-
- const fieldsWidgetEditorModeDraftState = useAtomComponentStateCallbackState(
- fieldsWidgetEditorModeDraftComponentState,
- pageLayoutId,
- );
-
- const fieldsWidgetEditorModePersistedState =
- useAtomComponentStateCallbackState(
- fieldsWidgetEditorModePersistedComponentState,
- pageLayoutId,
- );
+import {
+ type UpsertFieldsWidgetInput,
+ type ViewFragmentFragment,
+} from '~/generated-metadata/graphql';
+export const useSaveFieldsWidgetGroups = () => {
const [upsertFieldsWidgetMutation] = useMutation<
- UpsertFieldsWidgetResult,
+ { upsertFieldsWidget: ViewFragmentFragment },
{ input: UpsertFieldsWidgetInput }
>(UPSERT_FIELDS_WIDGET);
const store = useStore();
- const saveFieldsWidgetGroups = useCallback(async () => {
- const allDraftGroups = store.get(fieldsWidgetGroupsDraftState);
- const allPersistedGroups = store.get(fieldsWidgetGroupsPersistedState);
- const allUngroupedFieldsDraft = store.get(
- fieldsWidgetUngroupedFieldsDraftState,
- );
- const allEditorModes = store.get(fieldsWidgetEditorModeDraftState);
+ const saveFieldsWidgetGroups = useCallback(
+ async (pageLayoutId: string) => {
+ const allDraftGroups = store.get(
+ fieldsWidgetGroupsDraftComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+ const allPersistedGroups = store.get(
+ fieldsWidgetGroupsPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+ const allUngroupedFieldsDraft = store.get(
+ fieldsWidgetUngroupedFieldsDraftComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+ const allEditorModes = store.get(
+ fieldsWidgetEditorModeDraftComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
- const widgetIds = new Set([
- ...Object.keys(allDraftGroups),
- ...Object.keys(allPersistedGroups),
- ...Object.keys(allUngroupedFieldsDraft),
- ]);
+ const widgetIds = new Set([
+ ...Object.keys(allDraftGroups),
+ ...Object.keys(allPersistedGroups),
+ ...Object.keys(allUngroupedFieldsDraft),
+ ]);
- for (const widgetId of widgetIds) {
- const editorMode = allEditorModes[widgetId] ?? 'ungrouped';
+ for (const widgetId of widgetIds) {
+ const editorMode = allEditorModes[widgetId] ?? 'ungrouped';
- if (editorMode === 'grouped') {
- const draftGroups = allDraftGroups[widgetId] ?? [];
+ if (editorMode === 'grouped') {
+ const draftGroups = allDraftGroups[widgetId] ?? [];
- await upsertFieldsWidgetMutation({
- variables: {
- input: {
- widgetId,
- groups: draftGroups.map((group) => ({
- id: group.id,
- name: group.name,
- position: group.position,
- isVisible: group.isVisible,
- fields: group.fields.flatMap((field) => {
+ await upsertFieldsWidgetMutation({
+ variables: {
+ input: {
+ widgetId,
+ groups: draftGroups.map((group) => ({
+ id: group.id,
+ name: group.name,
+ position: group.position,
+ isVisible: group.isVisible,
+ fields: group.fields.flatMap((field) => {
+ if (!isDefined(field.viewFieldId)) {
+ return [];
+ }
+
+ return [
+ {
+ viewFieldId: field.viewFieldId,
+ isVisible: field.isVisible,
+ position: field.position,
+ },
+ ];
+ }),
+ })),
+ },
+ },
+ });
+ } else {
+ const ungroupedFields = allUngroupedFieldsDraft[widgetId] ?? [];
+
+ await upsertFieldsWidgetMutation({
+ variables: {
+ input: {
+ widgetId,
+ fields: ungroupedFields.flatMap((field) => {
if (!isDefined(field.viewFieldId)) {
return [];
}
@@ -125,54 +103,33 @@ export const useSaveFieldsWidgetGroups = ({
},
];
}),
- })),
+ },
},
- },
- });
- } else {
- const ungroupedFields = allUngroupedFieldsDraft[widgetId] ?? [];
-
- await upsertFieldsWidgetMutation({
- variables: {
- input: {
- widgetId,
- fields: ungroupedFields.flatMap((field) => {
- if (!isDefined(field.viewFieldId)) {
- return [];
- }
-
- return [
- {
- viewFieldId: field.viewFieldId,
- isVisible: field.isVisible,
- position: field.position,
- },
- ];
- }),
- },
- },
- });
+ });
+ }
}
- }
- store.set(fieldsWidgetGroupsPersistedState, allDraftGroups);
- store.set(
- fieldsWidgetUngroupedFieldsPersistedState,
- allUngroupedFieldsDraft,
- );
- store.set(fieldsWidgetEditorModePersistedState, allEditorModes);
-
- return { status: 'successful' as const };
- }, [
- fieldsWidgetGroupsDraftState,
- fieldsWidgetGroupsPersistedState,
- fieldsWidgetUngroupedFieldsDraftState,
- fieldsWidgetUngroupedFieldsPersistedState,
- fieldsWidgetEditorModeDraftState,
- fieldsWidgetEditorModePersistedState,
- upsertFieldsWidgetMutation,
- store,
- ]);
+ store.set(
+ fieldsWidgetGroupsPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ allDraftGroups,
+ );
+ store.set(
+ fieldsWidgetUngroupedFieldsPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ allUngroupedFieldsDraft,
+ );
+ store.set(
+ fieldsWidgetEditorModePersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ allEditorModes,
+ );
+ },
+ [store, upsertFieldsWidgetMutation],
+ );
return { saveFieldsWidgetGroups };
};
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useSetIsPageLayoutInEditMode.ts b/packages/twenty-front/src/modules/page-layout/hooks/useSetIsPageLayoutInEditMode.ts
index 9605f468fa..83fe077c5c 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/useSetIsPageLayoutInEditMode.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useSetIsPageLayoutInEditMode.ts
@@ -1,3 +1,4 @@
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
@@ -6,7 +7,8 @@ import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { isDashboardInEditModeComponentState } from '@/page-layout/states/isDashboardInEditModeComponentState';
+import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
@@ -14,6 +16,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
+import { PageLayoutType } from '~/generated-metadata/graphql';
export const useSetIsPageLayoutInEditMode = (pageLayoutIdFromProps: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -21,8 +24,8 @@ export const useSetIsPageLayoutInEditMode = (pageLayoutIdFromProps: string) => {
pageLayoutIdFromProps,
);
- const isPageLayoutInEditModeState = useAtomComponentStateCallbackState(
- isPageLayoutInEditModeComponentState,
+ const isDashboardInEditModeState = useAtomComponentStateCallbackState(
+ isDashboardInEditModeComponentState,
pageLayoutId,
);
@@ -63,6 +66,23 @@ export const useSetIsPageLayoutInEditMode = (pageLayoutIdFromProps: string) => {
const setIsPageLayoutInEditMode = useCallback(
(value: boolean) => {
+ const isLayoutCustomizationModeEnabled = store.get(
+ isLayoutCustomizationModeEnabledState.atom,
+ );
+
+ const pageLayoutPersisted = store.get(
+ pageLayoutPersistedComponentState.atomFamily({
+ instanceId: pageLayoutId,
+ }),
+ );
+
+ const isDashboardPageLayout =
+ pageLayoutPersisted?.type === PageLayoutType.DASHBOARD;
+
+ if (value && isLayoutCustomizationModeEnabled && isDashboardPageLayout) {
+ return;
+ }
+
if (value) {
store.set(fieldsWidgetGroupsDraftState, {});
store.set(fieldsWidgetUngroupedFieldsDraftState, {});
@@ -72,7 +92,7 @@ export const useSetIsPageLayoutInEditMode = (pageLayoutIdFromProps: string) => {
store.set(pageLayoutEditingWidgetIdState, null);
}
- store.set(isPageLayoutInEditModeState, value);
+ store.set(isDashboardInEditModeState, value);
store.set(contextStoreIsFullTabWidgetInEditModeState, value);
@@ -90,7 +110,7 @@ export const useSetIsPageLayoutInEditMode = (pageLayoutIdFromProps: string) => {
}
},
[
- isPageLayoutInEditModeState,
+ isDashboardInEditModeState,
contextStoreIsFullTabWidgetInEditModeState,
fieldsWidgetGroupsDraftState,
fieldsWidgetUngroupedFieldsDraftState,
diff --git a/packages/twenty-front/src/modules/page-layout/states/isPageLayoutInEditModeComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/isDashboardInEditModeComponentState.ts
similarity index 77%
rename from packages/twenty-front/src/modules/page-layout/states/isPageLayoutInEditModeComponentState.ts
rename to packages/twenty-front/src/modules/page-layout/states/isDashboardInEditModeComponentState.ts
index 7467d6100a..bd526b0691 100644
--- a/packages/twenty-front/src/modules/page-layout/states/isPageLayoutInEditModeComponentState.ts
+++ b/packages/twenty-front/src/modules/page-layout/states/isDashboardInEditModeComponentState.ts
@@ -2,9 +2,9 @@ import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/creat
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
-export const isPageLayoutInEditModeComponentState =
+export const isDashboardInEditModeComponentState =
createAtomComponentState({
- key: 'isPageLayoutInEditModeComponentState',
+ key: 'isDashboardInEditModeComponentState',
defaultValue: false,
componentInstanceContext: PageLayoutComponentInstanceContext,
});
diff --git a/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraftComponentState.ts b/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraftComponentState.ts
index 7f13cfb81d..b8b857051f 100644
--- a/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraftComponentState.ts
+++ b/packages/twenty-front/src/modules/page-layout/states/pageLayoutDraftComponentState.ts
@@ -13,6 +13,7 @@ export const pageLayoutDraftComponentState =
type: PageLayoutType.DASHBOARD,
objectMetadataId: null,
tabs: [],
+ defaultTabToFocusOnMobileAndSidePanelId: null,
},
componentInstanceContext: PageLayoutComponentInstanceContext,
});
diff --git a/packages/twenty-front/src/modules/page-layout/types/DraftPageLayout.ts b/packages/twenty-front/src/modules/page-layout/types/DraftPageLayout.ts
index d957f6855a..43a0dbcebd 100644
--- a/packages/twenty-front/src/modules/page-layout/types/DraftPageLayout.ts
+++ b/packages/twenty-front/src/modules/page-layout/types/DraftPageLayout.ts
@@ -1,6 +1,11 @@
import { type PageLayout } from '@/page-layout/types/PageLayout';
-export type DraftPageLayout = Omit<
+export type DraftPageLayout = Pick<
PageLayout,
- 'createdAt' | 'updatedAt' | 'deletedAt'
+ | 'id'
+ | 'name'
+ | 'type'
+ | 'objectMetadataId'
+ | 'tabs'
+ | 'defaultTabToFocusOnMobileAndSidePanelId'
>;
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/DashboardWidgetPlaceholder.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/DashboardWidgetPlaceholder.tsx
index cf2722598e..d5c85dabb5 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/components/DashboardWidgetPlaceholder.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/components/DashboardWidgetPlaceholder.tsx
@@ -1,11 +1,12 @@
-import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { WidgetCard } from '@/page-layout/widgets/widget-card/components/WidgetCard';
import { WidgetCardHeader } from '@/page-layout/widgets/widget-card/components/WidgetCardHeader';
+import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { SidePanelPages } from 'twenty-shared/types';
@@ -23,8 +24,9 @@ export const DashboardWidgetPlaceholder = () => {
PageLayoutComponentInstanceContext,
);
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const { setIsPageLayoutInEditMode } =
@@ -33,6 +35,10 @@ export const DashboardWidgetPlaceholder = () => {
const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel();
const handleClick = () => {
+ if (isLayoutCustomizationModeEnabled) {
+ return;
+ }
+
if (!isPageLayoutInEditMode) {
setIsPageLayoutInEditMode(true);
}
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx
index 9e700d99cd..f0888cbfab 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx
@@ -2,7 +2,7 @@ import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutCo
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWidget';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutResizingWidgetIdComponentState } from '@/page-layout/states/pageLayoutResizingWidgetIdComponentState';
@@ -54,9 +54,7 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
const { handleEditWidget } = useEditPageLayoutWidget();
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const pageLayoutDraggingWidgetId = useAtomComponentStateValue(
pageLayoutDraggingWidgetIdComponentState,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx
index 3415dedaf9..bf30e92da8 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx
@@ -24,7 +24,7 @@ import {
PAGE_LAYOUT_TEST_INSTANCE_ID,
PageLayoutTestWrapper,
} from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { isDashboardInEditModeComponentState } from '@/page-layout/states/isDashboardInEditModeComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
@@ -1053,7 +1053,7 @@ export const WithManyToOneRelationFieldWidget: Story = {
pageLayoutData,
);
jotaiStore.set(
- isPageLayoutInEditModeComponentState.atomFamily({
+ isDashboardInEditModeComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
false,
@@ -1172,7 +1172,7 @@ export const WithOneToManyRelationFieldWidget: Story = {
pageLayoutData,
);
jotaiStore.set(
- isPageLayoutInEditModeComponentState.atomFamily({
+ isDashboardInEditModeComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
false,
@@ -1283,7 +1283,7 @@ export const OneToManyRelationFieldWidgetWithSeeAllButton: Story = {
pageLayoutData,
);
jotaiStore.set(
- isPageLayoutInEditModeComponentState.atomFamily({
+ isDashboardInEditModeComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
false,
@@ -1795,7 +1795,7 @@ export const Catalog: CatalogStory = {
pageLayoutData,
);
jotaiStore.set(
- isPageLayoutInEditModeComponentState.atomFamily({
+ isDashboardInEditModeComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
isInEditMode,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsForDisplay.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsForDisplay.ts
index d9513e9250..93c8a5a7e7 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsForDisplay.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsForDisplay.ts
@@ -1,7 +1,7 @@
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups';
import { type FieldsWidgetDisplayMode } from '@/page-layout/widgets/fields/types/FieldsWidgetDisplayMode';
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
@@ -24,9 +24,7 @@ export const useFieldsWidgetGroupsForDisplay = ({
}: UseFieldsWidgetGroupsForDisplayParams) => {
const { t } = useLingui();
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const fieldsWidgetGroupsDraft = useAtomComponentStateValue(
fieldsWidgetGroupsDraftComponentState,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetHiddenFieldsForDisplay.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetHiddenFieldsForDisplay.ts
index ad5d31afbe..30ad953c83 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetHiddenFieldsForDisplay.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetHiddenFieldsForDisplay.ts
@@ -1,7 +1,7 @@
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { useFieldsWidgetHiddenFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetHiddenFields';
import { type FieldsWidgetGroupField } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { getHiddenFieldsFromGroups } from '@/page-layout/widgets/fields/utils/getHiddenFieldsFromGroups';
@@ -20,9 +20,7 @@ export const useFieldsWidgetHiddenFieldsForDisplay = ({
viewId,
objectNameSingular,
}: UseFieldsWidgetHiddenFieldsForDisplayParams) => {
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const fieldsWidgetGroupsDraft = useAtomComponentStateValue(
fieldsWidgetGroupsDraftComponentState,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx
index 14757587d1..727f2611e1 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx
@@ -3,11 +3,10 @@ import { Suspense, lazy } from 'react';
import { isDefined } from 'twenty-shared/utils';
-import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay';
+import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
const StyledContainer = styled.div<{ isInEditMode: boolean }>`
height: 100%;
@@ -29,9 +28,7 @@ type FrontComponentWidgetRendererProps = {
export const FrontComponentWidgetRenderer = ({
widget,
}: FrontComponentWidgetRendererProps) => {
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const configuration = widget.configuration;
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/__tests__/GraphWidgetTestWrapper.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/__tests__/GraphWidgetTestWrapper.tsx
index 0916595198..505ec5cea6 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/__tests__/GraphWidgetTestWrapper.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/__tests__/GraphWidgetTestWrapper.tsx
@@ -1,3 +1,4 @@
+import { PageLayoutEditModeProviderContext } from '@/page-layout/contexts/PageLayoutEditModeContext';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
import { type ReactNode } from 'react';
@@ -22,12 +23,14 @@ export const GraphWidgetTestWrapper = ({
pageLayoutInstanceIdFromProps ?? PAGE_LAYOUT_TEST_INSTANCE_ID;
return (
-
-
- {children}
-
-
+
+
+
+ {children}
+
+
+
);
};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetLegend.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetLegend.tsx
index 4749655458..9f43b8721d 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetLegend.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetLegend.tsx
@@ -1,4 +1,4 @@
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { GraphWidgetLegendDot } from '@/page-layout/widgets/graph/components/GraphWidgetLegendDot';
import { LEGEND_HIGHLIGHT_DIMMED_OPACITY } from '@/page-layout/widgets/graph/constants/LegendHighlightDimmedOpacity.constant';
import { LEGEND_ITEM_ESTIMATED_WIDTH } from '@/page-layout/widgets/graph/constants/LegendItemEstimatedWidth';
@@ -9,7 +9,6 @@ import { graphWidgetHiddenLegendIdsComponentState } from '@/page-layout/widgets/
import { graphWidgetHighlightedLegendIdComponentState } from '@/page-layout/widgets/graph/states/graphWidgetHighlightedLegendIdComponentState';
import { NodeDimensionEffect } from '@/ui/utilities/dimensions/components/NodeDimensionEffect';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { styled } from '@linaria/react';
import { AnimatePresence, motion } from 'framer-motion';
@@ -150,9 +149,7 @@ export const GraphWidgetLegend = ({
const [animationDirection, setAnimationDirection] =
useState('forward');
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const isInteractive = !isPageLayoutInEditMode;
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/components/GraphWidgetBarChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/components/GraphWidgetBarChartRenderer.tsx
index 441e4ee71c..de31fd9ce3 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/components/GraphWidgetBarChartRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/components/GraphWidgetBarChartRenderer.tsx
@@ -1,4 +1,4 @@
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { WidgetSkeletonLoader } from '@/page-layout/widgets/components/WidgetSkeletonLoader';
import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect';
import { useGraphBarChartWidgetData } from '@/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useGraphBarChartWidgetData';
@@ -10,7 +10,6 @@ import { isFilteredViewRedirectionSupported } from '@/page-layout/widgets/graph/
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
import { useUserFirstDayOfTheWeek } from '@/ui/input/components/internal/date/hooks/useUserFirstDayOfTheWeek';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { indexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/indexViewIdFromObjectMetadataItemFamilySelector';
import { lazy, Suspense } from 'react';
@@ -58,9 +57,7 @@ export const GraphWidgetBarChartRenderer = () => {
const navigate = useNavigate();
const configuration = widget.configuration;
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const axisNameDisplay = configuration.axisNameDisplay;
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/components/GraphWidgetLineChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/components/GraphWidgetLineChartRenderer.tsx
index f4a0e327ea..0f35fc7d72 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/components/GraphWidgetLineChartRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/components/GraphWidgetLineChartRenderer.tsx
@@ -1,4 +1,4 @@
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { WidgetSkeletonLoader } from '@/page-layout/widgets/components/WidgetSkeletonLoader';
import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect';
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graph-widget-line-chart/constants/LineChartConstants';
@@ -10,7 +10,6 @@ import { isFilteredViewRedirectionSupported } from '@/page-layout/widgets/graph/
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
import { useUserFirstDayOfTheWeek } from '@/ui/input/components/internal/date/hooks/useUserFirstDayOfTheWeek';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { indexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/indexViewIdFromObjectMetadataItemFamilySelector';
import { type LineSeries, type Point } from '@nivo/line';
@@ -56,9 +55,7 @@ export const GraphWidgetLineChartRenderer = () => {
const navigate = useNavigate();
const configuration = widget.configuration;
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const hasGroupByOnSecondaryAxis = isDefined(
configuration.secondaryAxisGroupByFieldMetadataId,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/components/GraphWidgetPieChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/components/GraphWidgetPieChartRenderer.tsx
index 26f04ce07c..c77c9426f2 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/components/GraphWidgetPieChartRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/components/GraphWidgetPieChartRenderer.tsx
@@ -1,4 +1,4 @@
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { WidgetSkeletonLoader } from '@/page-layout/widgets/components/WidgetSkeletonLoader';
import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect';
import { useGraphPieChartWidgetData } from '@/page-layout/widgets/graph/graph-widget-pie-chart/hooks/useGraphPieChartWidgetData';
@@ -9,7 +9,6 @@ import { isFilteredViewRedirectionSupported } from '@/page-layout/widgets/graph/
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
import { useUserFirstDayOfTheWeek } from '@/ui/input/components/internal/date/hooks/useUserFirstDayOfTheWeek';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { indexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/indexViewIdFromObjectMetadataItemFamilySelector';
import { lazy, Suspense } from 'react';
@@ -49,9 +48,7 @@ export const GraphWidgetPieChartRenderer = () => {
const navigate = useNavigate();
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const indexViewId = useAtomFamilySelectorValue(
indexViewIdFromObjectMetadataItemFamilySelector,
{ objectMetadataItemId: objectMetadataItem.id },
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsCurrentWidgetLastOfTab.ts b/packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsCurrentWidgetLastOfTab.ts
index 78659267f7..7e41bc0e85 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsCurrentWidgetLastOfTab.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsCurrentWidgetLastOfTab.ts
@@ -1,19 +1,16 @@
import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { buildWidgetVisibilityContext } from '@/page-layout/utils/buildWidgetVisibilityContext';
import { filterVisibleWidgets } from '@/page-layout/utils/filterVisibleWidgets';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { isDefined } from 'twenty-shared/utils';
export const useIsCurrentWidgetLastOfTab = (widgetId: string): boolean => {
const { currentPageLayout } = useCurrentPageLayout();
const isMobile = useIsMobile();
const { isInSidePanel } = useLayoutRenderingContext();
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
if (!isDefined(currentPageLayout)) {
return false;
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsInPinnedTab.ts b/packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsInPinnedTab.ts
index 39b1f49e55..401ab2e3d3 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsInPinnedTab.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsInPinnedTab.ts
@@ -1,10 +1,9 @@
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { getTabsByDisplayMode } from '@/page-layout/utils/getTabsByDisplayMode';
import { getTabsWithVisibleWidgets } from '@/page-layout/utils/getTabsWithVisibleWidgets';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { isDefined } from 'twenty-shared/utils';
import { useIsMobile } from 'twenty-ui/utilities';
@@ -15,9 +14,7 @@ export const useIsInPinnedTab = () => {
const { isInSidePanel } = useLayoutRenderingContext();
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const tabsWithVisibleWidgets = getTabsWithVisibleWidgets({
tabs: currentPageLayout.tabs,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx
index 7b22afd374..8bcde95a48 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx
@@ -1,5 +1,4 @@
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay';
import { WidgetSkeletonLoader } from '@/page-layout/widgets/components/WidgetSkeletonLoader';
@@ -57,9 +56,7 @@ export type IframeWidgetProps = {
};
export const IframeWidget = ({ widget }: IframeWidgetProps) => {
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const configuration = widget.configuration;
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx
index 6b3905b360..b8e2e334a3 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx
@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
import { type Attachment } from '@/activities/files/types/Attachment';
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { isDashboardInEditModeComponentState } from '@/page-layout/states/isDashboardInEditModeComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { DashboardsBlockEditor } from '@/page-layout/widgets/standalone-rich-text/components/DashboardsBlockEditor';
@@ -45,8 +45,8 @@ export const StandaloneRichTextEditorContent = ({
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
- const isPageLayoutInEditModeState = useAtomComponentStateCallbackState(
- isPageLayoutInEditModeComponentState,
+ const isDashboardInEditModeState = useAtomComponentStateCallbackState(
+ isDashboardInEditModeComponentState,
);
const pageLayoutEditingWidgetIdState = useAtomComponentStateCallbackState(
pageLayoutEditingWidgetIdComponentState,
@@ -57,12 +57,12 @@ export const StandaloneRichTextEditorContent = ({
const store = useStore();
const shouldPersistDraft = useCallback(() => {
- const isPageLayoutInEditMode = store.get(isPageLayoutInEditModeState);
+ const isDashboardInEditMode = store.get(isDashboardInEditModeState);
const editingWidgetId = store.get(pageLayoutEditingWidgetIdState);
- return isPageLayoutInEditMode && editingWidgetId === widget.id;
+ return isDashboardInEditMode && editingWidgetId === widget.id;
}, [
- isPageLayoutInEditModeState,
+ isDashboardInEditModeState,
pageLayoutEditingWidgetIdState,
widget.id,
store,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx
index 5339695658..ea837585bd 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextWidget.tsx
@@ -4,7 +4,7 @@ import { type Attachment } from '@/activities/files/types/Attachment';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
-import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
+import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { StandaloneRichTextEditorContent } from '@/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent';
@@ -40,9 +40,7 @@ export const StandaloneRichTextWidget = ({
widget,
}: StandaloneRichTextWidgetProps) => {
const containerElementRef = useRef(null);
- const isPageLayoutInEditMode = useAtomComponentStateValue(
- isPageLayoutInEditModeComponentState,
- );
+ const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const pageLayoutEditingWidgetId = useAtomComponentStateValue(
pageLayoutEditingWidgetIdComponentState,
diff --git a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts
index 3e245dd709..fd019f8f64 100644
--- a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts
+++ b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts
@@ -4,7 +4,7 @@ import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { emitSidePanelOpenEvent } from '@/ui/layout/side-panel/utils/emitSidePanelOpenEvent';
@@ -41,20 +41,23 @@ export const useSidePanelMenu = () => {
const openSidePanelMenu = useCallback(() => {
emitSidePanelOpenEvent();
closeAnyOpenDropdown();
- const isNavigationMenuInEditMode = store.get(
- isNavigationMenuInEditModeState.atom,
+ const isLayoutCustomizationModeEnabled = store.get(
+ isLayoutCustomizationModeEnabledState.atom,
);
const selectedNavigationItemId = store.get(
selectedNavigationMenuItemInEditModeState.atom,
);
- if (isNavigationMenuInEditMode && isDefined(selectedNavigationItemId)) {
+ if (
+ isLayoutCustomizationModeEnabled &&
+ isDefined(selectedNavigationItemId)
+ ) {
navigateSidePanel({
page: SidePanelPages.NavigationMenuItemEdit,
pageTitle: t`Edit`,
pageIcon: IconDotsVertical,
resetNavigationStack: true,
});
- } else if (isNavigationMenuInEditMode) {
+ } else if (isLayoutCustomizationModeEnabled) {
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
diff --git a/packages/twenty-front/src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx b/packages/twenty-front/src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
index 3f320bf499..e03906067d 100644
--- a/packages/twenty-front/src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/page-header/components/PageHeaderToggleSidePanelButton.tsx
@@ -1,7 +1,7 @@
import { SIDE_PANEL_TOP_BAR_HEIGHT_MOBILE } from '@/side-panel/constants/SidePanelTopBarHeightMobile';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
-import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderSidePanelButtonClickOutsideId';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -127,14 +127,14 @@ const AnimatedIcon = ({
export const PageHeaderToggleSidePanelButton = () => {
const { toggleSidePanelMenu } = useSidePanelMenu();
const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
- const isNavigationMenuInEditMode = useAtomStateValue(
- isNavigationMenuInEditModeState,
+ const isLayoutCustomizationModeEnabled = useAtomStateValue(
+ isLayoutCustomizationModeEnabledState,
);
const isMobile = useIsMobile();
const alignWithSidePanelTopBar =
- isMobile && isNavigationMenuInEditMode && isSidePanelOpened;
+ isMobile && isLayoutCustomizationModeEnabled && isSidePanelOpened;
const ariaLabel = isSidePanelOpened
? t`Close side panel`
diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx
index 8d86a775e2..7abb03d9e7 100644
--- a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx
@@ -5,7 +5,7 @@ import { AppPageErrorFallback } from '@/error-handler/components/AppPageErrorFal
import { FileUploadProvider } from '@/file-upload/components/FileUploadProvider';
import { InformationBannerIsImpersonating } from '@/information-banner/components/impersonate/InformationBannerIsImpersonating';
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
-import { NavigationMenuEditModeBar } from '@/navigation-menu-item/edit/components/NavigationMenuEditModeBar';
+import { LayoutCustomizationBar } from '@/layout-customization/components/LayoutCustomizationBar';
import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer';
import { MobileNavigationBar } from '@/navigation/components/MobileNavigationBar';
import { PageDragDropProvider } from '@/navigation/components/PageDragDropProvider';
@@ -75,7 +75,7 @@ export const DefaultLayout = () => {
-
+
{
+ await queryRunner.query(
+ `DELETE FROM "core"."commandMenuItem" WHERE "engineComponentKey" IN ('SAVE_RECORD_PAGE_LAYOUT', 'CANCEL_RECORD_PAGE_LAYOUT')`,
+ );
+ await queryRunner.query(
+ `ALTER TYPE "core"."commandMenuItem_enginecomponentkey_enum" RENAME TO "commandMenuItem_enginecomponentkey_enum_old"`,
+ );
+ await queryRunner.query(
+ `CREATE TYPE "core"."commandMenuItem_enginecomponentkey_enum" AS ENUM('NAVIGATE_TO_NEXT_RECORD', 'NAVIGATE_TO_PREVIOUS_RECORD', 'CREATE_NEW_RECORD', 'DELETE_SINGLE_RECORD', 'DELETE_MULTIPLE_RECORDS', 'RESTORE_SINGLE_RECORD', 'RESTORE_MULTIPLE_RECORDS', 'DESTROY_SINGLE_RECORD', 'DESTROY_MULTIPLE_RECORDS', 'ADD_TO_FAVORITES', 'REMOVE_FROM_FAVORITES', 'EXPORT_NOTE_TO_PDF', 'EXPORT_FROM_RECORD_INDEX', 'EXPORT_FROM_RECORD_SHOW', 'UPDATE_MULTIPLE_RECORDS', 'MERGE_MULTIPLE_RECORDS', 'EXPORT_MULTIPLE_RECORDS', 'IMPORT_RECORDS', 'EXPORT_VIEW', 'SEE_DELETED_RECORDS', 'CREATE_NEW_VIEW', 'HIDE_DELETED_RECORDS', 'GO_TO_PEOPLE', 'GO_TO_COMPANIES', 'GO_TO_DASHBOARDS', 'GO_TO_OPPORTUNITIES', 'GO_TO_SETTINGS', 'GO_TO_TASKS', 'GO_TO_NOTES', 'EDIT_RECORD_PAGE_LAYOUT', 'EDIT_DASHBOARD_LAYOUT', 'SAVE_DASHBOARD_LAYOUT', 'CANCEL_DASHBOARD_LAYOUT', 'DUPLICATE_DASHBOARD', 'GO_TO_WORKFLOWS', 'ACTIVATE_WORKFLOW', 'DEACTIVATE_WORKFLOW', 'DISCARD_DRAFT_WORKFLOW', 'TEST_WORKFLOW', 'SEE_ACTIVE_VERSION_WORKFLOW', 'SEE_RUNS_WORKFLOW', 'SEE_VERSIONS_WORKFLOW', 'ADD_NODE_WORKFLOW', 'TIDY_UP_WORKFLOW', 'DUPLICATE_WORKFLOW', 'GO_TO_RUNS', 'SEE_VERSION_WORKFLOW_RUN', 'SEE_WORKFLOW_WORKFLOW_RUN', 'STOP_WORKFLOW_RUN', 'SEE_RUNS_WORKFLOW_VERSION', 'SEE_WORKFLOW_WORKFLOW_VERSION', 'USE_AS_DRAFT_WORKFLOW_VERSION', 'SEE_VERSIONS_WORKFLOW_VERSION', 'SEARCH_RECORDS', 'SEARCH_RECORDS_FALLBACK', 'ASK_AI', 'VIEW_PREVIOUS_AI_CHATS')`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "core"."commandMenuItem" ALTER COLUMN "engineComponentKey" TYPE "core"."commandMenuItem_enginecomponentkey_enum" USING "engineComponentKey"::"text"::"core"."commandMenuItem_enginecomponentkey_enum"`,
+ );
+ await queryRunner.query(
+ `DROP TYPE "core"."commandMenuItem_enginecomponentkey_enum_old"`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `CREATE TYPE "core"."commandMenuItem_enginecomponentkey_enum_old" AS ENUM('ACTIVATE_WORKFLOW', 'ADD_NODE_WORKFLOW', 'ADD_TO_FAVORITES', 'ASK_AI', 'CANCEL_DASHBOARD_LAYOUT', 'CANCEL_RECORD_PAGE_LAYOUT', 'CREATE_NEW_RECORD', 'CREATE_NEW_VIEW', 'DEACTIVATE_WORKFLOW', 'DELETE_MULTIPLE_RECORDS', 'DELETE_SINGLE_RECORD', 'DESTROY_MULTIPLE_RECORDS', 'DESTROY_SINGLE_RECORD', 'DISCARD_DRAFT_WORKFLOW', 'DUPLICATE_DASHBOARD', 'DUPLICATE_WORKFLOW', 'EDIT_DASHBOARD_LAYOUT', 'EDIT_RECORD_PAGE_LAYOUT', 'EXPORT_FROM_RECORD_INDEX', 'EXPORT_FROM_RECORD_SHOW', 'EXPORT_MULTIPLE_RECORDS', 'EXPORT_NOTE_TO_PDF', 'EXPORT_VIEW', 'GO_TO_COMPANIES', 'GO_TO_DASHBOARDS', 'GO_TO_NOTES', 'GO_TO_OPPORTUNITIES', 'GO_TO_PEOPLE', 'GO_TO_RUNS', 'GO_TO_SETTINGS', 'GO_TO_TASKS', 'GO_TO_WORKFLOWS', 'HIDE_DELETED_RECORDS', 'IMPORT_RECORDS', 'MERGE_MULTIPLE_RECORDS', 'NAVIGATE_TO_NEXT_RECORD', 'NAVIGATE_TO_PREVIOUS_RECORD', 'REMOVE_FROM_FAVORITES', 'RESTORE_MULTIPLE_RECORDS', 'RESTORE_SINGLE_RECORD', 'SAVE_DASHBOARD_LAYOUT', 'SAVE_RECORD_PAGE_LAYOUT', 'SEARCH_RECORDS', 'SEARCH_RECORDS_FALLBACK', 'SEE_ACTIVE_VERSION_WORKFLOW', 'SEE_DELETED_RECORDS', 'SEE_RUNS_WORKFLOW', 'SEE_RUNS_WORKFLOW_VERSION', 'SEE_VERSIONS_WORKFLOW', 'SEE_VERSIONS_WORKFLOW_VERSION', 'SEE_VERSION_WORKFLOW_RUN', 'SEE_WORKFLOW_WORKFLOW_RUN', 'SEE_WORKFLOW_WORKFLOW_VERSION', 'STOP_WORKFLOW_RUN', 'TEST_WORKFLOW', 'TIDY_UP_WORKFLOW', 'UPDATE_MULTIPLE_RECORDS', 'USE_AS_DRAFT_WORKFLOW_VERSION', 'VIEW_PREVIOUS_AI_CHATS')`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "core"."commandMenuItem" ALTER COLUMN "engineComponentKey" TYPE "core"."commandMenuItem_enginecomponentkey_enum_old" USING "engineComponentKey"::"text"::"core"."commandMenuItem_enginecomponentkey_enum_old"`,
+ );
+ await queryRunner.query(
+ `DROP TYPE "core"."commandMenuItem_enginecomponentkey_enum"`,
+ );
+ await queryRunner.query(
+ `ALTER TYPE "core"."commandMenuItem_enginecomponentkey_enum_old" RENAME TO "commandMenuItem_enginecomponentkey_enum"`,
+ );
+ }
+}
diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts
index 5b0f29e9cc..4b6ffc48bd 100644
--- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum.ts
@@ -31,8 +31,6 @@ export enum EngineComponentKey {
GO_TO_TASKS = 'GO_TO_TASKS',
GO_TO_NOTES = 'GO_TO_NOTES',
EDIT_RECORD_PAGE_LAYOUT = 'EDIT_RECORD_PAGE_LAYOUT',
- SAVE_RECORD_PAGE_LAYOUT = 'SAVE_RECORD_PAGE_LAYOUT',
- CANCEL_RECORD_PAGE_LAYOUT = 'CANCEL_RECORD_PAGE_LAYOUT',
EDIT_DASHBOARD_LAYOUT = 'EDIT_DASHBOARD_LAYOUT',
SAVE_DASHBOARD_LAYOUT = 'SAVE_DASHBOARD_LAYOUT',
CANCEL_DASHBOARD_LAYOUT = 'CANCEL_DASHBOARD_LAYOUT',
diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts
index c07f82f211..f68d8e6359 100644
--- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts
@@ -440,36 +440,6 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
engineComponentKey: EngineComponentKey.EDIT_RECORD_PAGE_LAYOUT,
hotKeys: null,
},
- saveRecordPageLayout: {
- universalIdentifier: 'a3363589-e2a6-4451-a53c-b8c2710785e2',
- label: 'Save Page Layout',
- shortLabel: 'Save',
- icon: 'IconDeviceFloppy',
- position: 31,
- isPinned: true,
- availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
- conditionalAvailabilityExpression:
- 'pageType == "RECORD_PAGE" and isPageInEditMode and featureFlags.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED and noneDefined(selectedRecords, "deletedAt") and objectPermissions.canUpdateObjectRecords',
- availabilityObjectMetadataUniversalIdentifier: null,
- frontComponentUniversalIdentifier: null,
- engineComponentKey: EngineComponentKey.SAVE_RECORD_PAGE_LAYOUT,
- hotKeys: null,
- },
- cancelRecordPageLayout: {
- universalIdentifier: '0b9b4e93-2b4e-4ab0-908e-83ed1d674df7',
- label: 'Cancel Edition',
- shortLabel: 'Cancel',
- icon: 'IconCancel',
- position: 32,
- isPinned: true,
- availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
- conditionalAvailabilityExpression:
- 'pageType == "RECORD_PAGE" and isPageInEditMode and featureFlags.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED and noneDefined(selectedRecords, "deletedAt") and objectPermissions.canUpdateObjectRecords',
- availabilityObjectMetadataUniversalIdentifier: null,
- frontComponentUniversalIdentifier: null,
- engineComponentKey: EngineComponentKey.CANCEL_RECORD_PAGE_LAYOUT,
- hotKeys: null,
- },
editDashboardLayout: {
universalIdentifier: 'b9b53bbc-3129-4eb9-8344-c3f9628ffa7d',
label: 'Edit Dashboard',
diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
index 80306f1634..84a8db9b3b 100644
--- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
+++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
@@ -279,6 +279,7 @@ export {
IconNumber123,
IconNumber9,
IconNumbers,
+ IconPaint,
IconPaperclip,
IconPassword,
IconPencil,
diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts
index 9f23791674..6dae8d2297 100644
--- a/packages/twenty-ui/src/display/index.ts
+++ b/packages/twenty-ui/src/display/index.ts
@@ -354,6 +354,7 @@ export {
IconNumber123,
IconNumber9,
IconNumbers,
+ IconPaint,
IconPaperclip,
IconPassword,
IconPencil,