refactor: remove ProcessedNavigationMenuItem, derive display fields at point of use (#18687)
## Summary - Removes `ProcessedNavigationMenuItem` type and the `sortNavigationMenuItems` enrichment pipeline that pre-computed display fields (label, link, icon, avatarUrl, etc.) for every navigation menu item upfront - Replaces with `filterAndSortNavigationMenuItems` (pure filter+sort returning raw `NavigationMenuItem[]`) and small utility functions (`getNavigationMenuItemLabel`, `getNavigationMenuItemComputedLink`, `getNavigationMenuItemObjectNameSingular`) that components call on demand - Eliminates the redundant `itemType` field (was identical to the raw `type` field) across ~30 consumer files - Each type-specific renderer (`NavigationDrawerItemForObjectMetadataItem`, `NavigationMenuItemIcon`, link/folder components) now derives only the 1-2 display fields it actually needs from the raw item + globally available Jotai atoms Net result: -1199 / +1177 lines, 7 files deleted, 4 new utility files.
This commit is contained in:
+2
-2
@@ -5,7 +5,7 @@ import { IconGripVertical, type IconComponent } from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
|
||||
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
|
||||
|
||||
@@ -86,7 +86,7 @@ export const AddToNavigationDragHandle = ({
|
||||
? payload.iconColor
|
||||
: undefined;
|
||||
const effectiveColor = getEffectiveNavigationMenuItemColor(
|
||||
{ itemType: payload.type as NavigationMenuItemType },
|
||||
{ type: payload.type as NavigationMenuItemType },
|
||||
objectColor,
|
||||
);
|
||||
const hasBackgroundColor =
|
||||
|
||||
+120
-59
@@ -1,11 +1,14 @@
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useState } from 'react';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
|
||||
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
@@ -18,16 +21,17 @@ import { NavigationMenuItemFolderNavigationDrawerItemDropdown } from '@/navigati
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID } from '@/navigation-menu-item/constants/NavigationMenuItemFolderDeleteModalId';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
|
||||
import { useDeleteNavigationMenuItemFolder } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItemFolder';
|
||||
import { useRenameNavigationMenuItemFolder } from '@/navigation-menu-item/hooks/useRenameNavigationMenuItemFolder';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
|
||||
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
|
||||
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
@@ -45,14 +49,15 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
|
||||
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 { ViewKey } from '@/views/types/ViewKey';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type CurrentWorkspaceMemberNavigationMenuItemsProps = {
|
||||
folder: {
|
||||
id: string;
|
||||
folderName: string;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
};
|
||||
isGroup: boolean;
|
||||
isWorkspaceFolder?: boolean;
|
||||
@@ -65,6 +70,7 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
}: CurrentWorkspaceMemberNavigationMenuItemsProps) => {
|
||||
const { t } = useLingui();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const currentPath = location.pathname;
|
||||
@@ -105,13 +111,26 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
const firstNonLinkItem = folder.navigationMenuItems.find(
|
||||
(item) =>
|
||||
item.itemType !== NavigationMenuItemType.LINK &&
|
||||
isNonEmptyString(item.link),
|
||||
);
|
||||
if (isDefined(firstNonLinkItem?.link)) {
|
||||
navigate(firstNonLinkItem.link);
|
||||
const firstNonLinkItem = folder.navigationMenuItems.find((item) => {
|
||||
if (item.type === NavigationMenuItemType.LINK) {
|
||||
return false;
|
||||
}
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return isNonEmptyString(computedLink);
|
||||
});
|
||||
if (isDefined(firstNonLinkItem)) {
|
||||
const link = getNavigationMenuItemComputedLink(
|
||||
firstNonLinkItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
if (isNonEmptyString(link)) {
|
||||
navigate(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -131,8 +150,19 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const selectedNavigationMenuItemIndex = folder.navigationMenuItems.findIndex(
|
||||
(item) =>
|
||||
isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item),
|
||||
(item) => {
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
item.type,
|
||||
computedLink,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
@@ -261,53 +291,84 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{folder.navigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<DraggableItem
|
||||
key={navigationMenuItem.id}
|
||||
draggableId={navigationMenuItem.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
itemComponent={
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
})}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
to={isDragging ? undefined : navigationMenuItem.link}
|
||||
active={index === selectedNavigationMenuItemIndex}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index,
|
||||
arrayLength: navigationMenuItemFolderContentLength,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
rightOptions={
|
||||
isWorkspaceFolder ? undefined : (
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteNavigationMenuItem(navigationMenuItem.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
{folder.navigationMenuItems.map((navigationMenuItem, index) => {
|
||||
const label = getNavigationMenuItemLabel(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const objectNameSingular =
|
||||
getNavigationMenuItemObjectNameSingular(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const view = isDefined(navigationMenuItem.viewId)
|
||||
? views.find(
|
||||
(view) => view.id === navigationMenuItem.viewId,
|
||||
)
|
||||
: undefined;
|
||||
const isIndexView = view?.key === ViewKey.INDEX;
|
||||
|
||||
return (
|
||||
<DraggableItem
|
||||
key={navigationMenuItem.id}
|
||||
draggableId={navigationMenuItem.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
itemComponent={
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={
|
||||
isIndexView
|
||||
? undefined
|
||||
: getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
objectNameSingular ?? '',
|
||||
})
|
||||
}
|
||||
label={label}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)
|
||||
}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
iconColor={getEffectiveNavigationMenuItemColor(
|
||||
navigationMenuItem,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
to={isDragging ? undefined : computedLink}
|
||||
active={index === selectedNavigationMenuItemIndex}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index,
|
||||
arrayLength: navigationMenuItemFolderContentLength,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
rightOptions={
|
||||
isWorkspaceFolder ? undefined : (
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteNavigationMenuItem(
|
||||
navigationMenuItem.id,
|
||||
);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
)
|
||||
}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
iconColor={getEffectiveNavigationMenuItemColor(
|
||||
navigationMenuItem,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
+75
-51
@@ -14,10 +14,14 @@ import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/N
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
|
||||
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
|
||||
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
|
||||
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
|
||||
@@ -31,6 +35,7 @@ const StyledOrphanNavigationMenuItemsContainer = styled.div`
|
||||
|
||||
export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
const currentPath = useLocation().pathname;
|
||||
@@ -48,58 +53,77 @@ export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => {
|
||||
>
|
||||
{orphanNavigationMenuItems.length > 0 ? (
|
||||
<>
|
||||
{orphanNavigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<NavigationItemDropTarget
|
||||
key={navigationMenuItem.id}
|
||||
folderId={null}
|
||||
index={index}
|
||||
sectionId={NavigationSections.FAVORITES}
|
||||
>
|
||||
<DraggableItem
|
||||
draggableId={navigationMenuItem.id}
|
||||
{orphanNavigationMenuItems.map((navigationMenuItem, index) => {
|
||||
const label = getNavigationMenuItemLabel(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigationItemDropTarget
|
||||
key={navigationMenuItem.id}
|
||||
folderId={null}
|
||||
index={index}
|
||||
isInsideScrollableContainer={true}
|
||||
itemComponent={
|
||||
<StyledOrphanNavigationMenuItemsContainer>
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
})}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
iconColor={getEffectiveNavigationMenuItemColor(
|
||||
navigationMenuItem,
|
||||
)}
|
||||
active={isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
navigationMenuItem,
|
||||
)}
|
||||
to={isDragging ? undefined : navigationMenuItem.link}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteNavigationMenuItem(navigationMenuItem.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
</StyledOrphanNavigationMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
</NavigationItemDropTarget>
|
||||
))}
|
||||
sectionId={NavigationSections.FAVORITES}
|
||||
>
|
||||
<DraggableItem
|
||||
draggableId={navigationMenuItem.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer={true}
|
||||
itemComponent={
|
||||
<StyledOrphanNavigationMenuItemsContainer>
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
objectNameSingular ?? '',
|
||||
})}
|
||||
label={label}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
iconColor={getEffectiveNavigationMenuItemColor(
|
||||
navigationMenuItem,
|
||||
)}
|
||||
active={isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
navigationMenuItem.type,
|
||||
computedLink,
|
||||
)}
|
||||
to={isDragging ? undefined : computedLink}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteNavigationMenuItem(navigationMenuItem.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
</StyledOrphanNavigationMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
</NavigationItemDropTarget>
|
||||
);
|
||||
})}
|
||||
<NavigationItemDropTarget
|
||||
folderId={null}
|
||||
index={orphanNavigationMenuItems.length}
|
||||
|
||||
+61
-37
@@ -1,21 +1,25 @@
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { IconHeartOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemBackButton } from '@/navigation-menu-item/components/NavigationMenuItemBackButton';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
|
||||
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
|
||||
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
|
||||
type NavigationMenuItemFolderContentProps = {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
};
|
||||
|
||||
export const NavigationMenuItemFolderContent = ({
|
||||
@@ -24,6 +28,7 @@ export const NavigationMenuItemFolderContent = ({
|
||||
navigationMenuItems,
|
||||
}: NavigationMenuItemFolderContentProps) => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
|
||||
return (
|
||||
@@ -36,41 +41,60 @@ export const NavigationMenuItemFolderContent = ({
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{navigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<DraggableItem
|
||||
key={navigationMenuItem.id}
|
||||
draggableId={navigationMenuItem.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
itemComponent={
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
})}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteNavigationMenuItem(navigationMenuItem.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
triggerEvent="CLICK"
|
||||
to={navigationMenuItem.link}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{navigationMenuItems.map((navigationMenuItem, index) => {
|
||||
const label = getNavigationMenuItemLabel(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const objectNameSingular =
|
||||
getNavigationMenuItemObjectNameSingular(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
return (
|
||||
<DraggableItem
|
||||
key={navigationMenuItem.id}
|
||||
draggableId={navigationMenuItem.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
itemComponent={
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
objectNameSingular ?? '',
|
||||
})}
|
||||
label={label}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteNavigationMenuItem(navigationMenuItem.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
triggerEvent="CLICK"
|
||||
to={computedLink}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemFolderContent } from '@/navigation-menu-item/components/NavigationMenuItemFolderContent';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
|
||||
type NavigationMenuItemFolderContentDispatcherEffectProps = {
|
||||
folderName: string;
|
||||
folderId: string;
|
||||
navigationMenuItems?: ProcessedNavigationMenuItem[];
|
||||
navigationMenuItems?: NavigationMenuItem[];
|
||||
};
|
||||
|
||||
export const NavigationMenuItemFolderContentDispatcherEffect = ({
|
||||
|
||||
+64
-26
@@ -1,68 +1,86 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar, IconLink, IconWorld, useIcons } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay';
|
||||
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
|
||||
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
|
||||
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/getNavigationMenuItemIconStyleFromColor';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
|
||||
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
|
||||
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
|
||||
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
export const NavigationMenuItemIcon = ({
|
||||
navigationMenuItem,
|
||||
}: {
|
||||
navigationMenuItem: ProcessedNavigationMenuItem;
|
||||
navigationMenuItem: NavigationMenuItem;
|
||||
}) => {
|
||||
const { getIcon } = useIcons();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const { Icon: StandardIcon, IconColor } = useGetStandardObjectIcon(
|
||||
navigationMenuItem.objectNameSingular ?? '',
|
||||
);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const isRecord =
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.RECORD;
|
||||
const objectNameSingular =
|
||||
getNavigationMenuItemObjectNameSingular(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
) ?? '';
|
||||
|
||||
const { Icon: StandardIcon, IconColor } =
|
||||
useGetStandardObjectIcon(objectNameSingular);
|
||||
|
||||
const isRecord = navigationMenuItem.type === NavigationMenuItemType.RECORD;
|
||||
|
||||
const view = isDefined(navigationMenuItem.viewId)
|
||||
? views.find((view) => view.id === navigationMenuItem.viewId)
|
||||
: undefined;
|
||||
const isViewWithOverlay =
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
|
||||
navigationMenuItem.viewKey !== ViewKey.INDEX;
|
||||
navigationMenuItem.type === NavigationMenuItemType.VIEW &&
|
||||
isDefined(view) &&
|
||||
view.key !== ViewKey.INDEX;
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === navigationMenuItem.objectNameSingular,
|
||||
);
|
||||
const objectNavItemColor = useObjectNavItemColor(
|
||||
navigationMenuItem.objectNameSingular ?? '',
|
||||
(item) => item.nameSingular === objectNameSingular,
|
||||
);
|
||||
const objectNavItemColor = useObjectNavItemColor(objectNameSingular);
|
||||
const objectIconForView =
|
||||
objectMetadataItem?.icon != null
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: StandardIcon;
|
||||
|
||||
const canShowViewOverlay =
|
||||
isViewWithOverlay &&
|
||||
isDefined(objectIconForView) &&
|
||||
isDefined(navigationMenuItem.Icon);
|
||||
isViewWithOverlay && isDefined(objectIconForView) && isDefined(view?.icon);
|
||||
|
||||
if (canShowViewOverlay) {
|
||||
return (
|
||||
<ObjectIconWithViewOverlay
|
||||
ObjectIcon={objectIconForView}
|
||||
ViewIcon={getIcon(navigationMenuItem.Icon!)}
|
||||
ViewIcon={getIcon(view!.icon)}
|
||||
objectColor={objectNavItemColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
|
||||
if (navigationMenuItem.type === NavigationMenuItemType.LINK) {
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return (
|
||||
<LinkIconWithLinkOverlay
|
||||
link={navigationMenuItem.link}
|
||||
link={computedLink}
|
||||
LinkIcon={IconLink}
|
||||
DefaultIcon={IconWorld}
|
||||
color={getEffectiveNavigationMenuItemColor(navigationMenuItem)}
|
||||
@@ -70,9 +88,13 @@ export const NavigationMenuItemIcon = ({
|
||||
);
|
||||
}
|
||||
|
||||
const iconToUse =
|
||||
StandardIcon ??
|
||||
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
|
||||
const itemIcon = isRecord
|
||||
? undefined
|
||||
: objectMetadataItem?.icon
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: undefined;
|
||||
const iconToUse = StandardIcon ?? itemIcon;
|
||||
|
||||
const effectiveColor = getEffectiveNavigationMenuItemColor(
|
||||
navigationMenuItem,
|
||||
objectNavItemColor,
|
||||
@@ -88,14 +110,30 @@ export const NavigationMenuItemIcon = ({
|
||||
? IconColor
|
||||
: themeCssVariables.font.color.secondary;
|
||||
|
||||
const labelIdentifier = getNavigationMenuItemLabel(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
const recordIdentifier =
|
||||
isRecord &&
|
||||
isDefined(navigationMenuItem.targetRecordIdentifier) &&
|
||||
isDefined(objectMetadataItem)
|
||||
? recordIdentifierToObjectRecordIdentifier({
|
||||
recordIdentifier: navigationMenuItem.targetRecordIdentifier,
|
||||
objectMetadataItem,
|
||||
})
|
||||
: null;
|
||||
|
||||
const avatar = (
|
||||
<Avatar
|
||||
size={iconStyle ? 'sm' : 'md'}
|
||||
type={navigationMenuItem.avatarType}
|
||||
type={recordIdentifier?.avatarType ?? 'icon'}
|
||||
Icon={iconToUse}
|
||||
iconColor={iconColorToUse}
|
||||
avatarUrl={navigationMenuItem.avatarUrl}
|
||||
placeholder={navigationMenuItem.labelIdentifier}
|
||||
avatarUrl={recordIdentifier?.avatarUrl ?? ''}
|
||||
placeholder={labelIdentifier}
|
||||
placeholderColorSeed={navigationMenuItem.targetRecordId ?? undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
+24
-6
@@ -6,21 +6,24 @@ import {
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
|
||||
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
|
||||
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
type WorkspaceNavigationMenuItemFolderDragCloneProps = {
|
||||
draggableProvided: DraggableProvided;
|
||||
draggableSnapshot: DraggableStateSnapshot;
|
||||
rubric: DraggableRubric;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
navigationMenuItemFolderContentLength: number;
|
||||
selectedNavigationMenuItemIndex: number;
|
||||
};
|
||||
@@ -35,12 +38,28 @@ export const WorkspaceNavigationMenuItemFolderDragClone = ({
|
||||
}: WorkspaceNavigationMenuItemFolderDragCloneProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const navigationMenuItem = navigationMenuItems[rubric.source.index];
|
||||
|
||||
if (!isDefined(navigationMenuItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = getNavigationMenuItemLabel(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const view = isDefined(navigationMenuItem.viewId)
|
||||
? views.find((view) => view.id === navigationMenuItem.viewId)
|
||||
: undefined;
|
||||
const isIndexView = view?.key === ViewKey.INDEX;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={draggableProvided.innerRef}
|
||||
@@ -57,15 +76,14 @@ export const WorkspaceNavigationMenuItemFolderDragClone = ({
|
||||
>
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={
|
||||
navigationMenuItem.viewKey === ViewKey.INDEX
|
||||
isIndexView
|
||||
? undefined
|
||||
: getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
navigationMenuItemObjectNameSingular: objectNameSingular ?? '',
|
||||
})
|
||||
}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
label={label}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
|
||||
)}
|
||||
|
||||
+37
-16
@@ -1,12 +1,15 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
|
||||
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
|
||||
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
@@ -15,7 +18,7 @@ import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
type WorkspaceNavigationMenuItemFolderSubItemProps = {
|
||||
navigationMenuItem: ProcessedNavigationMenuItem;
|
||||
navigationMenuItem: NavigationMenuItem;
|
||||
index: number;
|
||||
arrayLength: number;
|
||||
selectedNavigationMenuItemIndex: number;
|
||||
@@ -38,20 +41,22 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
|
||||
);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const objectMetadataItem =
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT ||
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW ||
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.RECORD
|
||||
navigationMenuItem.type === NavigationMenuItemType.OBJECT ||
|
||||
navigationMenuItem.type === NavigationMenuItemType.VIEW ||
|
||||
navigationMenuItem.type === NavigationMenuItemType.RECORD
|
||||
? getObjectMetadataForNavigationMenuItem(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
)
|
||||
: null;
|
||||
|
||||
const isEditableInEditMode =
|
||||
isNavigationMenuInEditMode &&
|
||||
isDefined(onNavigationMenuItemClick) &&
|
||||
(navigationMenuItem.itemType === NavigationMenuItemType.LINK ||
|
||||
(navigationMenuItem.type === NavigationMenuItemType.LINK ||
|
||||
isDefined(objectMetadataItem));
|
||||
|
||||
const handleEditModeClick =
|
||||
@@ -63,26 +68,42 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const label = getNavigationMenuItemLabel(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
const view = isDefined(navigationMenuItem.viewId)
|
||||
? views.find((view) => view.id === navigationMenuItem.viewId)
|
||||
: undefined;
|
||||
const isIndexView = view?.key === ViewKey.INDEX;
|
||||
|
||||
return (
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={
|
||||
navigationMenuItem.viewKey === ViewKey.INDEX
|
||||
isIndexView
|
||||
? undefined
|
||||
: getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
navigationMenuItemObjectNameSingular: objectNameSingular ?? '',
|
||||
})
|
||||
}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
label={label}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
|
||||
)}
|
||||
to={
|
||||
isContextDragging || handleEditModeClick
|
||||
? undefined
|
||||
: navigationMenuItem.link
|
||||
}
|
||||
to={isContextDragging || handleEditModeClick ? undefined : computedLink}
|
||||
onClick={handleEditModeClick}
|
||||
active={index === selectedNavigationMenuItemIndex}
|
||||
isSelectedInEditMode={
|
||||
|
||||
+39
-18
@@ -13,7 +13,7 @@ import { LightIconButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
|
||||
import {
|
||||
@@ -25,16 +25,19 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/nav
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
|
||||
import { preloadWorkspaceDndKit } from '@/navigation/preloadWorkspaceDndKit';
|
||||
import { NavigationDrawerSectionForWorkspaceItems } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { navigationMenuItemsSelector } from '@/navigation-menu-item/states/navigationMenuItemsSelector';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
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 { useStore } from 'jotai';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
|
||||
const StyledRightIconsContainer = styled.div`
|
||||
align-items: center;
|
||||
@@ -45,6 +48,8 @@ const StyledRightIconsContainer = styled.div`
|
||||
export const WorkspaceNavigationMenuItems = () => {
|
||||
const items = useWorkspaceSectionItems();
|
||||
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const store = useStore();
|
||||
const enterEditMode = () => {
|
||||
const currentNavigationMenuItems = store.get(
|
||||
@@ -88,16 +93,27 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
);
|
||||
openNavigationMenuItemInSidePanel({
|
||||
pageTitle: t`Edit folder`,
|
||||
pageIcon: getIcon(item.icon ?? item.Icon ?? FOLDER_ICON_DEFAULT),
|
||||
pageIcon: getIcon(item.icon ?? FOLDER_ICON_DEFAULT),
|
||||
});
|
||||
const firstChild = workspaceNavigationMenuItemsSorted.find(
|
||||
(navItem) =>
|
||||
navItem.folderId === folderId &&
|
||||
navItem.itemType !== NavigationMenuItemType.LINK &&
|
||||
isNonEmptyString(navItem.link),
|
||||
);
|
||||
if (firstChild?.link) {
|
||||
navigate(firstChild.link);
|
||||
const firstChild = workspaceNavigationMenuItemsSorted.find((navItem) => {
|
||||
if (navItem.folderId !== folderId) return false;
|
||||
if (navItem.type === NavigationMenuItemType.LINK) return false;
|
||||
const link = getNavigationMenuItemComputedLink(
|
||||
navItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return isNonEmptyString(link);
|
||||
});
|
||||
if (firstChild) {
|
||||
const link = getNavigationMenuItemComputedLink(
|
||||
firstChild,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
if (isNonEmptyString(link)) {
|
||||
navigate(link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,16 +122,21 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
objectMetadataItem: ObjectMetadataItem | null | undefined,
|
||||
) => {
|
||||
if (objectMetadataItem) {
|
||||
const label =
|
||||
item.type === NavigationMenuItemType.VIEW ||
|
||||
item.type === NavigationMenuItemType.OBJECT
|
||||
? getNavigationMenuItemLabel(item, objectMetadataItems, views)
|
||||
: objectMetadataItem.labelSingular;
|
||||
openNavigationMenuItemInSidePanel({
|
||||
pageTitle:
|
||||
item.itemType === NavigationMenuItemType.VIEW ||
|
||||
item.itemType === NavigationMenuItemType.OBJECT
|
||||
? item.labelIdentifier
|
||||
: objectMetadataItem.labelSingular,
|
||||
pageTitle: label,
|
||||
pageIcon: getIcon(objectMetadataItem.icon),
|
||||
});
|
||||
}
|
||||
const link = 'link' in item ? item.link : undefined;
|
||||
const link = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
if (isNonEmptyString(link)) {
|
||||
navigate(link);
|
||||
}
|
||||
@@ -127,7 +148,7 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
const { item, objectMetadataItem } = params;
|
||||
setSelectedNavigationMenuItemInEditMode(item.id);
|
||||
|
||||
switch (item.itemType) {
|
||||
switch (item.type) {
|
||||
case NavigationMenuItemType.FOLDER:
|
||||
openFolderAndNavigateToFirstChild(item.id, item);
|
||||
break;
|
||||
|
||||
+2
-2
@@ -36,8 +36,8 @@ import { SortableDropTargetRefContext } from '@/navigation-menu-item/contexts/So
|
||||
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
@@ -76,7 +76,7 @@ type WorkspaceNavigationMenuItemsFolderProps = {
|
||||
folderName: string;
|
||||
folderIconKey?: string | null;
|
||||
folderColor?: string | null;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
isGroup: boolean;
|
||||
isSelectedInEditMode?: boolean;
|
||||
onEditModeClick?: () => void;
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
export enum NavigationMenuItemType {
|
||||
FOLDER = 'FOLDER',
|
||||
LINK = 'LINK',
|
||||
OBJECT = 'OBJECT',
|
||||
RECORD = 'RECORD',
|
||||
VIEW = 'VIEW',
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFolder, IconLink, useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddFolderToNavigationMenuDraft';
|
||||
import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddLinkToNavigationMenuDraft';
|
||||
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft';
|
||||
|
||||
+3
-38
@@ -1,11 +1,9 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { filterAndSortNavigationMenuItems } from '@/navigation-menu-item/utils/filterAndSortNavigationMenuItems';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
|
||||
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
@@ -16,7 +14,7 @@ type NavigationMenuItemFolder = {
|
||||
folderName: string;
|
||||
icon?: string | null;
|
||||
color?: string | null;
|
||||
navigationMenuItems: ReturnType<typeof sortNavigationMenuItems>[number][];
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
};
|
||||
|
||||
type NavigationMenuItemFolderEntry = Pick<
|
||||
@@ -85,43 +83,10 @@ export const useNavigationMenuItemsByFolder = () => {
|
||||
return sortedFolders.reduce<NavigationMenuItemFolder[]>((acc, folder) => {
|
||||
const itemsInFolder = itemsByFolderId.get(folder.id) || [];
|
||||
|
||||
const targetRecordIdentifiersMap = itemsInFolder.reduce<
|
||||
Map<string, ObjectRecordIdentifier>
|
||||
>((map, item) => {
|
||||
const itemTargetRecordId = item.targetRecordId;
|
||||
if (!isDefined(itemTargetRecordId) || isDefined(item.viewId)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
const targetRecordIdentifier = item.targetRecordIdentifier;
|
||||
|
||||
if (!isDefined(targetRecordIdentifier)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
const itemObjectMetadata = objectMetadataItems.find(
|
||||
(meta) => meta.id === item.targetObjectMetadataId,
|
||||
);
|
||||
|
||||
if (isDefined(itemObjectMetadata)) {
|
||||
const objectRecordIdentifier =
|
||||
recordIdentifierToObjectRecordIdentifier({
|
||||
recordIdentifier: targetRecordIdentifier,
|
||||
objectMetadataItem: itemObjectMetadata,
|
||||
});
|
||||
|
||||
map.set(itemTargetRecordId, objectRecordIdentifier);
|
||||
}
|
||||
|
||||
return map;
|
||||
}, new Map());
|
||||
|
||||
const sortedItems = sortNavigationMenuItems(
|
||||
const sortedItems = filterAndSortNavigationMenuItems(
|
||||
itemsInFolder,
|
||||
true,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
targetRecordIdentifiersMap,
|
||||
);
|
||||
|
||||
acc.push({
|
||||
|
||||
+22
-8
@@ -1,20 +1,31 @@
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
const getLabelForItemType = (
|
||||
itemType: NavigationMenuItemType,
|
||||
item: { name?: string | null; labelIdentifier?: string | null },
|
||||
const getLabelForItem = (
|
||||
item: NavigationMenuItem,
|
||||
objectMetadataItems: Parameters<typeof getNavigationMenuItemLabel>[1],
|
||||
views: Parameters<typeof getNavigationMenuItemLabel>[2],
|
||||
objectLabelSingular?: string | null,
|
||||
): string => {
|
||||
switch (itemType) {
|
||||
switch (item.type) {
|
||||
case NavigationMenuItemType.FOLDER:
|
||||
return item.name ?? 'Folder';
|
||||
case NavigationMenuItemType.LINK:
|
||||
return item.name ?? 'Link';
|
||||
case NavigationMenuItemType.OBJECT:
|
||||
case NavigationMenuItemType.VIEW:
|
||||
return item.labelIdentifier ?? objectLabelSingular ?? '';
|
||||
return (
|
||||
getNavigationMenuItemLabel(item, objectMetadataItems, views) ||
|
||||
objectLabelSingular ||
|
||||
''
|
||||
);
|
||||
default:
|
||||
return objectLabelSingular ?? '';
|
||||
}
|
||||
@@ -24,11 +35,14 @@ export const useSelectedNavigationMenuItemEditItemLabel = () => {
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const { selectedItemObjectMetadata } =
|
||||
useSelectedNavigationMenuItemEditItemObjectMetadata();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const selectedItemLabel = selectedItem
|
||||
? getLabelForItemType(
|
||||
selectedItem.itemType,
|
||||
? getLabelForItem(
|
||||
selectedItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
selectedItemObjectMetadata?.labelSingular,
|
||||
)
|
||||
: null;
|
||||
|
||||
+6
-90
@@ -1,13 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
|
||||
import { isNavigationMenuItemObject } from '@/navigation-menu-item/utils/isNavigationMenuItemObject';
|
||||
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
|
||||
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { filterAndSortNavigationMenuItems } from '@/navigation-menu-item/utils/filterAndSortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
@@ -19,99 +13,21 @@ export const useSortedNavigationMenuItems = () => {
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
|
||||
const targetRecordIdentifiers = useMemo(() => {
|
||||
const identifiersMap = new Map<string, ObjectRecordIdentifier>();
|
||||
|
||||
[...navigationMenuItems, ...workspaceNavigationMenuItems].forEach(
|
||||
(navigationMenuItem) => {
|
||||
if (isDefined(navigationMenuItem.viewId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemTargetRecordId = navigationMenuItem.targetRecordId;
|
||||
if (!isDefined(itemTargetRecordId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRecordIdentifier =
|
||||
navigationMenuItem.targetRecordIdentifier;
|
||||
if (!isDefined(targetRecordIdentifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === navigationMenuItem.targetObjectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const objectRecordIdentifier = recordIdentifierToObjectRecordIdentifier(
|
||||
{
|
||||
recordIdentifier: targetRecordIdentifier,
|
||||
objectMetadataItem,
|
||||
},
|
||||
);
|
||||
|
||||
identifiersMap.set(itemTargetRecordId, objectRecordIdentifier);
|
||||
},
|
||||
);
|
||||
|
||||
return identifiersMap;
|
||||
}, [navigationMenuItems, workspaceNavigationMenuItems, objectMetadataItems]);
|
||||
|
||||
const navigationMenuItemsSorted = useMemo(() => {
|
||||
return sortNavigationMenuItems(
|
||||
return filterAndSortNavigationMenuItems(
|
||||
navigationMenuItems,
|
||||
true,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
targetRecordIdentifiers,
|
||||
);
|
||||
}, [
|
||||
navigationMenuItems,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
targetRecordIdentifiers,
|
||||
]);
|
||||
}, [navigationMenuItems, views, objectMetadataItems]);
|
||||
|
||||
const workspaceNavigationMenuItemsSorted = useMemo(() => {
|
||||
const filtered = workspaceNavigationMenuItems.filter((item) => {
|
||||
if (isNavigationMenuItemFolder(item)) {
|
||||
return true;
|
||||
}
|
||||
if (isNavigationMenuItemLink(item)) {
|
||||
return true;
|
||||
}
|
||||
if (isNavigationMenuItemObject(item)) {
|
||||
return isDefined(item.targetObjectMetadataId);
|
||||
}
|
||||
if (isDefined(item.viewId)) {
|
||||
return views.some((view) => view.id === item.viewId);
|
||||
}
|
||||
|
||||
const itemTargetRecordId = item.targetRecordId;
|
||||
if (!isDefined(itemTargetRecordId)) {
|
||||
return false;
|
||||
}
|
||||
const matchesTargetRecord =
|
||||
targetRecordIdentifiers.has(itemTargetRecordId);
|
||||
return matchesTargetRecord;
|
||||
});
|
||||
return sortNavigationMenuItems(
|
||||
filtered,
|
||||
true,
|
||||
return filterAndSortNavigationMenuItems(
|
||||
workspaceNavigationMenuItems,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
targetRecordIdentifiers,
|
||||
);
|
||||
}, [
|
||||
workspaceNavigationMenuItems,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
targetRecordIdentifiers,
|
||||
]);
|
||||
}, [workspaceNavigationMenuItems, views, objectMetadataItems]);
|
||||
|
||||
return {
|
||||
navigationMenuItemsSorted,
|
||||
|
||||
+42
-12
@@ -1,19 +1,23 @@
|
||||
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 { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
|
||||
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';
|
||||
|
||||
type UseWorkspaceFolderOpenStateParams = {
|
||||
folderId: string;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
};
|
||||
|
||||
export const useWorkspaceFolderOpenState = ({
|
||||
@@ -25,6 +29,8 @@ export const useWorkspaceFolderOpenState = ({
|
||||
const currentPath = location.pathname;
|
||||
const currentViewPath = location.pathname + location.search;
|
||||
const isMobile = useIsMobile();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
|
||||
useAtomState(openNavigationMenuItemFolderIdsState);
|
||||
@@ -48,20 +54,44 @@ export const useWorkspaceFolderOpenState = ({
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
const firstNonLinkItem = navigationMenuItems.find(
|
||||
(item) =>
|
||||
item.itemType !== NavigationMenuItemType.LINK &&
|
||||
isNonEmptyString(item.link),
|
||||
);
|
||||
if (isDefined(firstNonLinkItem?.link)) {
|
||||
navigate(firstNonLinkItem.link);
|
||||
const firstNonLinkItem = navigationMenuItems.find((item) => {
|
||||
if (item.type === NavigationMenuItemType.LINK) {
|
||||
return false;
|
||||
}
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return isNonEmptyString(computedLink);
|
||||
});
|
||||
if (isDefined(firstNonLinkItem)) {
|
||||
const link = getNavigationMenuItemComputedLink(
|
||||
firstNonLinkItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
if (isNonEmptyString(link)) {
|
||||
navigate(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex(
|
||||
(item) =>
|
||||
isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item),
|
||||
(item) => {
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
item.type,
|
||||
computedLink,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+14
-23
@@ -1,8 +1,7 @@
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
@@ -17,19 +16,12 @@ import { useNavigationMenuItemsByFolder } from './useNavigationMenuItemsByFolder
|
||||
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
|
||||
import { useSortedNavigationMenuItems } from './useSortedNavigationMenuItems';
|
||||
|
||||
export type FlatWorkspaceItem =
|
||||
| ProcessedNavigationMenuItem
|
||||
| (NavigationMenuItem & {
|
||||
itemType: NavigationMenuItemType.FOLDER;
|
||||
Icon: string;
|
||||
});
|
||||
|
||||
export type NavigationMenuItemClickParams = {
|
||||
item: FlatWorkspaceItem;
|
||||
item: NavigationMenuItem;
|
||||
objectMetadataItem?: ObjectMetadataItem | null;
|
||||
};
|
||||
|
||||
export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
|
||||
export const useWorkspaceSectionItems = (): NavigationMenuItem[] => {
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsData();
|
||||
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { workspaceNavigationMenuItemsByFolder } =
|
||||
@@ -42,7 +34,7 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
|
||||
.filter((item) => !isDefined(item.folderId))
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
const processedObjectViewsById = new Map(
|
||||
const processedItemsById = new Map(
|
||||
workspaceNavigationMenuItemsSorted.map((item) => [item.id, item]),
|
||||
);
|
||||
|
||||
@@ -53,25 +45,24 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
|
||||
]),
|
||||
);
|
||||
|
||||
const flatItems: FlatWorkspaceItem[] = flatWorkspaceItems.reduce<
|
||||
FlatWorkspaceItem[]
|
||||
const flatItems: NavigationMenuItem[] = flatWorkspaceItems.reduce<
|
||||
NavigationMenuItem[]
|
||||
>((acc, item) => {
|
||||
if (isNavigationMenuItemFolder(item)) {
|
||||
acc.push({
|
||||
...item,
|
||||
itemType: NavigationMenuItemType.FOLDER,
|
||||
Icon: item.icon ?? FOLDER_ICON_DEFAULT,
|
||||
icon: item.icon ?? FOLDER_ICON_DEFAULT,
|
||||
});
|
||||
} else {
|
||||
const processedItem = processedObjectViewsById.get(item.id);
|
||||
if (!isDefined(processedItem)) {
|
||||
const validItem = processedItemsById.get(item.id);
|
||||
if (!isDefined(validItem)) {
|
||||
return acc;
|
||||
}
|
||||
if (processedItem.itemType === NavigationMenuItemType.LINK) {
|
||||
acc.push(processedItem);
|
||||
if (validItem.type === NavigationMenuItemType.LINK) {
|
||||
acc.push(validItem);
|
||||
} else {
|
||||
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
|
||||
processedItem,
|
||||
validItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
@@ -82,7 +73,7 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
|
||||
objectMetadataItem.id,
|
||||
).canReadObjectRecords
|
||||
) {
|
||||
acc.push(processedItem);
|
||||
acc.push(validItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +81,7 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
|
||||
}, []);
|
||||
|
||||
return flatItems.flatMap((item) =>
|
||||
item.itemType === NavigationMenuItemType.FOLDER
|
||||
item.type === NavigationMenuItemType.FOLDER
|
||||
? [item, ...(folderChildrenById.get(item.id) ?? [])]
|
||||
: [item],
|
||||
);
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { type ViewKey } from '@/views/types/ViewKey';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { type NavigationMenuItemDisplayFields } from '@/navigation-menu-item/utils/computeNavigationMenuItemDisplayFields';
|
||||
import { type NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
|
||||
export type ProcessedNavigationMenuItem = NavigationMenuItem &
|
||||
NavigationMenuItemDisplayFields & {
|
||||
viewKey?: ViewKey | null;
|
||||
itemType: NavigationMenuItemType;
|
||||
};
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
import { computeNavigationMenuItemDisplayFields } from '@/navigation-menu-item/utils/computeNavigationMenuItemDisplayFields';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||
|
||||
describe('computeNavigationMenuItemDisplayFields', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'metadata-id',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
labelSingular: 'Person',
|
||||
labelPlural: 'People',
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const mockObjectRecordIdentifier: ObjectRecordIdentifier = {
|
||||
id: 'record-id',
|
||||
name: 'John Doe',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
avatarType: 'rounded',
|
||||
linkToShowPage: '/app/objects/people/record-id',
|
||||
};
|
||||
|
||||
it('should return null when objectMetadataItem or objectRecordIdentifier is null', () => {
|
||||
expect(
|
||||
computeNavigationMenuItemDisplayFields(null, mockObjectRecordIdentifier),
|
||||
).toBeNull();
|
||||
expect(
|
||||
computeNavigationMenuItemDisplayFields(mockObjectMetadataItem, null),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return display fields from metadata and record identifier', () => {
|
||||
const result = computeNavigationMenuItemDisplayFields(
|
||||
mockObjectMetadataItem,
|
||||
mockObjectRecordIdentifier,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
labelIdentifier: 'John Doe',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
avatarType: 'rounded',
|
||||
link: '/app/objects/people/record-id',
|
||||
objectNameSingular: 'person',
|
||||
});
|
||||
});
|
||||
|
||||
it('should default optional identifier fields to empty string or icon', () => {
|
||||
const minimal: ObjectRecordIdentifier = {
|
||||
id: 'record-id',
|
||||
name: 'Jane Doe',
|
||||
};
|
||||
const result = computeNavigationMenuItemDisplayFields(
|
||||
mockObjectMetadataItem,
|
||||
minimal,
|
||||
);
|
||||
|
||||
expect(result?.avatarUrl).toBe('');
|
||||
expect(result?.avatarType).toBe('icon');
|
||||
expect(result?.link).toBe('');
|
||||
});
|
||||
});
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import { filterAndSortNavigationMenuItems } from '@/navigation-menu-item/utils/filterAndSortNavigationMenuItems';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
describe('filterAndSortNavigationMenuItems', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'metadata-id',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
labelPlural: 'People',
|
||||
icon: 'IconUser',
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const mockView: Pick<View, 'id' | 'objectMetadataId' | 'key'> = {
|
||||
id: 'view-id',
|
||||
objectMetadataId: 'metadata-id',
|
||||
key: ViewKey.INDEX,
|
||||
};
|
||||
|
||||
it('should return empty array when navigationMenuItems is empty', () => {
|
||||
const result = filterAndSortNavigationMenuItems([], [], []);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should keep view items when view and objectMetadata exist', () => {
|
||||
const navigationMenuItem = {
|
||||
id: 'item-id',
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewId: 'view-id',
|
||||
position: 1,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const result = filterAndSortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
[mockView],
|
||||
[mockObjectMetadataItem],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('item-id');
|
||||
});
|
||||
|
||||
it('should filter out view items when view is not found', () => {
|
||||
const navigationMenuItem = {
|
||||
id: 'item-id',
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewId: 'non-existent-view-id',
|
||||
position: 1,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const result = filterAndSortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should keep record items when targetRecordIdentifier exists', () => {
|
||||
const navigationMenuItem = {
|
||||
id: 'item-id',
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
targetRecordId: 'record-id',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
targetRecordIdentifier: {
|
||||
id: 'record-id',
|
||||
labelIdentifier: 'John Doe',
|
||||
},
|
||||
position: 2,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const result = filterAndSortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('item-id');
|
||||
});
|
||||
|
||||
it('should filter out record items when targetRecordId is not defined', () => {
|
||||
const navigationMenuItem = {
|
||||
id: 'item-id',
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 1,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const result = filterAndSortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out record items when objectMetadataItem is not found', () => {
|
||||
const navigationMenuItem = {
|
||||
id: 'item-id',
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
targetRecordId: 'record-id',
|
||||
targetObjectMetadataId: 'non-existent-metadata-id',
|
||||
targetRecordIdentifier: {
|
||||
id: 'record-id',
|
||||
labelIdentifier: 'John',
|
||||
},
|
||||
position: 1,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const result = filterAndSortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should sort navigation menu items by position', () => {
|
||||
const navigationMenuItems = [
|
||||
{
|
||||
id: 'item-3',
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
targetRecordId: 'record-id-3',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
targetRecordIdentifier: {
|
||||
id: 'record-id-3',
|
||||
labelIdentifier: 'C',
|
||||
},
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: 'item-1',
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
targetRecordId: 'record-id-1',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
targetRecordIdentifier: {
|
||||
id: 'record-id-1',
|
||||
labelIdentifier: 'A',
|
||||
},
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: 'item-2',
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
targetRecordId: 'record-id-2',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
targetRecordIdentifier: {
|
||||
id: 'record-id-2',
|
||||
labelIdentifier: 'B',
|
||||
},
|
||||
position: 2,
|
||||
},
|
||||
] as NavigationMenuItem[];
|
||||
|
||||
const result = filterAndSortNavigationMenuItems(
|
||||
navigationMenuItems,
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].id).toBe('item-1');
|
||||
expect(result[1].id).toBe('item-2');
|
||||
expect(result[2].id).toBe('item-3');
|
||||
});
|
||||
|
||||
it('should keep link items', () => {
|
||||
const result = filterAndSortNavigationMenuItems(
|
||||
[
|
||||
{
|
||||
id: 'link-1',
|
||||
type: NavigationMenuItemType.LINK,
|
||||
link: 'https://example.com',
|
||||
name: 'My Link',
|
||||
position: 1,
|
||||
} as NavigationMenuItem,
|
||||
],
|
||||
[],
|
||||
[],
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('link-1');
|
||||
});
|
||||
|
||||
it('should keep object items when objectMetadata exists', () => {
|
||||
const result = filterAndSortNavigationMenuItems(
|
||||
[
|
||||
{
|
||||
id: 'obj-1',
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 1,
|
||||
} as NavigationMenuItem,
|
||||
],
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('obj-1');
|
||||
});
|
||||
|
||||
it('should filter out object items when objectMetadata is not found', () => {
|
||||
const result = filterAndSortNavigationMenuItems(
|
||||
[
|
||||
{
|
||||
id: 'obj-1',
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectMetadataId: 'non-existent-id',
|
||||
position: 1,
|
||||
} as NavigationMenuItem,
|
||||
],
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should keep folder items', () => {
|
||||
const result = filterAndSortNavigationMenuItems(
|
||||
[
|
||||
{
|
||||
id: 'folder-1',
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
name: 'My Folder',
|
||||
position: 1,
|
||||
} as NavigationMenuItem,
|
||||
],
|
||||
[],
|
||||
[],
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('folder-1');
|
||||
});
|
||||
});
|
||||
+9
-9
@@ -1,4 +1,4 @@
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type View } from '@/views/types/View';
|
||||
@@ -30,7 +30,7 @@ const mockViews: View[] = [
|
||||
describe('getObjectMetadataForNavigationMenuItem', () => {
|
||||
it('should return null for link item type', () => {
|
||||
const result = getObjectMetadataForNavigationMenuItem(
|
||||
{ itemType: NavigationMenuItemType.LINK },
|
||||
{ type: NavigationMenuItemType.LINK },
|
||||
mockObjectMetadataItems,
|
||||
mockViews,
|
||||
);
|
||||
@@ -39,7 +39,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
|
||||
|
||||
it('should return object metadata for view item when view and metadata exist', () => {
|
||||
const result = getObjectMetadataForNavigationMenuItem(
|
||||
{ itemType: NavigationMenuItemType.VIEW, viewId: 'view-1' },
|
||||
{ type: NavigationMenuItemType.VIEW, viewId: 'view-1' },
|
||||
mockObjectMetadataItems,
|
||||
mockViews,
|
||||
);
|
||||
@@ -49,7 +49,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
|
||||
|
||||
it('should return null for view item when view is not found', () => {
|
||||
const result = getObjectMetadataForNavigationMenuItem(
|
||||
{ itemType: NavigationMenuItemType.VIEW, viewId: 'non-existent-view' },
|
||||
{ type: NavigationMenuItemType.VIEW, viewId: 'non-existent-view' },
|
||||
mockObjectMetadataItems,
|
||||
mockViews,
|
||||
);
|
||||
@@ -61,7 +61,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
|
||||
{ id: 'orphan-view', objectMetadataId: 'non-existent-metadata' } as View,
|
||||
];
|
||||
const result = getObjectMetadataForNavigationMenuItem(
|
||||
{ itemType: NavigationMenuItemType.VIEW, viewId: 'orphan-view' },
|
||||
{ type: NavigationMenuItemType.VIEW, viewId: 'orphan-view' },
|
||||
mockObjectMetadataItems,
|
||||
viewsWithOrphanView,
|
||||
);
|
||||
@@ -71,7 +71,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
|
||||
it('should return object metadata for record item when metadata exists', () => {
|
||||
const result = getObjectMetadataForNavigationMenuItem(
|
||||
{
|
||||
itemType: NavigationMenuItemType.RECORD,
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
targetObjectMetadataId: 'metadata-2',
|
||||
},
|
||||
mockObjectMetadataItems,
|
||||
@@ -83,7 +83,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
|
||||
|
||||
it('should return null for record item when targetObjectMetadataId is undefined', () => {
|
||||
const result = getObjectMetadataForNavigationMenuItem(
|
||||
{ itemType: NavigationMenuItemType.RECORD },
|
||||
{ type: NavigationMenuItemType.RECORD },
|
||||
mockObjectMetadataItems,
|
||||
mockViews,
|
||||
);
|
||||
@@ -93,7 +93,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
|
||||
it('should return null for record item when metadata is not found', () => {
|
||||
const result = getObjectMetadataForNavigationMenuItem(
|
||||
{
|
||||
itemType: NavigationMenuItemType.RECORD,
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
targetObjectMetadataId: 'non-existent-metadata',
|
||||
},
|
||||
mockObjectMetadataItems,
|
||||
@@ -104,7 +104,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
|
||||
|
||||
it('should return null for view item when viewId is undefined', () => {
|
||||
const result = getObjectMetadataForNavigationMenuItem(
|
||||
{ itemType: NavigationMenuItemType.VIEW },
|
||||
{ type: NavigationMenuItemType.VIEW },
|
||||
mockObjectMetadataItems,
|
||||
mockViews,
|
||||
);
|
||||
|
||||
+9
-17
@@ -1,4 +1,4 @@
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
|
||||
|
||||
describe('isLocationMatchingNavigationMenuItem', () => {
|
||||
@@ -7,20 +7,16 @@ describe('isLocationMatchingNavigationMenuItem', () => {
|
||||
isLocationMatchingNavigationMenuItem(
|
||||
'/app/objects/people',
|
||||
'/app/objects/people?viewId=123',
|
||||
{
|
||||
itemType: NavigationMenuItemType.RECORD,
|
||||
link: '/app/objects/people',
|
||||
},
|
||||
NavigationMenuItemType.RECORD,
|
||||
'/app/objects/people',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isLocationMatchingNavigationMenuItem(
|
||||
'/app/objects/companies',
|
||||
'/app/objects/companies?viewId=123',
|
||||
{
|
||||
itemType: NavigationMenuItemType.VIEW,
|
||||
link: '/app/objects/companies?viewId=123',
|
||||
},
|
||||
NavigationMenuItemType.VIEW,
|
||||
'/app/objects/companies?viewId=123',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -30,20 +26,16 @@ describe('isLocationMatchingNavigationMenuItem', () => {
|
||||
isLocationMatchingNavigationMenuItem(
|
||||
'/app/objects/people',
|
||||
'/app/objects/people?viewId=123',
|
||||
{
|
||||
itemType: NavigationMenuItemType.RECORD,
|
||||
link: '/app/objects/company',
|
||||
},
|
||||
NavigationMenuItemType.RECORD,
|
||||
'/app/objects/company',
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isLocationMatchingNavigationMenuItem(
|
||||
'/app/objects/companies',
|
||||
'/app/objects/companies?viewId=123',
|
||||
{
|
||||
itemType: NavigationMenuItemType.VIEW,
|
||||
link: '/app/objects/companies?viewId=456',
|
||||
},
|
||||
NavigationMenuItemType.VIEW,
|
||||
'/app/objects/companies?viewId=456',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
-360
@@ -1,360 +0,0 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
jest.mock('twenty-shared/utils', () => {
|
||||
const actual = jest.requireActual('twenty-shared/utils');
|
||||
return {
|
||||
...actual,
|
||||
getAppPath: jest.fn((path, params, query) => {
|
||||
const basePath = `/app/objects/${params.objectNamePlural}`;
|
||||
const viewId = query?.viewId;
|
||||
if (viewId !== undefined && viewId !== null) {
|
||||
return `${basePath}?viewId=${viewId}`;
|
||||
}
|
||||
return basePath;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('sortNavigationMenuItems', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'metadata-id',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
labelPlural: 'People',
|
||||
icon: 'IconUser',
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const mockView: Pick<
|
||||
View,
|
||||
'id' | 'name' | 'objectMetadataId' | 'icon' | 'key'
|
||||
> = {
|
||||
id: 'view-id',
|
||||
name: 'All People',
|
||||
objectMetadataId: 'metadata-id',
|
||||
icon: 'IconUser',
|
||||
key: ViewKey.INDEX,
|
||||
};
|
||||
|
||||
const mockObjectRecordIdentifier: ObjectRecordIdentifier = {
|
||||
id: 'record-id',
|
||||
name: 'John Doe',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
avatarType: 'rounded',
|
||||
linkToShowPage: '/app/objects/people/record-id',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return empty array when navigationMenuItems is empty', () => {
|
||||
const result = sortNavigationMenuItems([], true, [], [], new Map());
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should process view link navigation menu items', () => {
|
||||
const navigationMenuItem: NavigationMenuItem = {
|
||||
id: 'item-id',
|
||||
viewId: 'view-id',
|
||||
position: 1,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const result = sortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
true,
|
||||
[mockView],
|
||||
[mockObjectMetadataItem],
|
||||
new Map(),
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
id: 'item-id',
|
||||
viewId: 'view-id',
|
||||
position: 1,
|
||||
labelIdentifier: 'People',
|
||||
objectNameSingular: 'person',
|
||||
Icon: 'IconUser',
|
||||
});
|
||||
expect(result[0].link).toContain('viewId=view-id');
|
||||
});
|
||||
|
||||
it('should return null for view link when view is not found', () => {
|
||||
const navigationMenuItem: NavigationMenuItem = {
|
||||
id: 'item-id',
|
||||
viewId: 'non-existent-view-id',
|
||||
position: 1,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const result = sortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
true,
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
new Map(),
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should process record link navigation menu items', () => {
|
||||
const navigationMenuItem: NavigationMenuItem = {
|
||||
id: 'item-id',
|
||||
targetRecordId: 'record-id',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 2,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const targetRecordIdentifiers = new Map([
|
||||
['record-id', mockObjectRecordIdentifier],
|
||||
]);
|
||||
|
||||
const result = sortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
true,
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
targetRecordIdentifiers,
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
id: 'item-id',
|
||||
targetRecordId: 'record-id',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 2,
|
||||
labelIdentifier: 'John Doe',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
avatarType: 'rounded',
|
||||
link: '/app/objects/people/record-id',
|
||||
objectNameSingular: 'person',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for record link when targetRecordId is not defined', () => {
|
||||
const navigationMenuItem: NavigationMenuItem = {
|
||||
id: 'item-id',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 1,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const result = sortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
true,
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
new Map(),
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return null for record link when objectMetadataItem is not found', () => {
|
||||
const navigationMenuItem: NavigationMenuItem = {
|
||||
id: 'item-id',
|
||||
targetRecordId: 'record-id',
|
||||
targetObjectMetadataId: 'non-existent-metadata-id',
|
||||
position: 1,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const targetRecordIdentifiers = new Map([
|
||||
['record-id', mockObjectRecordIdentifier],
|
||||
]);
|
||||
|
||||
const result = sortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
true,
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
targetRecordIdentifiers,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return null for record link when targetRecordIdentifier is not found', () => {
|
||||
const navigationMenuItem: NavigationMenuItem = {
|
||||
id: 'item-id',
|
||||
targetRecordId: 'non-existent-record-id',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 1,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const result = sortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
true,
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
new Map(),
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty link when hasLinkToShowPage is false', () => {
|
||||
const navigationMenuItem: NavigationMenuItem = {
|
||||
id: 'item-id',
|
||||
targetRecordId: 'record-id',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 2,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const targetRecordIdentifiers = new Map([
|
||||
['record-id', mockObjectRecordIdentifier],
|
||||
]);
|
||||
|
||||
const result = sortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
false,
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
targetRecordIdentifiers,
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].link).toBe('');
|
||||
});
|
||||
|
||||
it('should sort navigation menu items by position', () => {
|
||||
const navigationMenuItems: NavigationMenuItem[] = [
|
||||
{
|
||||
id: 'item-3',
|
||||
targetRecordId: 'record-id-3',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: 'item-1',
|
||||
targetRecordId: 'record-id-1',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: 'item-2',
|
||||
targetRecordId: 'record-id-2',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 2,
|
||||
},
|
||||
] as NavigationMenuItem[];
|
||||
|
||||
const targetRecordIdentifiers = new Map([
|
||||
['record-id-1', { ...mockObjectRecordIdentifier, id: 'record-id-1' }],
|
||||
['record-id-2', { ...mockObjectRecordIdentifier, id: 'record-id-2' }],
|
||||
['record-id-3', { ...mockObjectRecordIdentifier, id: 'record-id-3' }],
|
||||
]);
|
||||
|
||||
const result = sortNavigationMenuItems(
|
||||
navigationMenuItems,
|
||||
true,
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
targetRecordIdentifiers,
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].id).toBe('item-1');
|
||||
expect(result[1].id).toBe('item-2');
|
||||
expect(result[2].id).toBe('item-3');
|
||||
});
|
||||
|
||||
it('should handle mixed view and record link items', () => {
|
||||
const navigationMenuItems: NavigationMenuItem[] = [
|
||||
{
|
||||
id: 'view-item',
|
||||
viewId: 'view-id',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: 'record-item',
|
||||
targetRecordId: 'record-id',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 2,
|
||||
},
|
||||
] as NavigationMenuItem[];
|
||||
|
||||
const targetRecordIdentifiers = new Map([
|
||||
['record-id', mockObjectRecordIdentifier],
|
||||
]);
|
||||
|
||||
const result = sortNavigationMenuItems(
|
||||
navigationMenuItems,
|
||||
true,
|
||||
[mockView],
|
||||
[mockObjectMetadataItem],
|
||||
targetRecordIdentifiers,
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].id).toBe('view-item');
|
||||
expect(result[0].objectNameSingular).toBe('person');
|
||||
expect(result[1].id).toBe('record-item');
|
||||
expect(result[1].objectNameSingular).toBe('person');
|
||||
});
|
||||
|
||||
it('should handle navigationMenuItem with both viewId and targetRecordId (viewId takes precedence)', () => {
|
||||
const navigationMenuItem: NavigationMenuItem = {
|
||||
id: 'item-id',
|
||||
viewId: 'view-id',
|
||||
targetRecordId: 'record-id',
|
||||
targetObjectMetadataId: 'metadata-id',
|
||||
position: 1,
|
||||
} as NavigationMenuItem;
|
||||
|
||||
const result = sortNavigationMenuItems(
|
||||
[navigationMenuItem],
|
||||
true,
|
||||
[mockView],
|
||||
[mockObjectMetadataItem],
|
||||
new Map(),
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].objectNameSingular).toBe('person');
|
||||
expect(result[0].viewId).toBe('view-id');
|
||||
});
|
||||
|
||||
it('should process link items with protocol normalization and label from name or link', () => {
|
||||
const withProtocol = sortNavigationMenuItems(
|
||||
[
|
||||
{
|
||||
id: 'link-1',
|
||||
type: NavigationMenuItemType.LINK,
|
||||
link: 'https://example.com',
|
||||
name: 'My Link',
|
||||
position: 1,
|
||||
} as NavigationMenuItem,
|
||||
],
|
||||
true,
|
||||
[],
|
||||
[],
|
||||
new Map(),
|
||||
);
|
||||
expect(withProtocol[0].labelIdentifier).toBe('My Link');
|
||||
expect(withProtocol[0].link).toBe('https://example.com');
|
||||
|
||||
const noProtocol = sortNavigationMenuItems(
|
||||
[
|
||||
{
|
||||
id: 'link-2',
|
||||
type: NavigationMenuItemType.LINK,
|
||||
link: 'example.com',
|
||||
position: 2,
|
||||
} as NavigationMenuItem,
|
||||
],
|
||||
true,
|
||||
[],
|
||||
[],
|
||||
new Map(),
|
||||
);
|
||||
expect(noProtocol[0].link).toBe('https://example.com');
|
||||
expect(noProtocol[0].labelIdentifier).toBe('example.com');
|
||||
});
|
||||
});
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type NavigationMenuItemDisplayFields = {
|
||||
labelIdentifier: string;
|
||||
avatarUrl: string;
|
||||
avatarType: 'icon' | 'rounded' | 'squared';
|
||||
link: string;
|
||||
objectNameSingular: string;
|
||||
Icon?: string;
|
||||
};
|
||||
|
||||
export const computeNavigationMenuItemDisplayFields = (
|
||||
objectMetadataItem: ObjectMetadataItem | null,
|
||||
objectRecordIdentifier: ObjectRecordIdentifier | null,
|
||||
): NavigationMenuItemDisplayFields | null => {
|
||||
if (!isDefined(objectMetadataItem) || !isDefined(objectRecordIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectNameSingular = objectMetadataItem.nameSingular;
|
||||
|
||||
return {
|
||||
labelIdentifier: objectRecordIdentifier.name,
|
||||
avatarUrl: objectRecordIdentifier.avatarUrl ?? '',
|
||||
avatarType: objectRecordIdentifier.avatarType ?? 'icon',
|
||||
link: objectRecordIdentifier.linkToShowPage ?? '',
|
||||
objectNameSingular,
|
||||
};
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
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 filterAndSortNavigationMenuItems = (
|
||||
navigationMenuItems: NavigationMenuItem[],
|
||||
views: Pick<View, 'id' | 'objectMetadataId' | 'key'>[],
|
||||
objectMetadataItems: Pick<ObjectMetadataItem, 'id'>[],
|
||||
): NavigationMenuItem[] => {
|
||||
return navigationMenuItems
|
||||
.filter((item) => {
|
||||
if (item.type === NavigationMenuItemType.FOLDER) {
|
||||
return true;
|
||||
}
|
||||
if (item.type === NavigationMenuItemType.LINK) {
|
||||
return true;
|
||||
}
|
||||
if (item.type === NavigationMenuItemType.OBJECT) {
|
||||
return (
|
||||
isDefined(item.targetObjectMetadataId) &&
|
||||
objectMetadataItems.some(
|
||||
(meta) => meta.id === item.targetObjectMetadataId,
|
||||
)
|
||||
);
|
||||
}
|
||||
if (item.type === NavigationMenuItemType.VIEW) {
|
||||
if (!isDefined(item.viewId)) {
|
||||
return false;
|
||||
}
|
||||
const view = views.find((view) => view.id === item.viewId);
|
||||
return (
|
||||
isDefined(view) &&
|
||||
objectMetadataItems.some((meta) => meta.id === view.objectMetadataId)
|
||||
);
|
||||
}
|
||||
if (item.type === NavigationMenuItemType.RECORD) {
|
||||
return (
|
||||
isDefined(item.targetRecordId) &&
|
||||
isDefined(item.targetObjectMetadataId) &&
|
||||
isDefined(item.targetRecordIdentifier) &&
|
||||
objectMetadataItems.some(
|
||||
(meta) => meta.id === item.targetObjectMetadataId,
|
||||
)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.sort((a, b) => a.position - b.position);
|
||||
};
|
||||
+6
-6
@@ -2,27 +2,27 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorLink';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
export const getEffectiveNavigationMenuItemColor = (
|
||||
navigationMenuItem: {
|
||||
itemType: NavigationMenuItemType;
|
||||
type: NavigationMenuItemType;
|
||||
color?: string | null;
|
||||
},
|
||||
objectColor?: string,
|
||||
): string | undefined => {
|
||||
if (navigationMenuItem.itemType === NavigationMenuItemType.FOLDER) {
|
||||
if (navigationMenuItem.type === NavigationMenuItemType.FOLDER) {
|
||||
return isNonEmptyString(navigationMenuItem.color)
|
||||
? navigationMenuItem.color
|
||||
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER;
|
||||
}
|
||||
if (navigationMenuItem.itemType === NavigationMenuItemType.OBJECT) {
|
||||
if (navigationMenuItem.type === NavigationMenuItemType.OBJECT) {
|
||||
return objectColor;
|
||||
}
|
||||
if (navigationMenuItem.itemType === NavigationMenuItemType.VIEW) {
|
||||
if (navigationMenuItem.type === NavigationMenuItemType.VIEW) {
|
||||
return objectColor;
|
||||
}
|
||||
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
|
||||
if (navigationMenuItem.type === NavigationMenuItemType.LINK) {
|
||||
return DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { AppPath, NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export const getNavigationMenuItemComputedLink = (
|
||||
item: NavigationMenuItem,
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
views: Pick<View, 'id' | 'objectMetadataId' | 'key'>[],
|
||||
): string => {
|
||||
switch (item.type) {
|
||||
case NavigationMenuItemType.OBJECT: {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === item.targetObjectMetadataId,
|
||||
);
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return '';
|
||||
}
|
||||
const indexView = views.find(
|
||||
(view) =>
|
||||
view.objectMetadataId === objectMetadataItem.id &&
|
||||
view.key === ViewKey.INDEX,
|
||||
);
|
||||
return getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: objectMetadataItem.namePlural },
|
||||
indexView ? { viewId: indexView.id } : {},
|
||||
);
|
||||
}
|
||||
case NavigationMenuItemType.VIEW: {
|
||||
const view = views.find((view) => view.id === item.viewId);
|
||||
if (!isDefined(view)) {
|
||||
return '';
|
||||
}
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === view.objectMetadataId,
|
||||
);
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return '';
|
||||
}
|
||||
return getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: objectMetadataItem.namePlural },
|
||||
{ viewId: item.viewId! },
|
||||
);
|
||||
}
|
||||
case NavigationMenuItemType.LINK: {
|
||||
const linkUrl = (item.link ?? '').trim();
|
||||
if (linkUrl.startsWith('http://') || linkUrl.startsWith('https://')) {
|
||||
return linkUrl;
|
||||
}
|
||||
return linkUrl ? `https://${linkUrl}` : '';
|
||||
}
|
||||
case NavigationMenuItemType.RECORD: {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === item.targetObjectMetadataId,
|
||||
);
|
||||
if (
|
||||
!isDefined(objectMetadataItem) ||
|
||||
!isDefined(item.targetRecordIdentifier)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
const objectRecordIdentifier = recordIdentifierToObjectRecordIdentifier({
|
||||
recordIdentifier: item.targetRecordIdentifier,
|
||||
objectMetadataItem,
|
||||
});
|
||||
return objectRecordIdentifier.linkToShowPage ?? '';
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export const getNavigationMenuItemLabel = (
|
||||
item: NavigationMenuItem,
|
||||
objectMetadataItems: Pick<
|
||||
ObjectMetadataItem,
|
||||
'id' | 'labelPlural' | 'nameSingular'
|
||||
>[],
|
||||
views: Pick<View, 'id' | 'name' | 'objectMetadataId' | 'key'>[],
|
||||
): string => {
|
||||
switch (item.type) {
|
||||
case NavigationMenuItemType.OBJECT: {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === item.targetObjectMetadataId,
|
||||
);
|
||||
return objectMetadataItem?.labelPlural ?? '';
|
||||
}
|
||||
case NavigationMenuItemType.VIEW: {
|
||||
const view = views.find((view) => view.id === item.viewId);
|
||||
if (!isDefined(view)) {
|
||||
return '';
|
||||
}
|
||||
if (view.key === ViewKey.INDEX) {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === view.objectMetadataId,
|
||||
);
|
||||
return objectMetadataItem?.labelPlural ?? view.name;
|
||||
}
|
||||
return view.name;
|
||||
}
|
||||
case NavigationMenuItemType.LINK: {
|
||||
const linkUrl = (item.link ?? '').trim();
|
||||
return (item.name ?? linkUrl) || 'Link';
|
||||
}
|
||||
case NavigationMenuItemType.RECORD: {
|
||||
return item.targetRecordIdentifier?.labelIdentifier ?? '';
|
||||
}
|
||||
case NavigationMenuItemType.FOLDER: {
|
||||
return item.name ?? 'Folder';
|
||||
}
|
||||
default:
|
||||
return item.name ?? '';
|
||||
}
|
||||
};
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
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 getNavigationMenuItemObjectNameSingular = (
|
||||
item: NavigationMenuItem,
|
||||
objectMetadataItems: Pick<ObjectMetadataItem, 'id' | 'nameSingular'>[],
|
||||
views: Pick<View, 'id' | 'objectMetadataId'>[],
|
||||
): string | undefined => {
|
||||
switch (item.type) {
|
||||
case NavigationMenuItemType.OBJECT:
|
||||
case NavigationMenuItemType.RECORD: {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === item.targetObjectMetadataId,
|
||||
);
|
||||
return objectMetadataItem?.nameSingular;
|
||||
}
|
||||
case NavigationMenuItemType.VIEW: {
|
||||
const view = views.find((view) => view.id === item.viewId);
|
||||
if (!isDefined(view)) {
|
||||
return undefined;
|
||||
}
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === view.objectMetadataId,
|
||||
);
|
||||
return objectMetadataItem?.nameSingular;
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
+22
-22
@@ -1,56 +1,56 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type View } from '@/views/types/View';
|
||||
|
||||
type NavigationMenuItemWithItemType = Pick<
|
||||
ProcessedNavigationMenuItem,
|
||||
'itemType' | 'viewId' | 'targetObjectMetadataId'
|
||||
>;
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export const getObjectMetadataForNavigationMenuItem = (
|
||||
navigationMenuItem: NavigationMenuItemWithItemType,
|
||||
navigationMenuItem: Pick<
|
||||
NavigationMenuItem,
|
||||
'type' | 'viewId' | 'targetObjectMetadataId'
|
||||
>,
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
views: View[],
|
||||
views: Pick<View, 'id' | 'objectMetadataId'>[],
|
||||
): ObjectMetadataItem | null => {
|
||||
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
|
||||
if (navigationMenuItem.type === NavigationMenuItemType.LINK) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT &&
|
||||
navigationMenuItem.type === NavigationMenuItemType.OBJECT &&
|
||||
isDefined(navigationMenuItem.targetObjectMetadataId)
|
||||
) {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
|
||||
return (
|
||||
objectMetadataItems.find(
|
||||
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
|
||||
) ?? null
|
||||
);
|
||||
return objectMetadataItem ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
|
||||
navigationMenuItem.type === NavigationMenuItemType.VIEW &&
|
||||
isDefined(navigationMenuItem.viewId)
|
||||
) {
|
||||
const view = views.find((view) => view.id === navigationMenuItem.viewId);
|
||||
if (!isDefined(view)) {
|
||||
return null;
|
||||
}
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === view.objectMetadataId,
|
||||
return (
|
||||
objectMetadataItems.find((meta) => meta.id === view.objectMetadataId) ??
|
||||
null
|
||||
);
|
||||
return objectMetadataItem ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.RECORD &&
|
||||
navigationMenuItem.type === NavigationMenuItemType.RECORD &&
|
||||
isDefined(navigationMenuItem.targetObjectMetadataId)
|
||||
) {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
|
||||
return (
|
||||
objectMetadataItems.find(
|
||||
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
|
||||
) ?? null
|
||||
);
|
||||
return objectMetadataItem ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
+7
-7
@@ -1,15 +1,15 @@
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
export const isLocationMatchingNavigationMenuItem = (
|
||||
currentPath: string,
|
||||
currentViewPath: string,
|
||||
navigationMenuItem: Pick<ProcessedNavigationMenuItem, 'itemType' | 'link'>,
|
||||
navigationMenuItemType: NavigationMenuItemType,
|
||||
computedLink: string,
|
||||
) => {
|
||||
const isViewBasedItem =
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW ||
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT;
|
||||
navigationMenuItemType === NavigationMenuItemType.VIEW ||
|
||||
navigationMenuItemType === NavigationMenuItemType.OBJECT;
|
||||
return isViewBasedItem
|
||||
? navigationMenuItem.link === currentViewPath
|
||||
: navigationMenuItem.link === currentPath;
|
||||
? computedLink === currentViewPath
|
||||
: computedLink === currentPath;
|
||||
};
|
||||
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import {
|
||||
computeNavigationMenuItemDisplayFields,
|
||||
type NavigationMenuItemDisplayFields,
|
||||
} from './computeNavigationMenuItemDisplayFields';
|
||||
import { isNavigationMenuItemLink } from './isNavigationMenuItemLink';
|
||||
|
||||
export { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
export type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
|
||||
export const sortNavigationMenuItems = (
|
||||
navigationMenuItems: NavigationMenuItem[],
|
||||
hasLinkToShowPage: boolean,
|
||||
views: Pick<View, 'id' | 'name' | 'objectMetadataId' | 'icon' | 'key'>[],
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
targetRecordIdentifiers: Map<string, ObjectRecordIdentifier>,
|
||||
): ProcessedNavigationMenuItem[] => {
|
||||
return navigationMenuItems
|
||||
.map((navigationMenuItem) => {
|
||||
if (
|
||||
navigationMenuItem.type === NavigationMenuItemType.OBJECT &&
|
||||
isDefined(navigationMenuItem.targetObjectMetadataId)
|
||||
) {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const indexView = views.find(
|
||||
(view) =>
|
||||
view.objectMetadataId === objectMetadataItem.id &&
|
||||
view.key === ViewKey.INDEX,
|
||||
);
|
||||
|
||||
const displayFields: NavigationMenuItemDisplayFields = {
|
||||
labelIdentifier: objectMetadataItem.labelPlural,
|
||||
avatarUrl: '',
|
||||
avatarType: 'icon',
|
||||
link: getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: objectMetadataItem.namePlural },
|
||||
indexView ? { viewId: indexView.id } : {},
|
||||
),
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
Icon: objectMetadataItem.icon ?? 'IconBox',
|
||||
};
|
||||
|
||||
return {
|
||||
...navigationMenuItem,
|
||||
...displayFields,
|
||||
viewKey: ViewKey.INDEX,
|
||||
itemType: NavigationMenuItemType.OBJECT,
|
||||
};
|
||||
}
|
||||
|
||||
if (isDefined(navigationMenuItem.viewId)) {
|
||||
const view = views.find(
|
||||
(view) => view.id === navigationMenuItem.viewId,
|
||||
);
|
||||
|
||||
if (isDefined(view)) {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === view.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const namePlural = objectMetadataItem.namePlural;
|
||||
|
||||
const isIndexView = view.key === ViewKey.INDEX;
|
||||
const labelIdentifier =
|
||||
isIndexView && isDefined(objectMetadataItem)
|
||||
? objectMetadataItem.labelPlural
|
||||
: view.name;
|
||||
const icon =
|
||||
isIndexView &&
|
||||
isDefined(objectMetadataItem) &&
|
||||
isDefined(objectMetadataItem.icon)
|
||||
? objectMetadataItem.icon
|
||||
: view.icon;
|
||||
|
||||
const displayFields: NavigationMenuItemDisplayFields = {
|
||||
labelIdentifier,
|
||||
avatarUrl: '',
|
||||
avatarType: 'icon',
|
||||
link: getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: namePlural },
|
||||
{ viewId: navigationMenuItem.viewId },
|
||||
),
|
||||
objectNameSingular: objectMetadataItem?.nameSingular ?? 'view',
|
||||
Icon: icon,
|
||||
};
|
||||
|
||||
return {
|
||||
...navigationMenuItem,
|
||||
...displayFields,
|
||||
viewKey: view.key,
|
||||
itemType: NavigationMenuItemType.VIEW,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isNavigationMenuItemLink(navigationMenuItem)) {
|
||||
const linkUrl = (navigationMenuItem.link ?? '').trim();
|
||||
const normalizedLink =
|
||||
linkUrl.startsWith('http://') || linkUrl.startsWith('https://')
|
||||
? linkUrl
|
||||
: `https://${linkUrl}`;
|
||||
const displayFields: NavigationMenuItemDisplayFields = {
|
||||
labelIdentifier: (navigationMenuItem.name ?? linkUrl) || 'Link',
|
||||
avatarUrl: '',
|
||||
avatarType: 'icon',
|
||||
link: normalizedLink,
|
||||
objectNameSingular: 'link',
|
||||
Icon: 'IconLink',
|
||||
};
|
||||
return {
|
||||
...navigationMenuItem,
|
||||
...displayFields,
|
||||
itemType: NavigationMenuItemType.LINK,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isDefined(navigationMenuItem.targetRecordId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === navigationMenuItem.targetObjectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectRecordIdentifier = targetRecordIdentifiers.get(
|
||||
navigationMenuItem.targetRecordId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectRecordIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayFields = computeNavigationMenuItemDisplayFields(
|
||||
objectMetadataItem,
|
||||
objectRecordIdentifier,
|
||||
);
|
||||
|
||||
if (!isDefined(displayFields)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...navigationMenuItem,
|
||||
...displayFields,
|
||||
link: hasLinkToShowPage ? displayFields.link : '',
|
||||
itemType: NavigationMenuItemType.RECORD,
|
||||
};
|
||||
})
|
||||
.filter(isDefined)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
};
|
||||
+49
-20
@@ -1,21 +1,29 @@
|
||||
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
|
||||
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
|
||||
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import {
|
||||
AppPath,
|
||||
CoreObjectNameSingular,
|
||||
NavigationMenuItemType,
|
||||
} from 'twenty-shared/types';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar, useIcons } from 'twenty-ui/display';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export type NavigationDrawerItemForObjectMetadataItemProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
navigationMenuItem?: ProcessedNavigationMenuItem;
|
||||
navigationMenuItem?: NavigationMenuItem;
|
||||
isSelectedInEditMode?: boolean;
|
||||
onEditModeClick?: () => void;
|
||||
onActiveItemClickWhenNotInEditMode?: () => void;
|
||||
@@ -36,6 +44,8 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
|
||||
lastVisitedViewPerObjectMetadataItemState,
|
||||
);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const lastVisitedViewId =
|
||||
lastVisitedViewPerObjectMetadataItem?.[objectMetadataItem.id];
|
||||
@@ -48,24 +58,28 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
const currentPath = location.pathname;
|
||||
const currentPathWithSearch = `${location.pathname}${location.search}`;
|
||||
|
||||
const isRecord =
|
||||
navigationMenuItem?.itemType === NavigationMenuItemType.RECORD;
|
||||
const isView = navigationMenuItem?.itemType === NavigationMenuItemType.VIEW;
|
||||
const isObject =
|
||||
navigationMenuItem?.itemType === NavigationMenuItemType.OBJECT;
|
||||
const isRecord = navigationMenuItem?.type === NavigationMenuItemType.RECORD;
|
||||
const isView = navigationMenuItem?.type === NavigationMenuItemType.VIEW;
|
||||
const isObject = navigationMenuItem?.type === NavigationMenuItemType.OBJECT;
|
||||
const hasCustomLink = isRecord || isView || isObject;
|
||||
|
||||
const navigationPath = hasCustomLink
|
||||
? navigationMenuItem!.link
|
||||
? getNavigationMenuItemComputedLink(
|
||||
navigationMenuItem!,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
)
|
||||
: getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: objectMetadataItem.namePlural },
|
||||
lastVisitedViewId ? { viewId: lastVisitedViewId } : undefined,
|
||||
);
|
||||
|
||||
const computedLink = hasCustomLink ? navigationPath : '';
|
||||
|
||||
const isActive = hasCustomLink
|
||||
? (isView || isObject ? currentPathWithSearch : currentPath) ===
|
||||
navigationMenuItem!.link
|
||||
computedLink
|
||||
: currentPath ===
|
||||
getAppPath(AppPath.RecordIndexPage, {
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
@@ -81,17 +95,32 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
|
||||
const shouldNavigate = !isNavigationMenuInEditMode;
|
||||
|
||||
const view = isDefined(navigationMenuItem?.viewId)
|
||||
? views.find((view) => view.id === navigationMenuItem!.viewId)
|
||||
: undefined;
|
||||
const viewKey = view?.key ?? null;
|
||||
|
||||
const isViewWithCustomName =
|
||||
isView &&
|
||||
navigationMenuItem?.viewKey !== ViewKey.INDEX &&
|
||||
isDefined(navigationMenuItem?.labelIdentifier);
|
||||
isView && viewKey !== ViewKey.INDEX && isDefined(view);
|
||||
|
||||
const itemLabel = isDefined(navigationMenuItem)
|
||||
? getNavigationMenuItemLabel(navigationMenuItem, objectMetadataItems, views)
|
||||
: objectMetadataItem.labelPlural;
|
||||
|
||||
const label = isRecord
|
||||
? navigationMenuItem!.labelIdentifier
|
||||
? itemLabel
|
||||
: isViewWithCustomName
|
||||
? navigationMenuItem!.labelIdentifier
|
||||
? itemLabel
|
||||
: objectMetadataItem.labelPlural;
|
||||
|
||||
const recordIdentifier =
|
||||
isRecord && isDefined(navigationMenuItem?.targetRecordIdentifier)
|
||||
? recordIdentifierToObjectRecordIdentifier({
|
||||
recordIdentifier: navigationMenuItem!.targetRecordIdentifier!,
|
||||
objectMetadataItem,
|
||||
})
|
||||
: null;
|
||||
|
||||
const Icon = isRecord
|
||||
? () => (
|
||||
<Avatar
|
||||
@@ -100,16 +129,16 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
? 'squared'
|
||||
: 'rounded'
|
||||
}
|
||||
avatarUrl={navigationMenuItem!.avatarUrl}
|
||||
avatarUrl={recordIdentifier?.avatarUrl}
|
||||
placeholderColorSeed={navigationMenuItem!.targetRecordId ?? undefined}
|
||||
placeholder={navigationMenuItem!.labelIdentifier}
|
||||
placeholder={itemLabel}
|
||||
/>
|
||||
)
|
||||
: isViewWithCustomName && isDefined(navigationMenuItem?.Icon)
|
||||
: isViewWithCustomName && isDefined(view?.icon)
|
||||
? () => (
|
||||
<ObjectIconWithViewOverlay
|
||||
ObjectIcon={getIcon(objectMetadataItem.icon)}
|
||||
ViewIcon={getIcon(navigationMenuItem!.Icon!)}
|
||||
ViewIcon={getIcon(view!.icon)}
|
||||
objectColor={objectNavItemColor}
|
||||
/>
|
||||
)
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { NavigationDrawerSectionForWorkspaceItemFolderContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemFolderContent';
|
||||
import { NavigationDrawerSectionForWorkspaceItemLinkContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemLinkContent';
|
||||
import { NavigationDrawerSectionForWorkspaceItemObjectContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemObjectContent';
|
||||
@@ -18,7 +18,7 @@ export const NavigationDrawerSectionForWorkspaceItemContent = ({
|
||||
onActiveObjectMetadataItemClick,
|
||||
readOnly,
|
||||
}: NavigationDrawerSectionForWorkspaceItemContentProps) => {
|
||||
switch (item.itemType) {
|
||||
switch (item.type) {
|
||||
case NavigationMenuItemType.FOLDER:
|
||||
return (
|
||||
<NavigationDrawerSectionForWorkspaceItemFolderContent
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ export const NavigationDrawerSectionForWorkspaceItemFolderContent = ({
|
||||
}: NavigationDrawerSectionForWorkspaceItemFolderContentProps) => {
|
||||
const folderId = item.id;
|
||||
const folderName = item.name ?? 'Folder';
|
||||
const folderIconKey = item.Icon;
|
||||
const folderIconKey = item.icon;
|
||||
const folderColor = 'color' in item ? item.color : undefined;
|
||||
const navigationMenuItems = folderChildrenById.get(item.id) ?? [];
|
||||
const isGroup = folderCount > 1;
|
||||
|
||||
+17
-5
@@ -1,9 +1,12 @@
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { IconArrowUpRight } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
@@ -18,15 +21,24 @@ export const NavigationDrawerSectionForWorkspaceItemLinkContent = ({
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const linkItem = item as ProcessedNavigationMenuItem;
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const label = getNavigationMenuItemLabel(item, objectMetadataItems, views);
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigationDrawerItem
|
||||
label={linkItem.labelIdentifier}
|
||||
to={isNavigationMenuInEditMode || isDragging ? undefined : linkItem.link}
|
||||
label={label}
|
||||
to={isNavigationMenuInEditMode || isDragging ? undefined : computedLink}
|
||||
onClick={
|
||||
isNavigationMenuInEditMode ? editModeProps.onEditModeClick : undefined
|
||||
}
|
||||
Icon={() => <NavigationMenuItemIcon navigationMenuItem={linkItem} />}
|
||||
Icon={() => <NavigationMenuItemIcon navigationMenuItem={item} />}
|
||||
active={false}
|
||||
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
|
||||
isDragging={isDragging}
|
||||
|
||||
+2
-3
@@ -1,5 +1,4 @@
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
|
||||
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
@@ -18,7 +17,7 @@ export const NavigationDrawerSectionForWorkspaceItemObjectContent = ({
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
|
||||
item as ProcessedNavigationMenuItem,
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
@@ -28,7 +27,7 @@ export const NavigationDrawerSectionForWorkspaceItemObjectContent = ({
|
||||
return (
|
||||
<NavigationDrawerItemForObjectMetadataItem
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
navigationMenuItem={item as ProcessedNavigationMenuItem}
|
||||
navigationMenuItem={item}
|
||||
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
|
||||
onEditModeClick={editModeProps.onEditModeClick}
|
||||
isDragging={isDragging}
|
||||
|
||||
+29
-29
@@ -1,17 +1,14 @@
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
import React, { lazy, Suspense, useContext } from 'react';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import {
|
||||
type FlatWorkspaceItem,
|
||||
type NavigationMenuItemClickParams,
|
||||
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
|
||||
import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly';
|
||||
import { WorkspaceSectionListEditModeFallback } from '@/object-metadata/components/WorkspaceSectionListEditModeFallback';
|
||||
@@ -40,7 +37,7 @@ const StyledWorkspaceSectionContentGapOffset = styled.div`
|
||||
|
||||
type NavigationDrawerSectionForWorkspaceItemsProps = {
|
||||
sectionTitle: string;
|
||||
items: FlatWorkspaceItem[];
|
||||
items: NavigationMenuItem[];
|
||||
rightIcon?: React.ReactNode;
|
||||
selectedNavigationMenuItemId?: string | null;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
@@ -75,33 +72,34 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
||||
const isAddToNavigationDropTargetVisible =
|
||||
addToNavigationFallbackDestination?.droppableId ===
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS;
|
||||
const folderChildrenById = items.reduce<
|
||||
Map<string, ProcessedNavigationMenuItem[]>
|
||||
>((acc, item) => {
|
||||
const folderId = item.folderId;
|
||||
if (isDefined(folderId)) {
|
||||
const children = acc.get(folderId) ?? [];
|
||||
children.push(item as ProcessedNavigationMenuItem);
|
||||
acc.set(folderId, children);
|
||||
}
|
||||
return acc;
|
||||
}, new Map());
|
||||
const folderChildrenById = items.reduce<Map<string, NavigationMenuItem[]>>(
|
||||
(acc, item) => {
|
||||
const folderId = item.folderId;
|
||||
if (isDefined(folderId)) {
|
||||
const children = acc.get(folderId) ?? [];
|
||||
children.push(item);
|
||||
acc.set(folderId, children);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
new Map(),
|
||||
);
|
||||
|
||||
const filteredItems = flatItems.filter((item) => {
|
||||
const type = item.itemType;
|
||||
const itemType = item.type;
|
||||
if (
|
||||
type === NavigationMenuItemType.FOLDER ||
|
||||
type === NavigationMenuItemType.LINK
|
||||
itemType === NavigationMenuItemType.FOLDER ||
|
||||
itemType === NavigationMenuItemType.LINK
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
type === NavigationMenuItemType.OBJECT ||
|
||||
type === NavigationMenuItemType.VIEW ||
|
||||
type === NavigationMenuItemType.RECORD
|
||||
itemType === NavigationMenuItemType.OBJECT ||
|
||||
itemType === NavigationMenuItemType.VIEW ||
|
||||
itemType === NavigationMenuItemType.RECORD
|
||||
) {
|
||||
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
|
||||
item as ProcessedNavigationMenuItem,
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
@@ -116,17 +114,19 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
||||
return false;
|
||||
});
|
||||
|
||||
const getEditModeProps = (item: FlatWorkspaceItem): EditModeProps => {
|
||||
const getEditModeProps = (item: NavigationMenuItem): EditModeProps => {
|
||||
const itemId = item.id;
|
||||
return {
|
||||
isSelectedInEditMode: selectedNavigationMenuItemId === itemId,
|
||||
onEditModeClick: onNavigationMenuItemClick
|
||||
? () => {
|
||||
const type = item.itemType;
|
||||
const itemType = item.type;
|
||||
const objectMetadataItem =
|
||||
type === 'OBJECT' || type === 'VIEW' || type === 'RECORD'
|
||||
itemType === 'OBJECT' ||
|
||||
itemType === 'VIEW' ||
|
||||
itemType === 'RECORD'
|
||||
? getObjectMetadataForNavigationMenuItem(
|
||||
item as ProcessedNavigationMenuItem,
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
)
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { WorkspaceDndKitDroppableSlot } from '@/navigation-menu-item/components/WorkspaceDndKitDroppableSlot';
|
||||
import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/components/WorkspaceDndKitSortableItem';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
@@ -46,7 +46,7 @@ export const WorkspaceSectionListDndKit = ({
|
||||
NavigationDropTargetContext,
|
||||
);
|
||||
const folderCount = filteredItems.filter(
|
||||
(item) => item.itemType === NavigationMenuItemType.FOLDER,
|
||||
(item) => item.type === NavigationMenuItemType.FOLDER,
|
||||
).length;
|
||||
const isAddMenuItemButtonVisible = isNavigationMenuInEditMode;
|
||||
return (
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import type { FlatWorkspaceItem } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps';
|
||||
import { NavigationDrawerSectionForWorkspaceItemContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent';
|
||||
|
||||
@@ -29,12 +29,12 @@ export const NavigationDrawerSectionForWorkspaceItemsListReadOnly = ({
|
||||
onActiveObjectMetadataItemClick,
|
||||
}: NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps) => {
|
||||
const folderCount = filteredItems.filter(
|
||||
(item) => item.itemType === NavigationMenuItemType.FOLDER,
|
||||
(item) => item.type === NavigationMenuItemType.FOLDER,
|
||||
).length;
|
||||
|
||||
return (
|
||||
<StyledList>
|
||||
{filteredItems.map((item: FlatWorkspaceItem) => (
|
||||
{filteredItems.map((item: NavigationMenuItem) => (
|
||||
<NavigationDrawerSectionForWorkspaceItemContent
|
||||
key={item.id}
|
||||
item={item}
|
||||
|
||||
+2
-2
@@ -5,12 +5,12 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { WorkspaceNavigationMenuItemFolderSubItem } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/hooks/useWorkspaceFolderOpenState';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
|
||||
@@ -35,7 +35,7 @@ type WorkspaceFolderReadOnlyProps = {
|
||||
folderName: string;
|
||||
folderIconKey?: string | null;
|
||||
folderColor?: string | null;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
isGroup: boolean;
|
||||
};
|
||||
|
||||
|
||||
+4
-7
@@ -1,17 +1,14 @@
|
||||
import type {
|
||||
FlatWorkspaceItem,
|
||||
NavigationMenuItemClickParams,
|
||||
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
|
||||
|
||||
export type WorkspaceSectionItemContentProps = {
|
||||
item: FlatWorkspaceItem;
|
||||
item: NavigationMenuItem;
|
||||
editModeProps: EditModeProps;
|
||||
isDragging: boolean;
|
||||
folderChildrenById: Map<string, ProcessedNavigationMenuItem[]>;
|
||||
folderChildrenById: Map<string, NavigationMenuItem[]>;
|
||||
folderCount: number;
|
||||
selectedNavigationMenuItemId: string | null;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
|
||||
+5
-8
@@ -1,16 +1,13 @@
|
||||
import type {
|
||||
FlatWorkspaceItem,
|
||||
NavigationMenuItemClickParams,
|
||||
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
|
||||
|
||||
export type WorkspaceSectionListDndKitProps = {
|
||||
filteredItems: FlatWorkspaceItem[];
|
||||
getEditModeProps: (item: FlatWorkspaceItem) => EditModeProps;
|
||||
folderChildrenById: Map<string, ProcessedNavigationMenuItem[]>;
|
||||
filteredItems: NavigationMenuItem[];
|
||||
getEditModeProps: (item: NavigationMenuItem) => EditModeProps;
|
||||
folderChildrenById: Map<string, NavigationMenuItem[]>;
|
||||
selectedNavigationMenuItemId: string | null;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
onActiveObjectMetadataItemClick?: (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoSta
|
||||
import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState';
|
||||
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useUpdateFolderInDraft } from '@/navigation-menu-item/hooks/useUpdateFolderInDraft';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
@@ -41,7 +41,7 @@ export const SidePanelFolderInfo = () => {
|
||||
const selectedItem = selectedNavigationMenuItemInEditMode
|
||||
? items.find(
|
||||
(item) =>
|
||||
item.itemType === NavigationMenuItemType.FOLDER &&
|
||||
item.type === NavigationMenuItemType.FOLDER &&
|
||||
item.id === selectedNavigationMenuItemInEditMode,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useLingui } from '@lingui/react/macro';
|
||||
import { IconLink, IconWorld } from 'twenty-ui/display';
|
||||
|
||||
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
@@ -33,7 +33,7 @@ export const SidePanelLinkInfo = () => {
|
||||
const selectedItem = selectedNavigationMenuItemInEditMode
|
||||
? items.find(
|
||||
(item) =>
|
||||
item.itemType === NavigationMenuItemType.LINK &&
|
||||
item.type === NavigationMenuItemType.LINK &&
|
||||
item.id === selectedNavigationMenuItemInEditMode,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
+8
-8
@@ -3,7 +3,7 @@ import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
|
||||
import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
|
||||
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
|
||||
@@ -15,12 +15,12 @@ export const SidePanelObjectViewRecordInfo = () => {
|
||||
const { selectedItemObjectMetadata } =
|
||||
useSelectedNavigationMenuItemEditItemObjectMetadata();
|
||||
|
||||
const processedItem =
|
||||
selectedItem && selectedItem.itemType !== NavigationMenuItemType.FOLDER
|
||||
const navItem =
|
||||
selectedItem && selectedItem.type !== NavigationMenuItemType.FOLDER
|
||||
? selectedItem
|
||||
: undefined;
|
||||
|
||||
if (!processedItem || !selectedItemLabel) {
|
||||
if (!navItem || !selectedItemLabel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -28,22 +28,22 @@ export const SidePanelObjectViewRecordInfo = () => {
|
||||
NavigationMenuItemType.OBJECT,
|
||||
NavigationMenuItemType.VIEW,
|
||||
NavigationMenuItemType.RECORD,
|
||||
].includes(processedItem.itemType);
|
||||
].includes(navItem.type);
|
||||
|
||||
if (!isObjectViewOrRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label =
|
||||
processedItem.itemType === NavigationMenuItemType.RECORD
|
||||
navItem.type === NavigationMenuItemType.RECORD
|
||||
? selectedItemObjectMetadata?.labelSingular
|
||||
: processedItem.itemType === NavigationMenuItemType.OBJECT
|
||||
: navItem.type === NavigationMenuItemType.OBJECT
|
||||
? t`Object`
|
||||
: t`View`;
|
||||
|
||||
return (
|
||||
<SidePanelPageInfoLayout
|
||||
icon={<NavigationMenuItemIcon navigationMenuItem={processedItem} />}
|
||||
icon={<NavigationMenuItemIcon navigationMenuItem={navItem} />}
|
||||
title={<OverflowingTextWithTooltip text={selectedItemLabel} />}
|
||||
label={label}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { SidePanelAskAIInfo } from '@/side-panel/components/SidePanelAskAIInfo';
|
||||
@@ -18,7 +18,6 @@ import { SidePanelPageLayoutInfo } from '@/side-panel/components/SidePanelPageLa
|
||||
import { SidePanelRecordInfo } from '@/side-panel/components/SidePanelRecordInfo';
|
||||
import { SidePanelWorkflowStepInfo } from '@/side-panel/components/SidePanelWorkflowStepInfo';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
|
||||
import { type SidePanelContextChipProps } from '@/side-panel/components/SidePanelContextChip';
|
||||
import { useContext } from 'react';
|
||||
@@ -51,20 +50,20 @@ export const SidePanelPageInfo = ({ pageChip }: SidePanelPageInfoProps) => {
|
||||
: undefined;
|
||||
|
||||
if (isNavigationMenuItemEditPage && isDefined(selectedNavItem)) {
|
||||
const itemType = selectedNavItem.itemType;
|
||||
const navItemType = selectedNavItem.type;
|
||||
|
||||
if (itemType === NavigationMenuItemType.FOLDER) {
|
||||
if (navItemType === NavigationMenuItemType.FOLDER) {
|
||||
return <SidePanelFolderInfo />;
|
||||
}
|
||||
|
||||
if (itemType === NavigationMenuItemType.LINK) {
|
||||
if (navItemType === NavigationMenuItemType.LINK) {
|
||||
return <SidePanelLinkInfo />;
|
||||
}
|
||||
|
||||
if (
|
||||
itemType === NavigationMenuItemType.OBJECT ||
|
||||
itemType === NavigationMenuItemType.VIEW ||
|
||||
itemType === NavigationMenuItemType.RECORD
|
||||
navItemType === NavigationMenuItemType.OBJECT ||
|
||||
navItemType === NavigationMenuItemType.VIEW ||
|
||||
navItemType === NavigationMenuItemType.RECORD
|
||||
) {
|
||||
return <SidePanelObjectViewRecordInfo />;
|
||||
}
|
||||
|
||||
+3
-3
@@ -2,8 +2,8 @@ import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { getAbsoluteUrl } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { extractDomainFromUrl } from '@/navigation-menu-item/utils/extractDomainFromUrl';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
@@ -16,7 +16,7 @@ import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigati
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
type SidePanelEditLinkItemViewProps = OrganizeActionsProps & {
|
||||
selectedItem: ProcessedNavigationMenuItem;
|
||||
selectedItem: NavigationMenuItem;
|
||||
onUpdateLink: (
|
||||
linkId: string,
|
||||
updates: { link?: string; name?: string },
|
||||
@@ -78,7 +78,7 @@ export const SidePanelEditLinkItemView = ({
|
||||
<TextInput
|
||||
fullWidth
|
||||
placeholder="www.google.com"
|
||||
value={urlEditInput || selectedItem.link}
|
||||
value={urlEditInput || selectedItem.link || ''}
|
||||
onChange={handleUrlChange}
|
||||
onBlur={handleUrlBlur}
|
||||
/>
|
||||
|
||||
+17
-5
@@ -1,8 +1,9 @@
|
||||
import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
|
||||
import { navigationMenuItemsSelector } from '@/navigation-menu-item/states/navigationMenuItemsSelector';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
|
||||
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
|
||||
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { SidePanelEditColorOption } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditColorOption';
|
||||
@@ -12,14 +13,16 @@ import {
|
||||
} from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
|
||||
import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
type SidePanelEditObjectViewBaseProps = OrganizeActionsProps & {
|
||||
onOpenFolderPicker: () => void;
|
||||
showColorOption?: boolean;
|
||||
selectedItem?: ProcessedNavigationMenuItem | null;
|
||||
selectedItem?: NavigationMenuItem | null;
|
||||
};
|
||||
|
||||
export const SidePanelEditObjectViewBase = ({
|
||||
@@ -36,9 +39,18 @@ export const SidePanelEditObjectViewBase = ({
|
||||
}: SidePanelEditObjectViewBaseProps) => {
|
||||
const { t } = useLingui();
|
||||
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
|
||||
const objectColor = useObjectNavItemColor(
|
||||
selectedItem?.objectNameSingular ?? '',
|
||||
);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const objectNameSingular = isDefined(selectedItem)
|
||||
? (getNavigationMenuItemObjectNameSingular(
|
||||
selectedItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
) ?? '')
|
||||
: '';
|
||||
|
||||
const objectColor = useObjectNavItemColor(objectNameSingular);
|
||||
|
||||
const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector);
|
||||
const persistedNavItem = navigationMenuItems.find(
|
||||
|
||||
+5
-9
@@ -1,12 +1,11 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/hooks/useOpenAddItemToFolderPage';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
|
||||
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
@@ -46,7 +45,7 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
);
|
||||
const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel();
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const selectedItemType = selectedItem?.itemType ?? null;
|
||||
const selectedItemType = selectedItem?.type ?? null;
|
||||
|
||||
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
|
||||
const openFolderPicker = () =>
|
||||
@@ -67,10 +66,7 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
|
||||
const handleAddItemToFolder = () => {
|
||||
if (
|
||||
!selectedItem ||
|
||||
selectedItem.itemType !== NavigationMenuItemType.FOLDER
|
||||
) {
|
||||
if (!selectedItem || selectedItem.type !== NavigationMenuItemType.FOLDER) {
|
||||
return;
|
||||
}
|
||||
const folderItemCount = workspaceNavigationMenuItems.filter(
|
||||
@@ -106,7 +102,7 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
onAddBefore={onAddBefore}
|
||||
onAddAfter={onAddAfter}
|
||||
showColorOption={isDefined(selectedItem)}
|
||||
selectedItem={selectedItem as ProcessedNavigationMenuItem | undefined}
|
||||
selectedItem={selectedItem}
|
||||
/>
|
||||
);
|
||||
case NavigationMenuItemType.VIEW:
|
||||
@@ -125,7 +121,7 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
case NavigationMenuItemType.LINK:
|
||||
if (
|
||||
isDefined(selectedItem) &&
|
||||
selectedItem.itemType === NavigationMenuItemType.LINK
|
||||
selectedItem.type === NavigationMenuItemType.LINK
|
||||
) {
|
||||
return (
|
||||
<SidePanelEditLinkItemView
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
|
||||
import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
|
||||
+4
-2
@@ -1,6 +1,5 @@
|
||||
import { Avatar, IconFolder, useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddRecordToNavigationMenuDraft';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
@@ -11,7 +10,10 @@ import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/Si
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import {
|
||||
CoreObjectNameSingular,
|
||||
NavigationMenuItemType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
type SearchRecord = {
|
||||
recordId: string;
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddViewToNavigationMenuDraft';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
|
||||
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useDraftNavigationMenuItemsAllFolders } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItemsAllFolders';
|
||||
import { useDraftNavigationMenuItemsWorkspaceFolders } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItemsWorkspaceFolders';
|
||||
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove';
|
||||
@@ -45,7 +45,7 @@ export const useFolderPickerSelectionData = () => {
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const selectedItemType = selectedItem?.itemType ?? null;
|
||||
const selectedItemType = selectedItem?.type ?? null;
|
||||
const { allFolders } = useDraftNavigationMenuItemsAllFolders();
|
||||
const { workspaceFolders } = useDraftNavigationMenuItemsWorkspaceFolders();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user