Clear stale parent-view filters on front-component cross-object navigation (#21869)

This commit is contained in:
Raphaël Bosi
2026-06-19 19:21:54 +02:00
committed by GitHub
parent a658a8dbb4
commit f9084fd208
2 changed files with 105 additions and 1 deletions
@@ -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', () => {
@@ -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<typeof navigateApp>[1],