Created a breadcrumb for left nav menu sub items (#6762)
Closes https://github.com/twentyhq/twenty/issues/6484 <img width="270" alt="image" src="https://github.com/user-attachments/assets/3cfd7a5a-5239-4998-87f7-a9b45e3b5229">
This commit is contained in:
@ -1,11 +1,10 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined, useIcons } from 'twenty-ui';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
|
||||
import { ObjectMetadataNavItemsSkeletonLoader } from '@/object-metadata/components/ObjectMetadataNavItemsSkeletonLoader';
|
||||
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||
@ -15,9 +14,9 @@ import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/compo
|
||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||
import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState';
|
||||
import { View } from '@/views/types/View';
|
||||
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
|
||||
import { Theme, useTheme } from '@emotion/react';
|
||||
|
||||
const ORDERED_STANDARD_OBJECTS = [
|
||||
'person',
|
||||
@ -27,20 +26,11 @@ const ORDERED_STANDARD_OBJECTS = [
|
||||
'note',
|
||||
];
|
||||
|
||||
const navItemsAnimationVariants = (theme: Theme) => ({
|
||||
hidden: {
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
marginTop: 0,
|
||||
},
|
||||
visible: {
|
||||
height: 'auto',
|
||||
opacity: 1,
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
});
|
||||
|
||||
export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => {
|
||||
export const NavigationDrawerSectionForObjectMetadataItems = ({
|
||||
isRemote,
|
||||
}: {
|
||||
isRemote: boolean;
|
||||
}) => {
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
|
||||
const { toggleNavigationSection, isNavigationSectionOpenState } =
|
||||
@ -57,13 +47,13 @@ export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => {
|
||||
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
||||
const loading = useIsPrefetchLoading();
|
||||
|
||||
const theme = useTheme();
|
||||
const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView();
|
||||
|
||||
if (loading && isDefined(currentUser)) {
|
||||
return <ObjectMetadataNavItemsSkeletonLoader />;
|
||||
return <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />;
|
||||
}
|
||||
|
||||
// TODO: refactor this by splitting into separate components
|
||||
return (
|
||||
filteredActiveObjectMetadataItems.length > 0 && (
|
||||
<NavigationDrawerSection>
|
||||
@ -121,6 +111,17 @@ export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => {
|
||||
currentPath === `/objects/${objectMetadataItem.namePlural}` &&
|
||||
objectMetadataViews.length > 1;
|
||||
|
||||
const sortedObjectMetadataViews = [...objectMetadataViews].sort(
|
||||
(viewA, viewB) =>
|
||||
viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position,
|
||||
);
|
||||
|
||||
const selectedSubItemIndex = sortedObjectMetadataViews.findIndex(
|
||||
(view) => viewId === view.id,
|
||||
);
|
||||
|
||||
const subItemArrayLength = sortedObjectMetadataViews.length;
|
||||
|
||||
return (
|
||||
<div key={objectMetadataItem.id}>
|
||||
<NavigationDrawerItem
|
||||
@ -132,33 +133,21 @@ export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => {
|
||||
currentPath === `/objects/${objectMetadataItem.namePlural}`
|
||||
}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{shouldSubItemsBeDisplayed && (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
variants={navItemsAnimationVariants(theme)}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
>
|
||||
{objectMetadataViews
|
||||
.sort((viewA, viewB) =>
|
||||
viewA.key === 'INDEX'
|
||||
? -1
|
||||
: viewA.position - viewB.position,
|
||||
)
|
||||
.map((view) => (
|
||||
<NavigationDrawerSubItem
|
||||
label={view.name}
|
||||
to={`/objects/${objectMetadataItem.namePlural}?view=${view.id}`}
|
||||
active={viewId === view.id}
|
||||
Icon={getIcon(view.icon)}
|
||||
key={view.id}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{shouldSubItemsBeDisplayed &&
|
||||
sortedObjectMetadataViews.map((view, index) => (
|
||||
<NavigationDrawerSubItem
|
||||
label={view.name}
|
||||
to={`/objects/${objectMetadataItem.namePlural}?view=${view.id}`}
|
||||
active={viewId === view.id}
|
||||
subItemState={getNavigationSubItemState({
|
||||
index,
|
||||
arrayLength: subItemArrayLength,
|
||||
selectedIndex: selectedSubItemIndex,
|
||||
})}
|
||||
Icon={getIcon(view.icon)}
|
||||
key={view.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -0,0 +1,29 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
|
||||
const StyledSkeletonColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: 76px;
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader: React.FC =
|
||||
() => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.light}
|
||||
borderRadius={4}
|
||||
>
|
||||
<StyledSkeletonColumn>
|
||||
<Skeleton width={196} height={16} />
|
||||
<Skeleton width={196} height={16} />
|
||||
<Skeleton width={196} height={16} />
|
||||
</StyledSkeletonColumn>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
||||
@ -1,28 +0,0 @@
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledSkeletonColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: 76px;
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const ObjectMetadataNavItemsSkeletonLoader: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.light}
|
||||
borderRadius={4}
|
||||
>
|
||||
<StyledSkeletonColumn>
|
||||
<Skeleton width={196} height={16} />
|
||||
<Skeleton width={196} height={16} />
|
||||
<Skeleton width={196} height={16} />
|
||||
</StyledSkeletonColumn>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
||||
@ -9,12 +9,12 @@ import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadat
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
|
||||
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
|
||||
import { PrefetchLoadedDecorator } from '~/testing/decorators/PrefetchLoadedDecorator';
|
||||
import { ObjectMetadataNavItems } from '../ObjectMetadataNavItems';
|
||||
|
||||
const meta: Meta<typeof ObjectMetadataNavItems> = {
|
||||
title: 'Modules/ObjectMetadata/ObjectMetadataNavItems',
|
||||
component: ObjectMetadataNavItems,
|
||||
const meta: Meta<typeof NavigationDrawerSectionForObjectMetadataItems> = {
|
||||
title: 'Modules/ObjectMetadata/NavigationDrawerSectionForObjectMetadataItems',
|
||||
component: NavigationDrawerSectionForObjectMetadataItems,
|
||||
decorators: [
|
||||
IconsProviderDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
@ -29,7 +29,7 @@ const meta: Meta<typeof ObjectMetadataNavItems> = {
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ObjectMetadataNavItems>;
|
||||
type Story = StoryObj<typeof NavigationDrawerSectionForObjectMetadataItems>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async () => {
|
||||
|
||||
Reference in New Issue
Block a user