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; +};