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,