fix(front): home redirect honors first object of the navigation menu (#21626)
https://github.com/user-attachments/assets/db11f02c-4502-4adc-8cf7-55d3dc0211a6 ## Summary Fixes [#21166](https://github.com/twentyhq/twenty/issues/21166). Going to the workspace root (`/`) always redirected to the alphabetically-first object. Following [the review discussion](https://github.com/twentyhq/twenty/issues/21166#issuecomment-4611570420), this redirects to the **first object of the navigation menu** instead of the last-visited object. "Last-visited" had too many edge cases (which page? records vs. index?), whereas the first item of the user's menu is unambiguous and matches what they see at the top of the left sidebar. ## Changes - New `getFirstObjectNavigationMenuItemLink` util walks the workspace navigation menu items in display order (sorted by `position`) and returns the link of the first object-backed item (`OBJECT`/`VIEW`) the user can read. - `useDefaultHomePagePath` uses it as the primary target. It stays on `AppPath.Index` until navigation menu items have loaded (they load *after* the minimal-metadata fast path, so resolving earlier would land on the wrong object during the post-login window), then falls back to the first readable object if the menu has no object item. Scope is intentionally minimal: the last-visited tracking is left in place and will be cleaned up separately now that the redirect no longer reads it. ## Test plan - `npx jest useDefaultHomePagePath --config=packages/twenty-front/jest.config.mjs` — covers menu order honored over alphabetical, `VIEW` item links, the loading deferrals (object metadata + navigation menu items), the readable-object fallback, and the no-readable-object profile-settings fallback. - Manually: visit a non-first object, navigate to `/`, confirm the redirect goes to the first object of the navigation menu.
This commit is contained in:
+174
-11
@@ -2,41 +2,113 @@ import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import { createElement, useEffect, type ReactNode } from 'react';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { AppPath, SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
type NavigationMenuItem,
|
||||
NavigationMenuItemType,
|
||||
ViewOpenRecordIn,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { mockedUserData } from '~/testing/mock-data/users';
|
||||
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
|
||||
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
|
||||
import { setTestViewsInMetadataStore } from '~/testing/utils/setTestViewsInMetadataStore';
|
||||
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
|
||||
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
|
||||
import { setTestViewsInMetadataStore } from '~/testing/utils/setTestViewsInMetadataStore';
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) =>
|
||||
createElement(JotaiProvider, { store: jotaiStore }, children);
|
||||
|
||||
const buildObjectNavigationMenuItem = (
|
||||
objectNameSingular: string,
|
||||
position: number,
|
||||
folderId?: string,
|
||||
): NavigationMenuItem => ({
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: `navigation-menu-item-${objectNameSingular}`,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectMetadataId:
|
||||
getMockObjectMetadataItemOrThrow(objectNameSingular).id,
|
||||
position,
|
||||
folderId,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const buildFolderNavigationMenuItem = (
|
||||
id: string,
|
||||
position: number,
|
||||
): NavigationMenuItem => ({
|
||||
__typename: 'NavigationMenuItem',
|
||||
id,
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
name: 'Folder',
|
||||
position,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const buildPageLayoutNavigationMenuItem = (
|
||||
pageLayoutId: string,
|
||||
position: number,
|
||||
): NavigationMenuItem => ({
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: `navigation-menu-item-page-layout-${pageLayoutId}`,
|
||||
type: NavigationMenuItemType.PAGE_LAYOUT,
|
||||
pageLayoutId,
|
||||
position,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const buildLinkNavigationMenuItem = (position: number): NavigationMenuItem => ({
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: `navigation-menu-item-link-${position}`,
|
||||
type: NavigationMenuItemType.LINK,
|
||||
link: 'https://example.com',
|
||||
position,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const buildViewNavigationMenuItem = (
|
||||
viewId: string,
|
||||
position: number,
|
||||
): NavigationMenuItem => ({
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: `navigation-menu-item-view-${viewId}`,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewId,
|
||||
position,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const renderHooks = ({
|
||||
withCurrentUser,
|
||||
withExistingView,
|
||||
withObjectMetadataLoaded = true,
|
||||
objectMetadataItems = getTestEnrichedObjectMetadataItemsMock(),
|
||||
navigationMenuItems = [],
|
||||
withNavigationMenuItemsLoaded = true,
|
||||
}: {
|
||||
withCurrentUser: boolean;
|
||||
withExistingView: boolean;
|
||||
withObjectMetadataLoaded?: boolean;
|
||||
objectMetadataItems?: EnrichedObjectMetadataItem[];
|
||||
navigationMenuItems?: NavigationMenuItem[];
|
||||
withNavigationMenuItemsLoaded?: boolean;
|
||||
}) => {
|
||||
if (withObjectMetadataLoaded) {
|
||||
setTestObjectMetadataItemsInMetadataStore(
|
||||
jotaiStore,
|
||||
getTestEnrichedObjectMetadataItemsMock(),
|
||||
);
|
||||
setTestObjectMetadataItemsInMetadataStore(jotaiStore, objectMetadataItems);
|
||||
} else {
|
||||
jotaiStore.set(metadataStoreState.atomFamily('objectMetadataItems'), {
|
||||
current: [],
|
||||
@@ -45,6 +117,12 @@ const renderHooks = ({
|
||||
});
|
||||
}
|
||||
|
||||
jotaiStore.set(metadataStoreState.atomFamily('navigationMenuItems'), {
|
||||
current: navigationMenuItems,
|
||||
draft: [],
|
||||
status: withNavigationMenuItemsLoaded ? 'up-to-date' : 'empty',
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentUser = useSetAtomState(currentUserState);
|
||||
@@ -119,25 +197,98 @@ describe('useDefaultHomePagePath', () => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(AppPath.SignInUp);
|
||||
});
|
||||
});
|
||||
it('should return proper path when currentUser is defined', async () => {
|
||||
it('should redirect to the first object of the navigation menu', async () => {
|
||||
const { result } = renderHooks({
|
||||
withCurrentUser: true,
|
||||
withExistingView: false,
|
||||
navigationMenuItems: [
|
||||
buildObjectNavigationMenuItem('person', 0),
|
||||
buildObjectNavigationMenuItem('company', 1),
|
||||
],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual('/objects/people');
|
||||
});
|
||||
});
|
||||
it('should honor display order over a lower-positioned item nested in a folder', async () => {
|
||||
const { result } = renderHooks({
|
||||
withCurrentUser: true,
|
||||
withExistingView: false,
|
||||
navigationMenuItems: [
|
||||
buildObjectNavigationMenuItem('company', 0, 'folder-1'),
|
||||
buildObjectNavigationMenuItem('person', 1),
|
||||
buildFolderNavigationMenuItem('folder-1', 2),
|
||||
],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual('/objects/people');
|
||||
});
|
||||
});
|
||||
it('should redirect to a PAGE_LAYOUT navigation menu item as homepage', async () => {
|
||||
const { result } = renderHooks({
|
||||
withCurrentUser: true,
|
||||
withExistingView: false,
|
||||
navigationMenuItems: [
|
||||
buildPageLayoutNavigationMenuItem('page-layout-1', 0),
|
||||
buildObjectNavigationMenuItem('person', 1),
|
||||
],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual('/page/page-layout-1');
|
||||
});
|
||||
});
|
||||
it('should skip a LINK navigation menu item and use the next valid item', async () => {
|
||||
const { result } = renderHooks({
|
||||
withCurrentUser: true,
|
||||
withExistingView: false,
|
||||
navigationMenuItems: [
|
||||
buildLinkNavigationMenuItem(0),
|
||||
buildObjectNavigationMenuItem('person', 1),
|
||||
],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual('/objects/people');
|
||||
});
|
||||
});
|
||||
it('should honor the view of a VIEW navigation menu item', async () => {
|
||||
const { result } = renderHooks({
|
||||
withCurrentUser: true,
|
||||
withExistingView: true,
|
||||
navigationMenuItems: [buildViewNavigationMenuItem('viewId', 0)],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(
|
||||
'/objects/companies?viewId=viewId',
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should fall back to the first readable object when the menu has no object item', async () => {
|
||||
const { result } = renderHooks({
|
||||
withCurrentUser: true,
|
||||
withExistingView: false,
|
||||
navigationMenuItems: [],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual('/objects/companies');
|
||||
});
|
||||
});
|
||||
it('should return proper path when currentUser is defined and view exists', async () => {
|
||||
it('should redirect to profile settings when there is no readable object', async () => {
|
||||
const { result } = renderHooks({
|
||||
withCurrentUser: true,
|
||||
withExistingView: true,
|
||||
withExistingView: false,
|
||||
objectMetadataItems: [],
|
||||
navigationMenuItems: [],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(
|
||||
'/objects/companies?viewId=viewId',
|
||||
getSettingsPath(SettingsPath.ProfilePage),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -151,6 +302,18 @@ describe('useDefaultHomePagePath', () => {
|
||||
withObjectMetadataLoaded: false,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(AppPath.Index);
|
||||
});
|
||||
});
|
||||
it('should defer to AppPath.Index when navigation menu items are not loaded yet', async () => {
|
||||
const { result } = renderHooks({
|
||||
withCurrentUser: true,
|
||||
withExistingView: false,
|
||||
navigationMenuItems: [buildObjectNavigationMenuItem('person', 0)],
|
||||
withNavigationMenuItemsLoaded: false,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(AppPath.Index);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { lastVisitedObjectMetadataItemIdState } from '@/navigation/states/lastVisitedObjectMetadataItemIdState';
|
||||
import { metadataStoreStatusFamilySelector } from '@/metadata-store/states/metadataStoreStatusFamilySelector';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { type ObjectPathInfo } from '@/navigation/types/ObjectPathInfo';
|
||||
import { getFirstNavigationMenuItemLink } from '@/navigation/utils/getFirstNavigationMenuItemLink';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { filterReadableActiveObjectMetadataItems } from '@/object-metadata/utils/filterReadableActiveObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
@@ -12,10 +16,8 @@ import isEmpty from 'lodash.isempty';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { AppPath, SettingsPath } from 'twenty-shared/types';
|
||||
import { getAppPath, getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useDefaultHomePagePath = () => {
|
||||
const store = useStore();
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const metadataStore = useAtomFamilyStateValue(
|
||||
@@ -23,8 +25,17 @@ export const useDefaultHomePagePath = () => {
|
||||
'objectMetadataItems',
|
||||
);
|
||||
const areObjectMetadataItemsLoaded = metadataStore.status === 'up-to-date';
|
||||
const navigationMenuItemsStatus = useAtomFamilySelectorValue(
|
||||
metadataStoreStatusFamilySelector,
|
||||
'navigationMenuItems',
|
||||
);
|
||||
const areNavigationMenuItemsLoaded =
|
||||
navigationMenuItemsStatus === 'up-to-date';
|
||||
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const navigationMenuItemsInDisplayOrder = useNavigationMenuItemSectionItems();
|
||||
|
||||
const readableNonSystemObjectMetadataItems = useMemo(
|
||||
() =>
|
||||
@@ -37,17 +48,6 @@ export const useDefaultHomePagePath = () => {
|
||||
[activeObjectMetadataItems, objectPermissionsByObjectMetadataId],
|
||||
);
|
||||
|
||||
const getActiveObjectMetadataItemMatchingId = useCallback(
|
||||
(objectMetadataId: string) => {
|
||||
return readableNonSystemObjectMetadataItems.find(
|
||||
(item) => item.id === objectMetadataId,
|
||||
);
|
||||
},
|
||||
[readableNonSystemObjectMetadataItems],
|
||||
);
|
||||
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const getFirstView = useCallback(
|
||||
(objectMetadataItemId: string | undefined | null) => {
|
||||
return views.find(
|
||||
@@ -57,6 +57,22 @@ export const useDefaultHomePagePath = () => {
|
||||
[views],
|
||||
);
|
||||
|
||||
const firstNavigationMenuItemLink = useMemo(
|
||||
() =>
|
||||
getFirstNavigationMenuItemLink({
|
||||
navigationMenuItemsInDisplayOrder,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
}),
|
||||
[
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
views,
|
||||
navigationMenuItemsInDisplayOrder,
|
||||
],
|
||||
);
|
||||
|
||||
const firstObjectPathInfo = useMemo<ObjectPathInfo | null>(() => {
|
||||
const [firstObjectMetadataItem] = readableNonSystemObjectMetadataItems;
|
||||
|
||||
@@ -64,37 +80,11 @@ export const useDefaultHomePagePath = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const view = getFirstView(firstObjectMetadataItem?.id);
|
||||
const view = getFirstView(firstObjectMetadataItem.id);
|
||||
|
||||
return { objectMetadataItem: firstObjectMetadataItem, view };
|
||||
}, [getFirstView, readableNonSystemObjectMetadataItems]);
|
||||
|
||||
const getDefaultObjectPathInfo = useCallback(() => {
|
||||
const lastVisitedObjectMetadataItemId = store.get(
|
||||
lastVisitedObjectMetadataItemIdState.atom,
|
||||
);
|
||||
|
||||
const lastVisitedObjectMetadataItem = isDefined(
|
||||
lastVisitedObjectMetadataItemId,
|
||||
)
|
||||
? getActiveObjectMetadataItemMatchingId(lastVisitedObjectMetadataItemId)
|
||||
: undefined;
|
||||
|
||||
if (isDefined(lastVisitedObjectMetadataItem)) {
|
||||
return {
|
||||
view: getFirstView(lastVisitedObjectMetadataItemId),
|
||||
objectMetadataItem: lastVisitedObjectMetadataItem,
|
||||
};
|
||||
}
|
||||
|
||||
return firstObjectPathInfo;
|
||||
}, [
|
||||
firstObjectPathInfo,
|
||||
getActiveObjectMetadataItemMatchingId,
|
||||
getFirstView,
|
||||
store,
|
||||
]);
|
||||
|
||||
const defaultHomePagePath = useMemo(() => {
|
||||
if (!isDefined(currentUser)) {
|
||||
return AppPath.SignInUp;
|
||||
@@ -113,25 +103,35 @@ export const useDefaultHomePagePath = () => {
|
||||
return getSettingsPath(SettingsPath.ProfilePage);
|
||||
}
|
||||
|
||||
const defaultObjectPathInfo = getDefaultObjectPathInfo();
|
||||
// The navigation menu drives the redirect and loads after the minimal-
|
||||
// metadata fast path. Wait for it instead of falling back to the
|
||||
// alphabetically-first object during the post-login window.
|
||||
if (!areNavigationMenuItemsLoaded) {
|
||||
return AppPath.Index;
|
||||
}
|
||||
|
||||
if (!isDefined(defaultObjectPathInfo)) {
|
||||
if (isDefined(firstNavigationMenuItemLink)) {
|
||||
return firstNavigationMenuItemLink;
|
||||
}
|
||||
|
||||
if (!isDefined(firstObjectPathInfo)) {
|
||||
return AppPath.NotFound;
|
||||
}
|
||||
|
||||
const namePlural = defaultObjectPathInfo.objectMetadataItem?.namePlural;
|
||||
const viewId = defaultObjectPathInfo.view?.id;
|
||||
|
||||
return getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: namePlural },
|
||||
viewId ? { viewId } : undefined,
|
||||
{ objectNamePlural: firstObjectPathInfo.objectMetadataItem?.namePlural },
|
||||
firstObjectPathInfo.view?.id
|
||||
? { viewId: firstObjectPathInfo.view.id }
|
||||
: undefined,
|
||||
);
|
||||
}, [
|
||||
currentUser,
|
||||
getDefaultObjectPathInfo,
|
||||
readableNonSystemObjectMetadataItems,
|
||||
areObjectMetadataItemsLoaded,
|
||||
areNavigationMenuItemsLoaded,
|
||||
firstNavigationMenuItemLink,
|
||||
firstObjectPathInfo,
|
||||
]);
|
||||
|
||||
return { defaultHomePagePath };
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
type GetFirstNavigationMenuItemLinkArgs = {
|
||||
navigationMenuItemsInDisplayOrder: NavigationMenuItem[];
|
||||
objectMetadataItems: EnrichedObjectMetadataItem[];
|
||||
views: Pick<View, 'id' | 'objectMetadataId' | 'key'>[];
|
||||
objectPermissionsByObjectMetadataId: Parameters<
|
||||
typeof getObjectPermissionsForObject
|
||||
>[0];
|
||||
};
|
||||
|
||||
const OBJECT_BACKED_NAVIGATION_MENU_ITEM_TYPES = [
|
||||
NavigationMenuItemType.OBJECT,
|
||||
NavigationMenuItemType.VIEW,
|
||||
NavigationMenuItemType.RECORD,
|
||||
];
|
||||
|
||||
export const getFirstNavigationMenuItemLink = ({
|
||||
navigationMenuItemsInDisplayOrder,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
}: GetFirstNavigationMenuItemLinkArgs): string | null => {
|
||||
for (const item of navigationMenuItemsInDisplayOrder) {
|
||||
if (
|
||||
item.type === NavigationMenuItemType.FOLDER ||
|
||||
item.type === NavigationMenuItemType.LINK
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (OBJECT_BACKED_NAVIGATION_MENU_ITEM_TYPES.includes(item.type)) {
|
||||
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(objectMetadataItem) ||
|
||||
objectMetadataItem.isSystem ||
|
||||
!getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataItem.id,
|
||||
).canReadObjectRecords
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const link = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
if (isNonEmptyString(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
Reference in New Issue
Block a user