From 3d27a1e48a83ea47f06b85fd134a48fc20a3f319 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Tue, 23 Sep 2025 10:36:37 +0200
Subject: [PATCH] Create a cancel action to leave the dashboard edit mode
without saving (#14650)
Closes https://github.com/twentyhq/core-team-issues/issues/1523
---
.../constants/DashboardActionsConfig.tsx | 38 ++++++++---
.../CancelDashboardSingleRecordAction.tsx | 27 ++++++++
.../types/DashboardSingleRecordActionKeys.ts | 1 +
...setDraftPageLayoutToPersistedPageLayout.ts | 64 ++++++++++++++++++
.../hooks/useSetDashboardInEditMode.ts | 1 +
.../PageLayoutInitializationQueryEffect.tsx | 23 +------
.../convertPageLayoutToTabLayouts.test.ts | 67 +++++++++++++++++++
.../utils/convertPageLayoutToTabLayouts.ts | 29 ++++++++
8 files changed, 220 insertions(+), 30 deletions(-)
create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/CancelDashboardSingleRecordAction.tsx
create mode 100644 packages/twenty-front/src/modules/dashboards/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts
create mode 100644 packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts
create mode 100644 packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts
diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DashboardActionsConfig.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DashboardActionsConfig.tsx
index 6e0bf94a11..9ddb3548b2 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DashboardActionsConfig.tsx
+++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DashboardActionsConfig.tsx
@@ -1,3 +1,4 @@
+import { CancelDashboardSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/dashboard-actions/components/CancelDashboardSingleRecordAction';
import { EditDashboardSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/dashboard-actions/components/EditDashboardSingleRecordAction';
import { DashboardSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/dashboard-actions/types/DashboardSingleRecordActionKeys';
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
@@ -7,7 +8,7 @@ import { ActionType } from '@/action-menu/actions/types/ActionType';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
-import { IconPencil } from 'twenty-ui/display';
+import { IconPencil, IconX } from 'twenty-ui/display';
export const DASHBOARD_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
config: {
@@ -28,6 +29,23 @@ export const DASHBOARD_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
availableOn: [ActionViewType.SHOW_PAGE],
component: ,
},
+ [DashboardSingleRecordActionKeys.CANCEL_DASHBOARD_EDITION]: {
+ key: DashboardSingleRecordActionKeys.CANCEL_DASHBOARD_EDITION,
+ label: msg`Cancel Edition`,
+ shortLabel: msg`Cancel`,
+ isPinned: true,
+ position: 2,
+ Icon: IconX,
+ type: ActionType.Standard,
+ scope: ActionScope.RecordSelection,
+ shouldBeRegistered: ({ selectedRecord }) =>
+ isDefined(selectedRecord) &&
+ !selectedRecord?.isRemote &&
+ !isDefined(selectedRecord?.deletedAt) &&
+ isDefined(selectedRecord?.pageLayoutId),
+ availableOn: [ActionViewType.SHOW_PAGE],
+ component: ,
+ },
},
actionKeys: [
SingleRecordActionKeys.ADD_TO_FAVORITES,
@@ -41,33 +59,33 @@ export const DASHBOARD_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
],
propertiesToOverwrite: {
[SingleRecordActionKeys.ADD_TO_FAVORITES]: {
- position: 2,
- },
- [SingleRecordActionKeys.REMOVE_FROM_FAVORITES]: {
position: 3,
},
- [SingleRecordActionKeys.DELETE]: {
+ [SingleRecordActionKeys.REMOVE_FROM_FAVORITES]: {
position: 4,
+ },
+ [SingleRecordActionKeys.DELETE]: {
+ position: 5,
label: msg`Delete dashboard`,
},
[SingleRecordActionKeys.EXPORT_FROM_RECORD_SHOW]: {
- position: 5,
+ position: 6,
label: msg`Export dashboard`,
},
[SingleRecordActionKeys.DESTROY]: {
- position: 6,
+ position: 7,
label: msg`Permanently destroy dashboard`,
},
[SingleRecordActionKeys.RESTORE]: {
- position: 7,
+ position: 8,
label: msg`Restore dashboard`,
},
[SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD]: {
- position: 8,
+ position: 9,
label: msg`Navigate to previous dashboard`,
},
[SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD]: {
- position: 9,
+ position: 10,
label: msg`Navigate to next dashboard`,
},
},
diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/CancelDashboardSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/CancelDashboardSingleRecordAction.tsx
new file mode 100644
index 0000000000..10d4a3b064
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/CancelDashboardSingleRecordAction.tsx
@@ -0,0 +1,27 @@
+import { Action } from '@/action-menu/actions/components/Action';
+import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
+import { useResetDraftPageLayoutToPersistedPageLayout } from '@/dashboards/hooks/useResetDraftPageLayoutToPersistedPageLayout';
+import { useSetIsDashboardInEditMode } from '@/dashboards/hooks/useSetDashboardInEditMode';
+import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
+import { useRecoilValue } from 'recoil';
+
+export const CancelDashboardSingleRecordAction = () => {
+ const recordId = useSelectedRecordIdOrThrow();
+
+ const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
+
+ const pageLayoutId = selectedRecord?.pageLayoutId;
+
+ const { setIsDashboardInEditMode } =
+ useSetIsDashboardInEditMode(pageLayoutId);
+
+ const { resetDraftPageLayoutToPersistedPageLayout } =
+ useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
+
+ const handleClick = () => {
+ resetDraftPageLayoutToPersistedPageLayout();
+ setIsDashboardInEditMode(false);
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/types/DashboardSingleRecordActionKeys.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/types/DashboardSingleRecordActionKeys.ts
index 3215b42c31..0fb6b7db0d 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/types/DashboardSingleRecordActionKeys.ts
+++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/types/DashboardSingleRecordActionKeys.ts
@@ -1,3 +1,4 @@
export enum DashboardSingleRecordActionKeys {
EDIT_DASHBOARD = 'edit-dashboard-single-record',
+ CANCEL_DASHBOARD_EDITION = 'cancel-dashboard-edition-single-record',
}
diff --git a/packages/twenty-front/src/modules/dashboards/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts b/packages/twenty-front/src/modules/dashboards/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts
new file mode 100644
index 0000000000..37a6c7c898
--- /dev/null
+++ b/packages/twenty-front/src/modules/dashboards/hooks/useResetDraftPageLayoutToPersistedPageLayout.ts
@@ -0,0 +1,64 @@
+import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
+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 { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
+import { useRecoilCallback } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
+
+export const useResetDraftPageLayoutToPersistedPageLayout = (
+ pageLayoutIdFromProps?: string,
+) => {
+ const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
+ PageLayoutComponentInstanceContext,
+ pageLayoutIdFromProps,
+ );
+
+ const pageLayoutDraftState = useRecoilComponentCallbackState(
+ pageLayoutDraftComponentState,
+ componentInstanceId,
+ );
+
+ const pageLayoutPersistedState = useRecoilComponentCallbackState(
+ pageLayoutPersistedComponentState,
+ componentInstanceId,
+ );
+
+ const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
+ pageLayoutCurrentLayoutsComponentState,
+ componentInstanceId,
+ );
+
+ const resetDraftPageLayoutToPersistedPageLayout = useRecoilCallback(
+ ({ set, snapshot }) =>
+ () => {
+ const pageLayoutPersisted = snapshot
+ .getLoadable(pageLayoutPersistedState)
+ .getValue();
+
+ if (isDefined(pageLayoutPersisted)) {
+ set(pageLayoutDraftState, {
+ id: pageLayoutPersisted.id,
+ name: pageLayoutPersisted.name,
+ type: pageLayoutPersisted.type,
+ objectMetadataId: pageLayoutPersisted.objectMetadataId,
+ tabs: pageLayoutPersisted.tabs,
+ });
+
+ const tabLayouts = convertPageLayoutToTabLayouts(pageLayoutPersisted);
+ set(pageLayoutCurrentLayoutsState, tabLayouts);
+ }
+ },
+ [
+ pageLayoutDraftState,
+ pageLayoutPersistedState,
+ pageLayoutCurrentLayoutsState,
+ ],
+ );
+
+ return {
+ resetDraftPageLayoutToPersistedPageLayout,
+ };
+};
diff --git a/packages/twenty-front/src/modules/dashboards/hooks/useSetDashboardInEditMode.ts b/packages/twenty-front/src/modules/dashboards/hooks/useSetDashboardInEditMode.ts
index 343418a271..ca20d44e04 100644
--- a/packages/twenty-front/src/modules/dashboards/hooks/useSetDashboardInEditMode.ts
+++ b/packages/twenty-front/src/modules/dashboards/hooks/useSetDashboardInEditMode.ts
@@ -28,6 +28,7 @@ export const useSetIsDashboardInEditMode = (pageLayoutIdFromProps: string) => {
set(forceRegisteredActionsByKeyState, (prev) => ({
...prev,
[DashboardSingleRecordActionKeys.EDIT_DASHBOARD]: !value,
+ [DashboardSingleRecordActionKeys.CANCEL_DASHBOARD_EDITION]: value,
}));
},
[forceRegisteredActionsByKeyState, isPageLayoutInEditModeState],
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 7bc9839586..625f015bca 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutInitializationQueryEffect.tsx
@@ -3,7 +3,7 @@ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pag
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { type PageLayoutWithData } from '@/page-layout/types/pageLayoutTypes';
-import { type TabLayouts } from '@/page-layout/types/tab-layouts';
+import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useQuery } from '@apollo/client';
@@ -59,25 +59,8 @@ export const PageLayoutInitializationQueryEffect = ({
tabs: layout.tabs,
});
- if (layout.tabs.length > 0) {
- const tabLayouts: TabLayouts = {};
- layout.tabs.forEach((tab) => {
- const layouts = tab.widgets.map((w) => ({
- i: w.id,
- x: w.gridPosition.column,
- y: w.gridPosition.row,
- w: w.gridPosition.columnSpan,
- h: w.gridPosition.rowSpan,
- }));
- tabLayouts[tab.id] = {
- desktop: layouts,
- mobile: layouts.map((l) => ({ ...l, w: 1, x: 0 })),
- };
- });
- set(pageLayoutCurrentLayoutsComponentCallbackState, tabLayouts);
- } else {
- set(pageLayoutCurrentLayoutsComponentCallbackState, {});
- }
+ const tabLayouts = convertPageLayoutToTabLayouts(layout);
+ set(pageLayoutCurrentLayoutsComponentCallbackState, tabLayouts);
}
},
[
diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts
new file mode 100644
index 0000000000..3818592412
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/convertPageLayoutToTabLayouts.test.ts
@@ -0,0 +1,67 @@
+import { type PageLayoutWithData } from '@/page-layout/types/pageLayoutTypes';
+import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
+import { PageLayoutType, WidgetType } from '~/generated/graphql';
+
+describe('convertPageLayoutToTabLayouts', () => {
+ it('should convert page layout to tab layouts', () => {
+ const pageLayout: PageLayoutWithData = {
+ id: 'page-layout-1',
+ name: 'Page Layout 1',
+ type: PageLayoutType.RECORD_PAGE,
+ objectMetadataId: 'object-metadata-1',
+ tabs: [
+ {
+ id: 'tab-1',
+ title: 'Tab 1',
+ position: 0,
+ pageLayoutId: 'page-layout-1',
+ widgets: [
+ {
+ id: 'widget-1',
+ pageLayoutTabId: 'tab-1',
+ title: 'Widget 1',
+ type: WidgetType.GRAPH,
+ gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 2 },
+ objectMetadataId: 'object-metadata-1',
+ createdAt: '2025-01-01T00:00:00.000Z',
+ updatedAt: '2025-01-01T00:00:00.000Z',
+ deletedAt: null,
+ },
+ {
+ id: 'widget-2',
+ pageLayoutTabId: 'tab-1',
+ title: 'Widget 2',
+ type: WidgetType.GRAPH,
+ gridPosition: { row: 2, column: 0, rowSpan: 2, columnSpan: 2 },
+ objectMetadataId: 'object-metadata-1',
+ createdAt: '2025-01-01T00:00:00.000Z',
+ updatedAt: '2025-01-01T00:00:00.000Z',
+ deletedAt: null,
+ },
+ ],
+ createdAt: '2025-01-01T00:00:00.000Z',
+ updatedAt: '2025-01-01T00:00:00.000Z',
+ deletedAt: null,
+ },
+ ],
+ createdAt: '2025-01-01T00:00:00.000Z',
+ updatedAt: '2025-01-01T00:00:00.000Z',
+ deletedAt: null,
+ };
+
+ const result = convertPageLayoutToTabLayouts(pageLayout);
+
+ expect(result).toEqual({
+ 'tab-1': {
+ desktop: [
+ { i: 'widget-1', x: 0, y: 0, w: 2, h: 2 },
+ { i: 'widget-2', x: 0, y: 2, w: 2, h: 2 },
+ ],
+ mobile: [
+ { i: 'widget-1', x: 0, y: 0, w: 1, h: 2 },
+ { i: 'widget-2', x: 0, y: 2, w: 1, h: 2 },
+ ],
+ },
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts b/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts
new file mode 100644
index 0000000000..0788d1f235
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/utils/convertPageLayoutToTabLayouts.ts
@@ -0,0 +1,29 @@
+import { type PageLayoutWithData } from '@/page-layout/types/pageLayoutTypes';
+import { type TabLayouts } from '@/page-layout/types/tab-layouts';
+
+export const convertPageLayoutToTabLayouts = (
+ pageLayout: PageLayoutWithData,
+): TabLayouts => {
+ if (pageLayout.tabs.length === 0) {
+ return {};
+ }
+
+ const tabLayouts: TabLayouts = {};
+
+ pageLayout.tabs.forEach((tab) => {
+ const layouts = tab.widgets.map((widget) => ({
+ i: widget.id,
+ x: widget.gridPosition.column,
+ y: widget.gridPosition.row,
+ w: widget.gridPosition.columnSpan,
+ h: widget.gridPosition.rowSpan,
+ }));
+
+ tabLayouts[tab.id] = {
+ desktop: layouts,
+ mobile: layouts.map((layout) => ({ ...layout, w: 1, x: 0 })),
+ };
+ });
+
+ return tabLayouts;
+};