Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fea82c9fbb |
@@ -3,6 +3,7 @@ import { ONBOARDING_PATHS } from '@/auth/constants/OnboardingPaths';
|
||||
import { ONGOING_USER_CREATION_PATHS } from '@/auth/constants/OngoingUserCreationPaths';
|
||||
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
|
||||
import { returnToPathState } from '@/auth/states/returnToPathState';
|
||||
import { isValidReturnToPath } from '@/auth/utils/isValidReturnToPath';
|
||||
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
|
||||
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
|
||||
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
|
||||
@@ -10,7 +11,6 @@ import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMeta
|
||||
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { isValidReturnToPath } from '@/auth/utils/isValidReturnToPath';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { AppPath, SettingsPath } from 'twenty-shared/types';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { safeRemoveLocalStorageItems } from '@/auth/utils/safeRemoveLocalStorageItems';
|
||||
|
||||
const SESSION_KEYS_TO_CLEAR = [
|
||||
'lastVisitedObjectMetadataItemIdState',
|
||||
'lastVisitedViewPerObjectMetadataItemState',
|
||||
'playgroundApiKeyState',
|
||||
'ai/agentChatDraftsByThreadIdState',
|
||||
|
||||
-9
@@ -3,7 +3,6 @@ import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { getViewType } from '@/context-store/utils/getViewType';
|
||||
import { useSetLastVisitedObjectMetadataId } from '@/navigation/hooks/useSetLastVisitedObjectMetadataId';
|
||||
import { useSetLastVisitedViewForObjectMetadataNamePlural } from '@/navigation/hooks/useSetLastVisitedViewForObjectMetadataNamePlural';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
@@ -29,9 +28,6 @@ export const MainContextStoreProviderEffect = ({
|
||||
const { setLastVisitedViewForObjectMetadataNamePlural } =
|
||||
useSetLastVisitedViewForObjectMetadataNamePlural();
|
||||
|
||||
const { setLastVisitedObjectMetadataId } =
|
||||
useSetLastVisitedObjectMetadataId();
|
||||
|
||||
const [contextStoreCurrentViewId, setContextStoreCurrentViewId] =
|
||||
useAtomComponentState(
|
||||
contextStoreCurrentViewIdComponentState,
|
||||
@@ -69,15 +65,10 @@ export const MainContextStoreProviderEffect = ({
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
viewId: viewId ?? '',
|
||||
});
|
||||
|
||||
setLastVisitedObjectMetadataId({
|
||||
objectMetadataItemId: objectMetadataItem.id,
|
||||
});
|
||||
}, [
|
||||
contextStoreCurrentObjectMetadataItemId,
|
||||
objectMetadataItem,
|
||||
setContextStoreCurrentObjectMetadataItemId,
|
||||
setLastVisitedObjectMetadataId,
|
||||
setLastVisitedViewForObjectMetadataNamePlural,
|
||||
viewId,
|
||||
]);
|
||||
|
||||
+12
-1
@@ -23,6 +23,12 @@ export const IsMinimalMetadataReadyEffect = () => {
|
||||
metadataStoreState,
|
||||
'views',
|
||||
);
|
||||
// oxlint-disable-next-line twenty/matching-state-variable
|
||||
const metadataStoreNavigationMenuItems = useAtomFamilyStateValue(
|
||||
metadataStoreState,
|
||||
'navigationMenuItems',
|
||||
);
|
||||
|
||||
const setIsMinimalMetadataReady = useSetAtomState(
|
||||
isMinimalMetadataReadyState,
|
||||
);
|
||||
@@ -32,11 +38,15 @@ export const IsMinimalMetadataReadyEffect = () => {
|
||||
|
||||
const areObjectsLoaded = metadataStore.status === 'up-to-date';
|
||||
const areViewsLoaded = metadataStoreViews.status === 'up-to-date';
|
||||
const areNavigationMenuItemsLoaded =
|
||||
metadataStoreNavigationMenuItems.status === 'up-to-date';
|
||||
|
||||
const isReady = !areObjectsLoaded
|
||||
? false
|
||||
: !hasAccessTokenPair ||
|
||||
(isDefined(currentUser) && (!hasActiveWorkspace || areViewsLoaded));
|
||||
(isDefined(currentUser) &&
|
||||
(!hasActiveWorkspace ||
|
||||
(areViewsLoaded && areNavigationMenuItemsLoaded)));
|
||||
|
||||
if (!areObjectsLoaded) {
|
||||
setIsMinimalMetadataReady(false);
|
||||
@@ -52,6 +62,7 @@ export const IsMinimalMetadataReadyEffect = () => {
|
||||
currentWorkspace,
|
||||
metadataStore.status,
|
||||
metadataStoreViews.status,
|
||||
metadataStoreNavigationMenuItems.status,
|
||||
setIsMinimalMetadataReady,
|
||||
]);
|
||||
|
||||
|
||||
@@ -9,11 +9,12 @@ import { useOpenRecordsSearchPageInSidePanel } from '@/side-panel/hooks/useOpenR
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type IconComponent,
|
||||
IconList,
|
||||
@@ -67,7 +68,7 @@ export const MobileNavigationBar = () => {
|
||||
);
|
||||
setCurrentMobileNavigationDrawer('main');
|
||||
|
||||
if (isSettingsPage) {
|
||||
if (isSettingsPage && isDefined(defaultHomePagePath)) {
|
||||
navigate(defaultHomePagePath);
|
||||
}
|
||||
},
|
||||
|
||||
+242
-61
@@ -1,5 +1,7 @@
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
@@ -7,8 +9,11 @@ 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, NavigationMenuItemType, SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
type NavigationMenuItem,
|
||||
ViewKey,
|
||||
ViewOpenRecordIn,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
@@ -16,24 +21,65 @@ import {
|
||||
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 { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
|
||||
import { setTestViewsInMetadataStore } from '~/testing/utils/setTestViewsInMetadataStore';
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) =>
|
||||
createElement(JotaiProvider, { store: jotaiStore }, children);
|
||||
|
||||
const renderHooks = ({
|
||||
const setNavigationMenuItemsInStore = (items: NavigationMenuItem[]) => {
|
||||
jotaiStore.set(metadataStoreState.atomFamily('navigationMenuItems'), {
|
||||
current: items,
|
||||
draft: [],
|
||||
status: 'up-to-date',
|
||||
});
|
||||
};
|
||||
|
||||
const makeObjectNavItem = (
|
||||
overrides: Partial<NavigationMenuItem> & {
|
||||
id: string;
|
||||
targetObjectMetadataId: string;
|
||||
position: number;
|
||||
},
|
||||
): NavigationMenuItem => ({
|
||||
type: NavigationMenuItemType.OBJECT as unknown as NavigationMenuItem['type'],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeViewNavItem = (
|
||||
overrides: Partial<NavigationMenuItem> & {
|
||||
id: string;
|
||||
viewId: string;
|
||||
position: number;
|
||||
},
|
||||
): NavigationMenuItem => ({
|
||||
type: NavigationMenuItemType.VIEW as unknown as NavigationMenuItem['type'],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const companyMetadata = getMockObjectMetadataItemOrThrow('company');
|
||||
const personMetadata = getMockObjectMetadataItemOrThrow('person');
|
||||
|
||||
const setupStore = ({
|
||||
withCurrentUser,
|
||||
withExistingView,
|
||||
views,
|
||||
navigationMenuItems,
|
||||
}: {
|
||||
withCurrentUser: boolean;
|
||||
withExistingView: boolean;
|
||||
views: Parameters<typeof setTestViewsInMetadataStore>[1];
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
}) => {
|
||||
setTestObjectMetadataItemsInMetadataStore(
|
||||
jotaiStore,
|
||||
getTestEnrichedObjectMetadataItemsMock(),
|
||||
);
|
||||
|
||||
jotaiStore.set(isMinimalMetadataReadyState.atom, true);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentUser = useSetAtomState(currentUserState);
|
||||
@@ -42,34 +88,8 @@ const renderHooks = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (withExistingView) {
|
||||
setTestViewsInMetadataStore(jotaiStore, [
|
||||
{
|
||||
id: 'viewId',
|
||||
name: 'Test View',
|
||||
objectMetadataId: getMockObjectMetadataItemOrThrow('company').id,
|
||||
type: ViewType.TABLE,
|
||||
key: null,
|
||||
isCompact: false,
|
||||
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
|
||||
viewFields: [],
|
||||
viewFieldGroups: [],
|
||||
viewGroups: [],
|
||||
viewSorts: [],
|
||||
viewFilters: [],
|
||||
viewFilterGroups: [],
|
||||
kanbanAggregateOperation: AggregateOperations.COUNT,
|
||||
icon: '',
|
||||
kanbanAggregateOperationFieldMetadataId: '',
|
||||
position: 0,
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
createdByUserWorkspaceId: null,
|
||||
shouldHideEmptyGroups: false,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setTestViewsInMetadataStore(jotaiStore, []);
|
||||
}
|
||||
setTestViewsInMetadataStore(jotaiStore, views);
|
||||
setNavigationMenuItemsInStore(navigationMenuItems);
|
||||
|
||||
if (withCurrentUser) {
|
||||
setCurrentUser(mockedUserData);
|
||||
@@ -79,53 +99,214 @@ const renderHooks = ({
|
||||
|
||||
return useDefaultHomePagePath();
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
return { result };
|
||||
};
|
||||
|
||||
const makeView = (
|
||||
id: string,
|
||||
objectMetadataId: string,
|
||||
overrides?: { key?: ViewKey | null },
|
||||
) => ({
|
||||
id,
|
||||
name: 'Test View',
|
||||
objectMetadataId,
|
||||
type: ViewType.TABLE,
|
||||
key: overrides?.key ?? null,
|
||||
isCompact: false,
|
||||
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
|
||||
viewFields: [],
|
||||
viewFieldGroups: [],
|
||||
viewGroups: [],
|
||||
viewSorts: [],
|
||||
viewFilters: [],
|
||||
viewFilterGroups: [],
|
||||
kanbanAggregateOperation: AggregateOperations.COUNT,
|
||||
icon: '',
|
||||
kanbanAggregateOperationFieldMetadataId: '',
|
||||
position: 0,
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
createdByUserWorkspaceId: null,
|
||||
shouldHideEmptyGroups: false,
|
||||
});
|
||||
|
||||
describe('useDefaultHomePagePath', () => {
|
||||
it('should return proper path when no currentUser', async () => {
|
||||
const { result } = renderHooks({
|
||||
it('should return SignInUp when no currentUser', async () => {
|
||||
const { result } = setupStore({
|
||||
withCurrentUser: false,
|
||||
withExistingView: false,
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(AppPath.SignInUp);
|
||||
});
|
||||
});
|
||||
it('should return proper path when no currentUser and existing view', async () => {
|
||||
const { result } = renderHooks({
|
||||
withCurrentUser: false,
|
||||
withExistingView: true,
|
||||
|
||||
it('should return first OBJECT nav item path', async () => {
|
||||
const personView = makeView('personViewId', personMetadata.id, {
|
||||
key: ViewKey.INDEX,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(AppPath.SignInUp);
|
||||
});
|
||||
});
|
||||
it('should return proper path when currentUser is defined', async () => {
|
||||
const { result } = renderHooks({
|
||||
const { result } = setupStore({
|
||||
withCurrentUser: true,
|
||||
withExistingView: false,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual('/objects/companies');
|
||||
});
|
||||
});
|
||||
it('should return proper path when currentUser is defined and view exists', async () => {
|
||||
const { result } = renderHooks({
|
||||
withCurrentUser: true,
|
||||
withExistingView: true,
|
||||
views: [personView],
|
||||
navigationMenuItems: [
|
||||
makeObjectNavItem({
|
||||
id: 'nav-person',
|
||||
targetObjectMetadataId: personMetadata.id,
|
||||
position: 0,
|
||||
}),
|
||||
makeObjectNavItem({
|
||||
id: 'nav-company',
|
||||
targetObjectMetadataId: companyMetadata.id,
|
||||
position: 1,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(
|
||||
'/objects/companies?viewId=viewId',
|
||||
'/objects/people?viewId=personViewId',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return first VIEW nav item path', async () => {
|
||||
const companyView = makeView('companyViewId', companyMetadata.id);
|
||||
|
||||
const { result } = setupStore({
|
||||
withCurrentUser: true,
|
||||
views: [companyView],
|
||||
navigationMenuItems: [
|
||||
makeViewNavItem({
|
||||
id: 'nav-view',
|
||||
viewId: 'companyViewId',
|
||||
position: 0,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(
|
||||
'/objects/companies?viewId=companyViewId',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect position ordering', async () => {
|
||||
const companyView = makeView('companyViewId', companyMetadata.id, {
|
||||
key: ViewKey.INDEX,
|
||||
});
|
||||
const personView = makeView('personViewId', personMetadata.id, {
|
||||
key: ViewKey.INDEX,
|
||||
});
|
||||
|
||||
const { result } = setupStore({
|
||||
withCurrentUser: true,
|
||||
views: [companyView, personView],
|
||||
navigationMenuItems: [
|
||||
makeObjectNavItem({
|
||||
id: 'nav-company',
|
||||
targetObjectMetadataId: companyMetadata.id,
|
||||
position: 5,
|
||||
}),
|
||||
makeObjectNavItem({
|
||||
id: 'nav-person',
|
||||
targetObjectMetadataId: personMetadata.id,
|
||||
position: 1,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(
|
||||
'/objects/people?viewId=personViewId',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Settings profile page when no navigable items', async () => {
|
||||
const { result } = setupStore({
|
||||
withCurrentUser: true,
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(
|
||||
getSettingsPath(SettingsPath.ProfilePage),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when metadata is not yet ready', async () => {
|
||||
setTestObjectMetadataItemsInMetadataStore(
|
||||
jotaiStore,
|
||||
getTestEnrichedObjectMetadataItemsMock(),
|
||||
);
|
||||
|
||||
jotaiStore.set(isMinimalMetadataReadyState.atom, false);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentUser = useSetAtomState(currentUserState);
|
||||
const setCurrentUserWorkspace = useSetAtomState(
|
||||
currentUserWorkspaceState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentUser(mockedUserData);
|
||||
setCurrentUserWorkspace(mockedUserData.currentUserWorkspace);
|
||||
}, [setCurrentUser, setCurrentUserWorkspace]);
|
||||
|
||||
return useDefaultHomePagePath();
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip FOLDER and LINK items', async () => {
|
||||
const personView = makeView('personViewId', personMetadata.id, {
|
||||
key: ViewKey.INDEX,
|
||||
});
|
||||
|
||||
const { result } = setupStore({
|
||||
withCurrentUser: true,
|
||||
views: [personView],
|
||||
navigationMenuItems: [
|
||||
{
|
||||
id: 'nav-folder',
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
position: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as NavigationMenuItem,
|
||||
{
|
||||
id: 'nav-link',
|
||||
type: NavigationMenuItemType.LINK,
|
||||
position: 1,
|
||||
link: 'https://example.com',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as NavigationMenuItem,
|
||||
makeObjectNavItem({
|
||||
id: 'nav-person',
|
||||
targetObjectMetadataId: personMetadata.id,
|
||||
position: 2,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.defaultHomePagePath).toEqual(
|
||||
'/objects/people?viewId=personViewId',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,91 +1,84 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
AppPath,
|
||||
NavigationMenuItemType,
|
||||
SettingsPath,
|
||||
} from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { lastVisitedObjectMetadataItemIdState } from '@/navigation/states/lastVisitedObjectMetadataItemIdState';
|
||||
import { type ObjectPathInfo } from '@/navigation/types/ObjectPathInfo';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { filterReadableActiveObjectMetadataItems } from '@/object-metadata/utils/filterReadableActiveObjectMetadataItems';
|
||||
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
|
||||
import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
|
||||
import { filterAndSortNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterAndSortNavigationMenuItems';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
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 { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
|
||||
const readableNonSystemObjectMetadataItems = useMemo(
|
||||
() =>
|
||||
filterReadableActiveObjectMetadataItems(
|
||||
activeObjectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
)
|
||||
.filter((item) => !item.isSystem)
|
||||
.sort((a, b) => a.nameSingular.localeCompare(b.nameSingular)),
|
||||
[activeObjectMetadataItems, objectPermissionsByObjectMetadataId],
|
||||
);
|
||||
|
||||
const getActiveObjectMetadataItemMatchingId = useCallback(
|
||||
(objectMetadataId: string) => {
|
||||
return readableNonSystemObjectMetadataItems.find(
|
||||
(item) => item.id === objectMetadataId,
|
||||
);
|
||||
},
|
||||
[readableNonSystemObjectMetadataItems],
|
||||
);
|
||||
|
||||
const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const isMinimalMetadataReady = useAtomStateValue(isMinimalMetadataReadyState);
|
||||
|
||||
const getFirstView = useCallback(
|
||||
(objectMetadataItemId: string | undefined | null) => {
|
||||
return views.find(
|
||||
(view) => view.objectMetadataId === objectMetadataItemId,
|
||||
);
|
||||
},
|
||||
[views],
|
||||
);
|
||||
const firstNavigableLink = useMemo(() => {
|
||||
const workspaceItems =
|
||||
filterWorkspaceNavigationMenuItems(navigationMenuItems);
|
||||
|
||||
const firstObjectPathInfo = useMemo<ObjectPathInfo | null>(() => {
|
||||
const [firstObjectMetadataItem] = readableNonSystemObjectMetadataItems;
|
||||
|
||||
if (!isDefined(firstObjectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const view = getFirstView(firstObjectMetadataItem?.id);
|
||||
|
||||
return { objectMetadataItem: firstObjectMetadataItem, view };
|
||||
}, [getFirstView, readableNonSystemObjectMetadataItems]);
|
||||
|
||||
const getDefaultObjectPathInfo = useCallback(() => {
|
||||
const lastVisitedObjectMetadataItemId = store.get(
|
||||
lastVisitedObjectMetadataItemIdState.atom,
|
||||
const sortedItems = filterAndSortNavigationMenuItems(
|
||||
workspaceItems,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
);
|
||||
|
||||
const lastVisitedObjectMetadataItem = isDefined(
|
||||
lastVisitedObjectMetadataItemId,
|
||||
)
|
||||
? getActiveObjectMetadataItemMatchingId(lastVisitedObjectMetadataItemId)
|
||||
: undefined;
|
||||
for (const item of sortedItems) {
|
||||
if (
|
||||
item.type !== NavigationMenuItemType.OBJECT &&
|
||||
item.type !== NavigationMenuItemType.VIEW
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDefined(lastVisitedObjectMetadataItem)) {
|
||||
return {
|
||||
view: getFirstView(lastVisitedObjectMetadataItemId),
|
||||
objectMetadataItem: lastVisitedObjectMetadataItem,
|
||||
};
|
||||
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(objectMetadataItem) ||
|
||||
!getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataItem.id,
|
||||
).canReadObjectRecords
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const link = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
if (isNonEmptyString(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
return firstObjectPathInfo;
|
||||
return null;
|
||||
}, [
|
||||
firstObjectPathInfo,
|
||||
getActiveObjectMetadataItemMatchingId,
|
||||
getFirstView,
|
||||
store,
|
||||
navigationMenuItems,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
views,
|
||||
]);
|
||||
|
||||
const defaultHomePagePath = useMemo(() => {
|
||||
@@ -93,29 +86,16 @@ export const useDefaultHomePagePath = () => {
|
||||
return AppPath.SignInUp;
|
||||
}
|
||||
|
||||
if (isEmpty(readableNonSystemObjectMetadataItems)) {
|
||||
return getSettingsPath(SettingsPath.ProfilePage);
|
||||
if (isNonEmptyString(firstNavigableLink)) {
|
||||
return firstNavigableLink;
|
||||
}
|
||||
|
||||
const defaultObjectPathInfo = getDefaultObjectPathInfo();
|
||||
|
||||
if (!isDefined(defaultObjectPathInfo)) {
|
||||
return AppPath.NotFound;
|
||||
if (!isMinimalMetadataReady) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const namePlural = defaultObjectPathInfo.objectMetadataItem?.namePlural;
|
||||
const viewId = defaultObjectPathInfo.view?.id;
|
||||
|
||||
return getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: namePlural },
|
||||
viewId ? { viewId } : undefined,
|
||||
);
|
||||
}, [
|
||||
currentUser,
|
||||
getDefaultObjectPathInfo,
|
||||
readableNonSystemObjectMetadataItems,
|
||||
]);
|
||||
return getSettingsPath(SettingsPath.ProfilePage);
|
||||
}, [currentUser, firstNavigableLink, isMinimalMetadataReady]);
|
||||
|
||||
return { defaultHomePagePath };
|
||||
};
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { lastVisitedObjectMetadataItemIdState } from '@/navigation/states/lastVisitedObjectMetadataItemIdState';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useSetLastVisitedObjectMetadataId = () => {
|
||||
const store = useStore();
|
||||
const setLastVisitedObjectMetadataId = useCallback(
|
||||
({ objectMetadataItemId }: { objectMetadataItemId: string }) => {
|
||||
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === objectMetadataItemId,
|
||||
);
|
||||
|
||||
const lastVisitedObjectMetadataItemId = store.get(
|
||||
lastVisitedObjectMetadataItemIdState.atom,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(objectMetadataItem) &&
|
||||
lastVisitedObjectMetadataItemId !== objectMetadataItemId
|
||||
) {
|
||||
store.set(
|
||||
lastVisitedObjectMetadataItemIdState.atom,
|
||||
objectMetadataItemId,
|
||||
);
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return {
|
||||
setLastVisitedObjectMetadataId,
|
||||
};
|
||||
};
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const lastVisitedObjectMetadataItemIdState = createAtomState<
|
||||
string | null
|
||||
>({
|
||||
key: 'lastVisitedObjectMetadataItemIdState',
|
||||
defaultValue: null,
|
||||
useLocalStorage: true,
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type View } from '@/views/types/View';
|
||||
|
||||
export type ObjectPathInfo = {
|
||||
objectMetadataItem: EnrichedObjectMetadataItem;
|
||||
view: View | undefined;
|
||||
};
|
||||
Reference in New Issue
Block a user