Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7bc3542e4 | ||
|
|
8e53ba0f4a | ||
|
|
00bad2cb99 | ||
|
|
6e05bbdf12 | ||
|
|
65db4d8d9a | ||
|
|
6af013b1dd | ||
|
|
74232f718e | ||
|
|
6dbec4a7e2 | ||
|
|
9f55948785 | ||
|
|
ff7cf245c4 | ||
|
|
aae3f4301b | ||
|
|
a3d7ef76cb | ||
|
|
af66b6acc8 | ||
|
|
25ee116332 | ||
|
|
1b2e4899a2 | ||
|
|
ba12a26541 | ||
|
|
918fdcbf87 |
+8
@@ -0,0 +1,8 @@
|
||||
import { type ActiveNavigationItem } from '@/navigation-menu-item/common/types/ActiveNavigationItem';
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const activeNavigationItemState =
|
||||
createAtomState<ActiveNavigationItem | null>({
|
||||
key: 'activeNavigationItemState',
|
||||
defaultValue: null,
|
||||
});
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export type ActiveNavigationItem = {
|
||||
navItemId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
describe('isLocationMatchingNavigationMenuItem', () => {
|
||||
it('should return true when item link matches current path (non-view) or current view path (view)', () => {
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
|
||||
export const matchesRecordShowPathForObject = (
|
||||
pathname: string,
|
||||
objectNameSingular: string,
|
||||
): boolean => {
|
||||
const recordShowBase = getAppPath(AppPath.RecordShowPage, {
|
||||
objectNameSingular,
|
||||
objectRecordId: '',
|
||||
});
|
||||
|
||||
return pathname.startsWith(`${recordShowBase}/`);
|
||||
};
|
||||
+14
@@ -3,6 +3,7 @@ import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { activeNavigationItemState } from '@/navigation-menu-item/common/states/activeNavigationItemState';
|
||||
import { getNavigationMenuItemColor } from '@/navigation-menu-item/common/utils/getNavigationMenuItemColor';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
|
||||
import type { EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
@@ -16,6 +17,7 @@ import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMeta
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
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';
|
||||
|
||||
type NavigationMenuItemFolderSubItemProps = {
|
||||
@@ -79,6 +81,17 @@ export const NavigationMenuItemFolderSubItem = ({
|
||||
(navigationMenuItem.type === NavigationMenuItemType.LINK ||
|
||||
isDefined(objectMetadataItem));
|
||||
|
||||
const setActiveNavigationItem = useSetAtomState(activeNavigationItemState);
|
||||
|
||||
const handleBeforeNavigation = () => {
|
||||
if (isDefined(objectNameSingular)) {
|
||||
setActiveNavigationItem({
|
||||
navItemId: navigationMenuItem.id,
|
||||
objectNameSingular,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick =
|
||||
onClick ??
|
||||
(isEditable
|
||||
@@ -108,6 +121,7 @@ export const NavigationMenuItemFolderSubItem = ({
|
||||
objectMetadataItem ?? undefined,
|
||||
)}
|
||||
to={isDragging || handleClick ? undefined : computedLink}
|
||||
onBeforeNavigation={handleBeforeNavigation}
|
||||
onClick={handleClick}
|
||||
active={index === selectedNavigationMenuItemIndex}
|
||||
isSelectedInEditMode={isEditHighlightedInNavigationMenu}
|
||||
|
||||
+24
-32
@@ -1,15 +1,16 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { activeNavigationItemState } from '@/navigation-menu-item/common/states/activeNavigationItemState';
|
||||
import { currentNavigationMenuItemFolderIdState } from '@/navigation-menu-item/common/states/currentNavigationMenuItemFolderIdState';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
|
||||
import { resolveFolderSelectedNavigationMenuItemIndex } from '@/navigation-menu-item/display/folder/utils/resolveFolderSelectedNavigationMenuItemIndex';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
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';
|
||||
@@ -31,6 +32,7 @@ export const useNavigationMenuItemFolderOpenState = ({
|
||||
const isMobile = useIsMobile();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsData();
|
||||
|
||||
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
|
||||
useAtomState(openNavigationMenuItemFolderIdsState);
|
||||
@@ -54,45 +56,35 @@ export const useNavigationMenuItemFolderOpenState = ({
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
const firstNonLinkItem = navigationMenuItems.find((item) => {
|
||||
for (const item of navigationMenuItems) {
|
||||
if (item.type === NavigationMenuItemType.LINK) {
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return isNonEmptyString(computedLink);
|
||||
});
|
||||
if (isDefined(firstNonLinkItem)) {
|
||||
const link = getNavigationMenuItemComputedLink(
|
||||
firstNonLinkItem,
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
if (isNonEmptyString(link)) {
|
||||
navigate(link);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex(
|
||||
(item) => {
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
item.type,
|
||||
computedLink,
|
||||
);
|
||||
},
|
||||
);
|
||||
const activeNavigationItem = useAtomStateValue(activeNavigationItemState);
|
||||
|
||||
const selectedNavigationMenuItemIndex =
|
||||
resolveFolderSelectedNavigationMenuItemIndex({
|
||||
navigationMenuItems,
|
||||
activeNavigationItem,
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
workspaceNavigationMenuItems,
|
||||
});
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem';
|
||||
import { matchesRecordShowPathForObject } from '@/navigation-menu-item/common/utils/matchesRecordShowPathForObject';
|
||||
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 { type View } from '@/views/types/View';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
type DoesFolderNavigationMenuItemMatchUrlForSelectionParams = {
|
||||
folderChildNavigationMenuItem: NavigationMenuItem;
|
||||
workspaceNavigationMenuItems: NavigationMenuItem[];
|
||||
currentPath: string;
|
||||
currentViewPath: string;
|
||||
objectMetadataItems: EnrichedObjectMetadataItem[];
|
||||
views: Pick<View, 'id' | 'objectMetadataId'>[];
|
||||
};
|
||||
|
||||
export const doesFolderNavigationMenuItemMatchUrlForSelection = ({
|
||||
folderChildNavigationMenuItem,
|
||||
workspaceNavigationMenuItems,
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
}: DoesFolderNavigationMenuItemMatchUrlForSelectionParams): boolean => {
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
folderChildNavigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
const matchesByLink = isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
folderChildNavigationMenuItem.type,
|
||||
computedLink,
|
||||
);
|
||||
|
||||
const objectMetadataForFolderChild = getObjectMetadataForNavigationMenuItem(
|
||||
folderChildNavigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
const matchesByRecordShow =
|
||||
!matchesByLink &&
|
||||
isDefined(objectMetadataForFolderChild) &&
|
||||
matchesRecordShowPathForObject(
|
||||
currentPath,
|
||||
objectMetadataForFolderChild.nameSingular,
|
||||
);
|
||||
|
||||
if (!matchesByLink && !matchesByRecordShow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isDefined(objectMetadataForFolderChild)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !workspaceNavigationMenuItems.some((workspaceItem) => {
|
||||
if (
|
||||
isDefined(workspaceItem.folderId) ||
|
||||
workspaceItem.type !== NavigationMenuItemType.OBJECT
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const topLevelComputedLink = getNavigationMenuItemComputedLink(
|
||||
workspaceItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
if (
|
||||
!isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
workspaceItem.type,
|
||||
topLevelComputedLink,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const objectMetadataForTopLevelItem =
|
||||
getObjectMetadataForNavigationMenuItem(
|
||||
workspaceItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
return (
|
||||
isDefined(objectMetadataForTopLevelItem) &&
|
||||
objectMetadataForTopLevelItem.id === objectMetadataForFolderChild.id
|
||||
);
|
||||
});
|
||||
};
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type ActiveNavigationItem } from '@/navigation-menu-item/common/types/ActiveNavigationItem';
|
||||
import { doesFolderNavigationMenuItemMatchUrlForSelection } from '@/navigation-menu-item/display/folder/utils/doesFolderNavigationMenuItemMatchUrlForSelection';
|
||||
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 { type View } from '@/views/types/View';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
type ResolveFolderSelectedNavigationMenuItemIndexParams = {
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
activeNavigationItem: ActiveNavigationItem | null;
|
||||
currentPath: string;
|
||||
currentViewPath: string;
|
||||
objectMetadataItems: EnrichedObjectMetadataItem[];
|
||||
views: Pick<View, 'id' | 'objectMetadataId'>[];
|
||||
workspaceNavigationMenuItems: NavigationMenuItem[];
|
||||
};
|
||||
|
||||
export const resolveFolderSelectedNavigationMenuItemIndex = ({
|
||||
navigationMenuItems,
|
||||
activeNavigationItem,
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
workspaceNavigationMenuItems,
|
||||
}: ResolveFolderSelectedNavigationMenuItemIndexParams): number => {
|
||||
const explicitMatchIndex = isDefined(activeNavigationItem)
|
||||
? navigationMenuItems.findIndex(
|
||||
(item) => item.id === activeNavigationItem.navItemId,
|
||||
)
|
||||
: -1;
|
||||
|
||||
const activeNavigationObjectNameSingular =
|
||||
activeNavigationItem?.objectNameSingular;
|
||||
|
||||
const isActiveNavigationItemObjectInFolder =
|
||||
isDefined(activeNavigationObjectNameSingular) &&
|
||||
navigationMenuItems.some((navigationMenuItem) => {
|
||||
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
objectMetadataItem.nameSingular === activeNavigationObjectNameSingular
|
||||
);
|
||||
});
|
||||
|
||||
const recordMatchIndex = navigationMenuItems.findIndex((item) => {
|
||||
if (item.type !== NavigationMenuItemType.RECORD) {
|
||||
return false;
|
||||
}
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return computedLink === currentPath;
|
||||
});
|
||||
|
||||
const urlMatchIndex = navigationMenuItems.findIndex((folderChildItem) =>
|
||||
doesFolderNavigationMenuItemMatchUrlForSelection({
|
||||
folderChildNavigationMenuItem: folderChildItem,
|
||||
workspaceNavigationMenuItems,
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
}),
|
||||
);
|
||||
|
||||
let selectedIndex: number;
|
||||
|
||||
if (!isActiveNavigationItemObjectInFolder) {
|
||||
selectedIndex = urlMatchIndex;
|
||||
} else if (explicitMatchIndex !== -1) {
|
||||
selectedIndex = explicitMatchIndex;
|
||||
} else if (recordMatchIndex !== -1) {
|
||||
selectedIndex = recordMatchIndex;
|
||||
} else {
|
||||
selectedIndex = urlMatchIndex;
|
||||
}
|
||||
|
||||
return selectedIndex;
|
||||
};
|
||||
+17
-4
@@ -1,21 +1,34 @@
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
|
||||
|
||||
export const useWorkspaceNavigationMenuItems = (): {
|
||||
objectMetadataIdsInWorkspaceNav: Set<string>;
|
||||
objectMetadataIdsWithObjectWorkspaceItem: Set<string>;
|
||||
} => {
|
||||
const { workspaceNavigationMenuItems: rawWorkspaceNavigationMenuItems } =
|
||||
useNavigationMenuItemsData();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const objectMetadataIdsInWorkspaceNav = new Set(
|
||||
const objectMetadataIdsWithObjectWorkspaceItem = new Set(
|
||||
rawWorkspaceNavigationMenuItems
|
||||
.filter((item) => item.type === NavigationMenuItemType.OBJECT)
|
||||
.map((item) => item.targetObjectMetadataId)
|
||||
.map(
|
||||
(item) =>
|
||||
getObjectMetadataForNavigationMenuItem(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
)?.id,
|
||||
)
|
||||
.filter((objectMetadataId) => isDefined(objectMetadataId)),
|
||||
);
|
||||
|
||||
return {
|
||||
objectMetadataIdsInWorkspaceNav,
|
||||
objectMetadataIdsWithObjectWorkspaceItem,
|
||||
};
|
||||
};
|
||||
|
||||
+24
-15
@@ -3,7 +3,9 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Fragment, type ReactNode, useContext } from 'react';
|
||||
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { activeNavigationItemState } from '@/navigation-menu-item/common/states/activeNavigationItemState';
|
||||
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/common/utils/recordIdentifierToObjectRecordIdentifier';
|
||||
import { isNavigationItemActive } from '@/navigation-menu-item/display/object/utils/isNavigationItemActive';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
|
||||
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/display/view/components/ObjectIconWithViewOverlay';
|
||||
@@ -15,6 +17,7 @@ import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObject
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
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 { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
@@ -68,8 +71,6 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
const { getIcon } = useIcons();
|
||||
const objectNavItemColor = getObjectColorWithFallback(objectMetadataItem);
|
||||
const location = useLocation();
|
||||
const currentPath = location.pathname;
|
||||
const currentPathWithSearch = `${location.pathname}${location.search}`;
|
||||
|
||||
const isRecord = navigationMenuItem?.type === NavigationMenuItemType.RECORD;
|
||||
const isView = navigationMenuItem?.type === NavigationMenuItemType.VIEW;
|
||||
@@ -90,19 +91,26 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
|
||||
const computedLink = hasCustomLink ? navigationPath : '';
|
||||
|
||||
const isActive = hasCustomLink
|
||||
? (isView || isObject ? currentPathWithSearch : currentPath) ===
|
||||
computedLink
|
||||
: currentPath ===
|
||||
getAppPath(AppPath.RecordIndexPage, {
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
}) ||
|
||||
currentPath.includes(
|
||||
getAppPath(AppPath.RecordShowPage, {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectRecordId: '',
|
||||
}) + '/',
|
||||
);
|
||||
const activeNavigationItem = useAtomStateValue(activeNavigationItemState);
|
||||
const setActiveNavigationItem = useSetAtomState(activeNavigationItemState);
|
||||
|
||||
const isActive = isNavigationItemActive({
|
||||
navigationMenuItem,
|
||||
activeNavigationItem,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
computedLink,
|
||||
currentLocation: location,
|
||||
});
|
||||
|
||||
const handleBeforeNavigation = () => {
|
||||
if (isDefined(navigationMenuItem?.id)) {
|
||||
setActiveNavigationItem({
|
||||
navItemId: navigationMenuItem.id,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = isLayoutCustomizationModeEnabled
|
||||
? onEditModeClick
|
||||
@@ -184,6 +192,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
? navigationPath
|
||||
: undefined
|
||||
}
|
||||
onBeforeNavigation={handleBeforeNavigation}
|
||||
onClick={handleClick}
|
||||
Icon={Icon}
|
||||
iconColor={iconThemeColor}
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { AppPath, NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type ActiveNavigationItem } from '@/navigation-menu-item/common/types/ActiveNavigationItem';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem';
|
||||
import { matchesRecordShowPathForObject } from '@/navigation-menu-item/common/utils/matchesRecordShowPathForObject';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
type IsNavigationItemActiveParams = {
|
||||
navigationMenuItem: Pick<NavigationMenuItem, 'id' | 'type'> | undefined;
|
||||
activeNavigationItem: ActiveNavigationItem | null;
|
||||
objectNameSingular: string;
|
||||
objectNamePlural: string;
|
||||
computedLink: string;
|
||||
currentLocation: { pathname: string; search: string };
|
||||
};
|
||||
|
||||
export const isNavigationItemActive = ({
|
||||
navigationMenuItem,
|
||||
activeNavigationItem,
|
||||
objectNameSingular,
|
||||
objectNamePlural,
|
||||
computedLink,
|
||||
currentLocation,
|
||||
}: IsNavigationItemActiveParams): boolean => {
|
||||
const currentPath = currentLocation.pathname;
|
||||
const currentPathWithSearch = `${currentLocation.pathname}${currentLocation.search}`;
|
||||
|
||||
const navigationMenuItemType = navigationMenuItem?.type;
|
||||
const isRecord = navigationMenuItemType === NavigationMenuItemType.RECORD;
|
||||
const isObject = navigationMenuItemType === NavigationMenuItemType.OBJECT;
|
||||
const hasCustomLink =
|
||||
isRecord ||
|
||||
isObject ||
|
||||
navigationMenuItemType === NavigationMenuItemType.VIEW;
|
||||
|
||||
const isOnObjectRecordShowPage = matchesRecordShowPathForObject(
|
||||
currentPath,
|
||||
objectNameSingular,
|
||||
);
|
||||
|
||||
const isOnObjectIndexPage =
|
||||
currentPath === getAppPath(AppPath.RecordIndexPage, { objectNamePlural });
|
||||
|
||||
const isCurrentPathMatchingObject =
|
||||
isOnObjectIndexPage || isOnObjectRecordShowPage;
|
||||
|
||||
const matchesNavigationMenuItemLink =
|
||||
hasCustomLink &&
|
||||
isDefined(navigationMenuItemType) &&
|
||||
isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentPathWithSearch,
|
||||
navigationMenuItemType,
|
||||
computedLink,
|
||||
);
|
||||
|
||||
const isActiveByUrl =
|
||||
matchesNavigationMenuItemLink ||
|
||||
(isObject && isOnObjectRecordShowPage) ||
|
||||
(!hasCustomLink && isCurrentPathMatchingObject);
|
||||
|
||||
const shouldUseExplicitActiveItem =
|
||||
isDefined(activeNavigationItem) &&
|
||||
activeNavigationItem.objectNameSingular === objectNameSingular &&
|
||||
isCurrentPathMatchingObject;
|
||||
|
||||
if (!shouldUseExplicitActiveItem || !isDefined(navigationMenuItem)) {
|
||||
return isActiveByUrl;
|
||||
}
|
||||
|
||||
const isRecordMatchingCurrentPage = isRecord && computedLink === currentPath;
|
||||
|
||||
return (
|
||||
activeNavigationItem.navItemId === navigationMenuItem.id ||
|
||||
isRecordMatchingCurrentPage
|
||||
);
|
||||
};
|
||||
+25
-10
@@ -1,18 +1,27 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useWorkspaceNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useWorkspaceNavigationMenuItems';
|
||||
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
import { activeNavigationItemState } from '@/navigation-menu-item/common/states/activeNavigationItemState';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
import { shouldShowOpenedSection } from '@/navigation-menu-item/display/sections/utils/shouldShowOpenedSection';
|
||||
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
|
||||
export const NavigationDrawerOpenedSection = () => {
|
||||
const { t } = useLingui();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
|
||||
const { objectMetadataIdsInWorkspaceNav } = useWorkspaceNavigationMenuItems();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsData();
|
||||
const activeNavigationItem = useAtomStateValue(activeNavigationItemState);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const {
|
||||
objectNamePlural: currentObjectNamePlural,
|
||||
@@ -24,12 +33,18 @@ export const NavigationDrawerOpenedSection = () => {
|
||||
item.namePlural === currentObjectNamePlural ||
|
||||
item.nameSingular === currentObjectNameSingular,
|
||||
);
|
||||
const shouldShowOpenedSection = isDefined(objectMetadataItem)
|
||||
? !objectMetadataIdsInWorkspaceNav.has(objectMetadataItem.id)
|
||||
: false;
|
||||
|
||||
const isExpanded = shouldShowOpenedSection({
|
||||
objectMetadataItem,
|
||||
pathname,
|
||||
activeNavigationItem,
|
||||
workspaceNavigationMenuItems,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
});
|
||||
|
||||
return (
|
||||
<AnimatedExpandableContainer isExpanded={shouldShowOpenedSection}>
|
||||
<AnimatedExpandableContainer isExpanded={isExpanded}>
|
||||
<NavigationDrawerSectionForObjectMetadataItems
|
||||
sectionTitle={t`Opened`}
|
||||
objectMetadataItems={
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type ActiveNavigationItem } from '@/navigation-menu-item/common/types/ActiveNavigationItem';
|
||||
import { matchesRecordShowPathForObject } from '@/navigation-menu-item/common/utils/matchesRecordShowPathForObject';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
type ShouldShowOpenedSectionParams = {
|
||||
objectMetadataItem: EnrichedObjectMetadataItem | undefined;
|
||||
pathname: string;
|
||||
activeNavigationItem: ActiveNavigationItem | null;
|
||||
workspaceNavigationMenuItems: NavigationMenuItem[];
|
||||
objectMetadataItems: EnrichedObjectMetadataItem[];
|
||||
views: Pick<View, 'id' | 'objectMetadataId'>[];
|
||||
};
|
||||
|
||||
export const shouldShowOpenedSection = ({
|
||||
objectMetadataItem,
|
||||
pathname,
|
||||
activeNavigationItem,
|
||||
workspaceNavigationMenuItems,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
}: ShouldShowOpenedSectionParams): boolean => {
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!matchesRecordShowPathForObject(pathname, objectMetadataItem.nameSingular)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasTopLevelObjectWorkspaceItem = workspaceNavigationMenuItems.some(
|
||||
(item) => {
|
||||
if (item.type !== NavigationMenuItemType.OBJECT) {
|
||||
return false;
|
||||
}
|
||||
const metadata = getObjectMetadataForNavigationMenuItem(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return isDefined(metadata) && metadata.id === objectMetadataItem.id;
|
||||
},
|
||||
);
|
||||
|
||||
if (hasTopLevelObjectWorkspaceItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isDefined(activeNavigationItem)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
activeNavigationItem.objectNameSingular !== objectMetadataItem.nameSingular
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const activeWorkspaceNavigationMenuItem = workspaceNavigationMenuItems.find(
|
||||
(item) => item.id === activeNavigationItem.navItemId,
|
||||
);
|
||||
|
||||
if (!isDefined(activeWorkspaceNavigationMenuItem)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isViewOrRecord =
|
||||
activeWorkspaceNavigationMenuItem.type === NavigationMenuItemType.VIEW ||
|
||||
activeWorkspaceNavigationMenuItem.type === NavigationMenuItemType.RECORD;
|
||||
|
||||
if (!isViewOrRecord) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const objectMetadataForActiveItem = getObjectMetadataForNavigationMenuItem(
|
||||
activeWorkspaceNavigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
const targetsCurrentObject =
|
||||
isDefined(objectMetadataForActiveItem) &&
|
||||
objectMetadataForActiveItem.id === objectMetadataItem.id;
|
||||
|
||||
return !targetsCurrentObject;
|
||||
};
|
||||
+5
-3
@@ -62,6 +62,7 @@ export type NavigationDrawerItemProps = {
|
||||
preventCollapseOnMobile?: boolean;
|
||||
isSelectedInEditMode?: boolean;
|
||||
variant?: 'default' | 'tertiary';
|
||||
onBeforeNavigation?: () => void;
|
||||
};
|
||||
|
||||
type StyledItemProps = Pick<
|
||||
@@ -267,6 +268,7 @@ export const NavigationDrawerItem = ({
|
||||
preventCollapseOnMobile = false,
|
||||
isSelectedInEditMode = false,
|
||||
variant = 'default',
|
||||
onBeforeNavigation: onBeforeNavigationProp,
|
||||
}: NavigationDrawerItemProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const isMobile = useIsMobile();
|
||||
@@ -286,10 +288,11 @@ export const NavigationDrawerItem = ({
|
||||
const showBreadcrumb = indentationLevel === 2;
|
||||
const showStyledSpacer = isDefined(modifier) || isDefined(rightOptions);
|
||||
|
||||
const handleMobileNavigation = () => {
|
||||
const handleBeforeNavigation = () => {
|
||||
if (isMobile && !preventCollapseOnMobile) {
|
||||
setIsNavigationDrawerExpanded(false);
|
||||
}
|
||||
onBeforeNavigationProp?.();
|
||||
};
|
||||
|
||||
const isExternalLink =
|
||||
@@ -297,7 +300,6 @@ export const NavigationDrawerItem = ({
|
||||
const isInternalLink = isDefined(to) && !isExternalLink;
|
||||
|
||||
const handleExternalLinkClick = () => {
|
||||
handleMobileNavigation();
|
||||
if (isDefined(to)) {
|
||||
window.open(to, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
@@ -309,7 +311,7 @@ export const NavigationDrawerItem = ({
|
||||
} = useMouseDownNavigation({
|
||||
to: isExternalLink ? undefined : to,
|
||||
onClick: isExternalLink ? (onClick ?? handleExternalLinkClick) : onClick,
|
||||
onBeforeNavigation: handleMobileNavigation,
|
||||
onBeforeNavigation: handleBeforeNavigation,
|
||||
triggerEvent,
|
||||
});
|
||||
|
||||
|
||||
+2
@@ -21,6 +21,7 @@ export const NavigationDrawerSubItem = ({
|
||||
isSelectedInEditMode,
|
||||
triggerEvent,
|
||||
variant,
|
||||
onBeforeNavigation,
|
||||
}: NavigationDrawerSubItemProps) => {
|
||||
return (
|
||||
<NavigationDrawerItem
|
||||
@@ -40,6 +41,7 @@ export const NavigationDrawerSubItem = ({
|
||||
isSelectedInEditMode={isSelectedInEditMode}
|
||||
triggerEvent={triggerEvent}
|
||||
variant={variant}
|
||||
onBeforeNavigation={onBeforeNavigation}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user