fix: filtered view resetting to unfiltered list on navigation (#21080)

## Summary
- Resolves #21079 
- Object navigation links no longer force the default index view, which
had no saved filters.
- Returning to an object after “Save as new view” now opens the last
visited (filtered) view instead of the unfiltered default list.

## Test plan
1. Add a filter -> save as new view -> list is filtered
2. Navigate away and back -> filters still applied (not reset to
unfiltered)

## Screencast
### Before

https://github.com/user-attachments/assets/25326339-a3a1-4171-89cc-5149e254982e

### After

https://github.com/user-attachments/assets/b4600043-fc7a-4670-9c68-23daa6c31ec8

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Parship Chowdhury
2026-06-19 14:10:14 +05:30
committed by GitHub
parent 99f4084a05
commit c32cb78562
13 changed files with 226 additions and 45 deletions
@@ -96,11 +96,11 @@ export const NavigationMenuItemIcon = ({
}
if (navigationMenuItem.type === NavigationMenuItemType.LINK) {
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
const computedLink = getNavigationMenuItemComputedLink({
item: navigationMenuItem,
objectMetadataItems,
views,
);
});
return (
<LinkIconWithLinkOverlay
link={computedLink}
@@ -15,6 +15,7 @@ import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMeta
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
type NavigationMenuItemFolderContentProps = {
folderId: string;
@@ -29,6 +30,9 @@ export const NavigationMenuItemFolderContent = ({
}: NavigationMenuItemFolderContentProps) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
lastVisitedViewPerObjectMetadataItemState,
);
const { deleteManyNavigationMenuItems } = useDeleteManyNavigationMenuItems();
const folderDroppableId = `${NavigationMenuItemDroppableIds.FAVORITE_FOLDER_PREFIX}${folderId}`;
@@ -42,11 +46,12 @@ export const NavigationMenuItemFolderContent = ({
objectMetadataItems,
views,
);
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
const computedLink = getNavigationMenuItemComputedLink({
item: navigationMenuItem,
objectMetadataItems,
views,
);
lastVisitedViewPerObjectMetadataItem,
});
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
@@ -21,6 +21,7 @@ import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-dr
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 { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
type NavigationMenuItemFolderSubItemProps = {
navigationMenuItem: NavigationMenuItem;
@@ -50,6 +51,9 @@ export const NavigationMenuItemFolderSubItem = ({
useIsNavigationMenuItemEditHighlighted(navigationMenuItem);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
lastVisitedViewPerObjectMetadataItemState,
);
const navigate = useNavigate();
const setLastClickedNavigationMenuItemId = useSetAtomState(
lastClickedNavigationMenuItemIdState,
@@ -65,11 +69,12 @@ export const NavigationMenuItemFolderSubItem = ({
objectMetadataItems,
views,
);
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
const computedLink = getNavigationMenuItemComputedLink({
item: navigationMenuItem,
objectMetadataItems,
views,
);
lastVisitedViewPerObjectMetadataItem,
});
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
@@ -16,6 +16,7 @@ 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 { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
type UseNavigationMenuItemFolderOpenStateParams = {
folderId: string;
@@ -30,6 +31,9 @@ export const useNavigationMenuItemFolderOpenState = ({
const isMobile = useIsMobile();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
lastVisitedViewPerObjectMetadataItemState,
);
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
useAtomState(openNavigationMenuItemFolderIdsState);
@@ -74,20 +78,22 @@ export const useNavigationMenuItemFolderOpenState = ({
if (item.type === NavigationMenuItemType.LINK) {
return false;
}
const computedLink = getNavigationMenuItemComputedLink(
const computedLink = getNavigationMenuItemComputedLink({
item,
objectMetadataItems,
views,
);
lastVisitedViewPerObjectMetadataItem,
});
return isNonEmptyString(computedLink);
},
);
if (isDefined(firstNonLinkItem)) {
const link = getNavigationMenuItemComputedLink(
firstNonLinkItem,
const link = getNavigationMenuItemComputedLink({
item: firstNonLinkItem,
objectMetadataItems,
views,
);
lastVisitedViewPerObjectMetadataItem,
});
if (isNonEmptyString(link)) {
setLastClickedNavigationMenuItemId(firstNonLinkItem.id);
navigate(link);
@@ -13,6 +13,7 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilte
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
export const useIdentifyActiveNavigationMenuItems = (): {
activeNavigationMenuItemIds: string[];
@@ -23,6 +24,9 @@ export const useIdentifyActiveNavigationMenuItems = (): {
lastClickedNavigationMenuItemIdState,
);
const views = useAtomStateValue(viewsSelector);
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
lastVisitedViewPerObjectMetadataItemState,
);
const { activeObjectMetadataItems, objectMetadataItems } =
useFilteredObjectMetadataItems();
@@ -64,11 +68,12 @@ export const useIdentifyActiveNavigationMenuItems = (): {
if (isDefined(lastClickedItem)) {
const lastClickedNavigationMenuItemLink =
getNavigationMenuItemComputedLink(
lastClickedItem,
getNavigationMenuItemComputedLink({
item: lastClickedItem,
objectMetadataItems,
views,
);
lastVisitedViewPerObjectMetadataItem,
});
const lastClickedObjectMetadataId =
getObjectMetadataForNavigationMenuItem(
lastClickedItem,
@@ -100,11 +105,12 @@ export const useIdentifyActiveNavigationMenuItems = (): {
if (item.type !== NavigationMenuItemType.RECORD) {
return false;
}
const link = getNavigationMenuItemComputedLink(
const link = getNavigationMenuItemComputedLink({
item,
objectMetadataItems,
views,
);
lastVisitedViewPerObjectMetadataItem,
});
return link === currentPath;
})
.map((item) => item.id);
@@ -174,6 +180,7 @@ export const useIdentifyActiveNavigationMenuItems = (): {
lastClickedNavigationMenuItemId,
objectMetadataItems,
views,
lastVisitedViewPerObjectMetadataItem,
currentPathWithSearch,
currentPath,
currentObjectMetadataItem,
@@ -85,11 +85,12 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
const hasNavigationMenuItem = isRecord || isView || isObject;
const navigationPath = hasNavigationMenuItem
? getNavigationMenuItemComputedLink(
navigationMenuItem!,
? getNavigationMenuItemComputedLink({
item: navigationMenuItem!,
objectMetadataItems,
views,
)
lastVisitedViewPerObjectMetadataItem,
})
: getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
@@ -0,0 +1,62 @@
import { getObjectNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/object/utils/getObjectNavigationMenuItemComputedLink';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type View } from '@/views/types/View';
import { ViewKey } from '~/generated-metadata/graphql';
const mockObjectMetadataItems: Pick<
EnrichedObjectMetadataItem,
'id' | 'namePlural'
>[] = [
{
id: 'metadata-1',
namePlural: 'people',
},
];
const mockViews: Pick<View, 'id' | 'objectMetadataId' | 'key'>[] = [
{ id: 'view-index', objectMetadataId: 'metadata-1', key: ViewKey.INDEX },
];
describe('getObjectNavigationMenuItemComputedLink', () => {
it('should link to the index view when no last visited view is provided', () => {
const result = getObjectNavigationMenuItemComputedLink(
{ targetObjectMetadataId: 'metadata-1' },
mockObjectMetadataItems,
mockViews,
);
expect(result).toBe('/objects/people?viewId=view-index');
});
it('should link to the last visited view when provided, overriding the index view', () => {
const result = getObjectNavigationMenuItemComputedLink(
{ targetObjectMetadataId: 'metadata-1' },
mockObjectMetadataItems,
mockViews,
'view-42',
);
expect(result).toBe('/objects/people?viewId=view-42');
});
it('should link to the bare object path when neither a last visited nor an index view exists', () => {
const result = getObjectNavigationMenuItemComputedLink(
{ targetObjectMetadataId: 'metadata-1' },
mockObjectMetadataItems,
[],
);
expect(result).toBe('/objects/people');
});
it('should return an empty string when the target object metadata is not found', () => {
const result = getObjectNavigationMenuItemComputedLink(
{ targetObjectMetadataId: 'non-existent-metadata' },
mockObjectMetadataItems,
mockViews,
'view-42',
);
expect(result).toBe('');
});
});
@@ -7,8 +7,9 @@ import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const getObjectNavigationMenuItemComputedLink = (
item: Pick<NavigationMenuItem, 'targetObjectMetadataId'>,
objectMetadataItems: EnrichedObjectMetadataItem[],
objectMetadataItems: Pick<EnrichedObjectMetadataItem, 'id' | 'namePlural'>[],
views: Pick<View, 'id' | 'objectMetadataId' | 'key'>[],
lastVisitedViewId?: string,
): string => {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
@@ -16,14 +17,18 @@ export const getObjectNavigationMenuItemComputedLink = (
if (!isDefined(objectMetadataItem)) {
return '';
}
const indexView = views.find(
const indexViewId = views.find(
(view) =>
view.objectMetadataId === objectMetadataItem.id &&
view.key === ViewKey.INDEX,
);
)?.id;
const targetViewId = lastVisitedViewId ?? indexViewId;
return getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
indexView ? { viewId: indexView.id } : {},
isDefined(targetViewId) ? { viewId: targetViewId } : undefined,
);
};
@@ -35,6 +35,7 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
const StyledRightIconsContainer = styled.div`
align-items: center;
@@ -47,6 +48,9 @@ export const WorkspaceSection = () => {
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
lastVisitedViewPerObjectMetadataItemState,
);
const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode();
const isLayoutCustomizationModeEnabled = useAtomStateValue(
isLayoutCustomizationModeEnabledState,
@@ -85,19 +89,21 @@ export const WorkspaceSection = () => {
const firstChild = workspaceNavigationMenuItemsSorted.find((navItem) => {
if (navItem.folderId !== folderId) return false;
if (navItem.type === NavigationMenuItemType.LINK) return false;
const link = getNavigationMenuItemComputedLink(
navItem,
const link = getNavigationMenuItemComputedLink({
item: navItem,
objectMetadataItems,
views,
);
lastVisitedViewPerObjectMetadataItem,
});
return isNonEmptyString(link);
});
if (firstChild) {
const link = getNavigationMenuItemComputedLink(
firstChild,
const link = getNavigationMenuItemComputedLink({
item: firstChild,
objectMetadataItems,
views,
);
lastVisitedViewPerObjectMetadataItem,
});
if (isNonEmptyString(link)) {
navigate(link);
}
@@ -119,11 +125,12 @@ export const WorkspaceSection = () => {
pageIcon: getIcon(objectMetadataItem.icon),
});
}
const link = getNavigationMenuItemComputedLink(
const link = getNavigationMenuItemComputedLink({
item,
objectMetadataItems,
views,
);
lastVisitedViewPerObjectMetadataItem,
});
if (isNonEmptyString(link)) {
navigate(link);
}
@@ -0,0 +1,65 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type View } from '@/views/types/View';
import {
NavigationMenuItemType,
ViewKey,
type NavigationMenuItem,
} from '~/generated-metadata/graphql';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
const objectMetadataItems = [
{ id: 'obj-1', namePlural: 'people' },
{ id: 'obj-2', namePlural: 'companies' },
] as EnrichedObjectMetadataItem[];
const views: Pick<View, 'id' | 'objectMetadataId' | 'key'>[] = [
{ id: 'view-index', objectMetadataId: 'obj-1', key: ViewKey.INDEX },
];
const objectItem: NavigationMenuItem = {
id: 'nav-1',
type: NavigationMenuItemType.OBJECT,
targetObjectMetadataId: 'obj-1',
position: 0,
createdAt: '',
updatedAt: '',
};
describe('getNavigationMenuItemComputedLink', () => {
it('should link an object item to its last visited view when one is recorded', () => {
const link = getNavigationMenuItemComputedLink({
item: objectItem,
objectMetadataItems,
views,
lastVisitedViewPerObjectMetadataItem: { 'obj-1': 'view-custom' },
});
expect(link).toBe('/objects/people?viewId=view-custom');
});
// Regression: the link used to always force ?viewId=<index view>, ignoring
// any last visited view. The last visited view now takes precedence, and the
// index view is only the fallback when none is recorded.
it('should fall back to the index view when no last visited view is recorded', () => {
const link = getNavigationMenuItemComputedLink({
item: objectItem,
objectMetadataItems,
views,
lastVisitedViewPerObjectMetadataItem: null,
});
expect(link).toBe('/objects/people?viewId=view-index');
});
it('should fall back to the index view rather than another object last visited view', () => {
const link = getNavigationMenuItemComputedLink({
item: objectItem,
objectMetadataItems,
views,
lastVisitedViewPerObjectMetadataItem: { 'obj-2': 'view-other' },
});
expect(link).toBe('/objects/people?viewId=view-index');
});
});
@@ -6,20 +6,33 @@ import { getViewNavigationMenuItemComputedLink } from '@/navigation-menu-item/di
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type View } from '@/views/types/View';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const getNavigationMenuItemComputedLink = (
item: NavigationMenuItem,
objectMetadataItems: EnrichedObjectMetadataItem[],
views: Pick<View, 'id' | 'objectMetadataId' | 'key'>[],
): string => {
export const getNavigationMenuItemComputedLink = ({
item,
objectMetadataItems,
views,
lastVisitedViewPerObjectMetadataItem,
}: {
item: NavigationMenuItem;
objectMetadataItems: EnrichedObjectMetadataItem[];
views: Pick<View, 'id' | 'objectMetadataId' | 'key'>[];
lastVisitedViewPerObjectMetadataItem?: Record<string, string> | null;
}): string => {
switch (item.type) {
case NavigationMenuItemType.OBJECT:
case NavigationMenuItemType.OBJECT: {
const lastVisitedViewId = isDefined(item.targetObjectMetadataId)
? lastVisitedViewPerObjectMetadataItem?.[item.targetObjectMetadataId]
: undefined;
return getObjectNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
lastVisitedViewId,
);
}
case NavigationMenuItemType.VIEW:
return getViewNavigationMenuItemComputedLink(
item,
@@ -21,6 +21,7 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
const computeInsertionPosition = (
selectedItem: { id: string; folderId?: string | null },
@@ -65,6 +66,9 @@ export const useNavigationMenuItemEditOrganizeActions =
const { moveUp, moveDown, remove } = useNavigationMenuItemMoveRemove();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
lastVisitedViewPerObjectMetadataItemState,
);
const selectedItem = selectedNavigationMenuItemIdInEditMode
? items.find((item) => item.id === selectedNavigationMenuItemIdInEditMode)
@@ -116,11 +120,12 @@ export const useNavigationMenuItemEditOrganizeActions =
if (isDefined(nextItem)) {
setSelectedNavigationMenuItemIdInEditMode(nextItem.id);
const link = getNavigationMenuItemComputedLink(
nextItem,
const link = getNavigationMenuItemComputedLink({
item: nextItem,
objectMetadataItems,
views,
);
lastVisitedViewPerObjectMetadataItem,
});
if (isNonEmptyString(link)) {
navigate(link);
}
@@ -56,11 +56,11 @@ export const getFirstNavigationMenuItemLink = ({
}
}
const link = getNavigationMenuItemComputedLink(
const link = getNavigationMenuItemComputedLink({
item,
objectMetadataItems,
views,
);
});
if (isNonEmptyString(link)) {
return link;