diff --git a/packages/twenty-front/src/modules/front-components/hooks/__tests__/useFrontComponentExecutionContext.test.tsx b/packages/twenty-front/src/modules/front-components/hooks/__tests__/useFrontComponentExecutionContext.test.tsx index 9290ce009c..d84432ccab 100644 --- a/packages/twenty-front/src/modules/front-components/hooks/__tests__/useFrontComponentExecutionContext.test.tsx +++ b/packages/twenty-front/src/modules/front-components/hooks/__tests__/useFrontComponentExecutionContext.test.tsx @@ -1,7 +1,11 @@ import { i18n } from '@lingui/core'; import { I18nProvider } from '@lingui/react'; import { act, renderHook } from '@testing-library/react'; +import { getDefaultStore } from 'jotai'; +import { AppPath } from 'twenty-shared/types'; +import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; +import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState'; import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext'; const mockNavigateApp = jest.fn(); @@ -109,10 +113,24 @@ const renderUseFrontComponentExecutionContext = ( const FRONT_COMPONENT_ID = 'fc-test-id'; const COMMAND_MENU_ITEM_ID = 'cmd-item-1'; +const parentViewAtom = + contextStoreRecordShowParentViewComponentState.atomFamily({ + instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID, + }); + +const createParentView = (parentViewObjectNameSingular: string) => ({ + parentViewComponentId: 'parent-view-component-id', + parentViewObjectNameSingular, + parentViewFilterGroups: [], + parentViewFilters: [], + parentViewSorts: [], +}); + describe('useFrontComponentExecutionContext', () => { beforeEach(() => { jest.clearAllMocks(); mockCurrentUser = { id: 'user-123' }; + getDefaultStore().set(parentViewAtom, undefined); }); describe('executionContext', () => { @@ -207,6 +225,67 @@ describe('useFrontComponentExecutionContext', () => { { replace: true }, ); }); + + it('should clear stale parent-view state when navigating to a record of a different object', async () => { + const store = getDefaultStore(); + store.set(parentViewAtom, createParentView('company')); + + const { result } = renderUseFrontComponentExecutionContext({ + frontComponentId: FRONT_COMPONENT_ID, + }); + + await act(async () => { + await result.current.frontComponentHostCommunicationApi.navigate( + AppPath.RecordShowPage, + { objectNameSingular: 'person', objectRecordId: 'record-1' }, + ); + }); + + expect(store.get(parentViewAtom)).toBeUndefined(); + expect(mockNavigateApp).toHaveBeenCalledWith( + AppPath.RecordShowPage, + { objectNameSingular: 'person', objectRecordId: 'record-1' }, + undefined, + undefined, + ); + }); + + it('should keep parent-view state when navigating to a record of the same object', async () => { + const store = getDefaultStore(); + const parentView = createParentView('company'); + store.set(parentViewAtom, parentView); + + const { result } = renderUseFrontComponentExecutionContext({ + frontComponentId: FRONT_COMPONENT_ID, + }); + + await act(async () => { + await result.current.frontComponentHostCommunicationApi.navigate( + AppPath.RecordShowPage, + { objectNameSingular: 'company', objectRecordId: 'record-2' }, + ); + }); + + expect(store.get(parentViewAtom)).toEqual(parentView); + }); + + it('should keep parent-view state when navigating to a non-record page', async () => { + const store = getDefaultStore(); + const parentView = createParentView('company'); + store.set(parentViewAtom, parentView); + + const { result } = renderUseFrontComponentExecutionContext({ + frontComponentId: FRONT_COMPONENT_ID, + }); + + await act(async () => { + await result.current.frontComponentHostCommunicationApi.navigate( + AppPath.SettingsCatchAll, + ); + }); + + expect(store.get(parentViewAtom)).toEqual(parentView); + }); }); describe('openSidePanelPage', () => { diff --git a/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts b/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts index 3b85f8206b..3f7a4c6bb2 100644 --- a/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts +++ b/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts @@ -6,12 +6,14 @@ import { type FrontComponentExecutionContext, type FrontComponentHostCommunicationApi, } from 'twenty-front-component-renderer'; -import { type AppPath, type EnqueueSnackbarParams } from 'twenty-shared/types'; +import { AppPath, type EnqueueSnackbarParams } from 'twenty-shared/types'; import { currentUserState } from '@/auth/states/currentUserState'; import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal'; import { useUnmountCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand'; import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState'; +import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; +import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState'; import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh'; import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel'; import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; @@ -19,6 +21,7 @@ import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState'; +import { useStore } from 'jotai'; import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/icon'; import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; @@ -44,6 +47,7 @@ export const useFrontComponentExecutionContext = ({ } => { const currentUser = useAtomStateValue(currentUserState); const navigateApp = useNavigateApp(); + const store = useStore(); const { requestAccessTokenRefresh } = useRequestApplicationTokenRefresh({ frontComponentId, }); @@ -74,6 +78,27 @@ export const useFrontComponentExecutionContext = ({ queryParams, options, ) => { + if (to === AppPath.RecordShowPage) { + const targetObjectNameSingular = ( + params as { objectNameSingular?: string | null } | undefined + )?.objectNameSingular; + + const parentViewAtom = + contextStoreRecordShowParentViewComponentState.atomFamily({ + instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID, + }); + + const parentView = store.get(parentViewAtom); + + if ( + isDefined(parentView) && + isDefined(targetObjectNameSingular) && + parentView.parentViewObjectNameSingular !== targetObjectNameSingular + ) { + store.set(parentViewAtom, undefined); + } + } + navigateApp( to as AppPath, params as Parameters[1],