From d3d777ee2832173322c1a429b1f41cd3be9057ae Mon Sep 17 00:00:00 2001 From: spaenleh Date: Fri, 10 Jan 2025 08:05:52 +0100 Subject: [PATCH 1/3] fix: start working on the Navigator component --- .../ui/Navigator/CurrentItemNavigation.tsx | 56 ++++++ .../ui/Navigator/ExtraItemsMenu.tsx | 78 ++++++++ .../ui/Navigator/ExtraItemsNavigation.tsx | 40 +++++ src/components/ui/Navigator/HomeMenu.tsx | 98 +++++++++++ src/components/ui/Navigator/ItemMenu.tsx | 101 +++++++++++ .../ui/Navigator/Navigator.stories.tsx | 166 ++++++++++++++++++ src/components/ui/Navigator/Navigator.tsx | 85 +++++++++ .../ui/Navigator/ParentsNavigation.tsx | 48 +++++ .../Navigator/common/CenterAlignWrapper.tsx | 8 + .../ui/Navigator/common/NavigationLink.tsx | 14 ++ .../ui/Navigator/common/constants.ts | 1 + 11 files changed, 695 insertions(+) create mode 100644 src/components/ui/Navigator/CurrentItemNavigation.tsx create mode 100644 src/components/ui/Navigator/ExtraItemsMenu.tsx create mode 100644 src/components/ui/Navigator/ExtraItemsNavigation.tsx create mode 100644 src/components/ui/Navigator/HomeMenu.tsx create mode 100644 src/components/ui/Navigator/ItemMenu.tsx create mode 100644 src/components/ui/Navigator/Navigator.stories.tsx create mode 100644 src/components/ui/Navigator/Navigator.tsx create mode 100644 src/components/ui/Navigator/ParentsNavigation.tsx create mode 100644 src/components/ui/Navigator/common/CenterAlignWrapper.tsx create mode 100644 src/components/ui/Navigator/common/NavigationLink.tsx create mode 100644 src/components/ui/Navigator/common/constants.ts diff --git a/src/components/ui/Navigator/CurrentItemNavigation.tsx b/src/components/ui/Navigator/CurrentItemNavigation.tsx new file mode 100644 index 000000000..833cd3688 --- /dev/null +++ b/src/components/ui/Navigator/CurrentItemNavigation.tsx @@ -0,0 +1,56 @@ +import { Typography } from '@mui/material'; + +import { DiscriminatedItem, ItemType } from '@graasp/sdk'; + +import ItemMenu, { ItemMenuProps } from './ItemMenu.js'; +import CenterAlignWrapper from './common/CenterAlignWrapper.js'; +import NavigationLink from './common/NavigationLink.js'; +import { ITEM_NAME_MAX_LENGTH } from './common/constants.js'; + +export type CurrentItemProps = { + item: DiscriminatedItem; + buildBreadcrumbsItemLinkId?: (id: string) => string; + buildIconId?: (id: string) => string; + buildMenuId?: (id: string) => string; + buildMenuItemId?: (id: string) => string; + useChildren: ItemMenuProps['useChildren']; + buildToItemPath: (id: string) => LinkProps['to']; + showArrow: boolean; +}; +const CurrentItemNavigation = ({ + item, + buildBreadcrumbsItemLinkId, + buildToItemPath, + useChildren, + buildIconId, + buildMenuId, + buildMenuItemId, + showArrow, +}: CurrentItemProps): JSX.Element | null => { + return ( + + + + {truncate(item.name, { length: ITEM_NAME_MAX_LENGTH })} + + + {(item.type === ItemType.FOLDER || showArrow) && ( + + )} + + ); +}; + +export default CurrentItemNavigation; diff --git a/src/components/ui/Navigator/ExtraItemsMenu.tsx b/src/components/ui/Navigator/ExtraItemsMenu.tsx new file mode 100644 index 000000000..c789d0aad --- /dev/null +++ b/src/components/ui/Navigator/ExtraItemsMenu.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; + +import { + IconButton, + IconButtonProps, + Menu, + MenuItem, + Typography, +} from '@mui/material'; + +import { ChevronRightIcon } from 'lucide-react'; + +import { MenuItemType } from './Navigator.js'; + +export type ExtraItemsMenuProps = { + icon?: JSX.Element; + menuItems: MenuItemType[]; + buildIconId?: (id: string) => string; + buildMenuId?: (itemId: string) => string; + name: string; +}; + +const Separator = ; + +const ExtraItemsMenu = ({ + icon = Separator, + menuItems, + buildIconId, + buildMenuId, + name, +}: ExtraItemsMenuProps): JSX.Element => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const handleClick: IconButtonProps['onClick'] = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = (): void => { + setAnchorEl(null); + }; + + return ( + <> + + {icon} + + + {menuItems?.map(({ name, path }) => ( + + {name} + + ))} + + + ); +}; + +export default ExtraItemsMenu; diff --git a/src/components/ui/Navigator/ExtraItemsNavigation.tsx b/src/components/ui/Navigator/ExtraItemsNavigation.tsx new file mode 100644 index 000000000..93437e992 --- /dev/null +++ b/src/components/ui/Navigator/ExtraItemsNavigation.tsx @@ -0,0 +1,40 @@ +import { Box, Typography } from '@mui/material'; + +import truncate from 'lodash.truncate'; + +import ExtraItemsMenu from './ExtraItemsMenu.js'; +import { MenuItemType } from './Navigator.js'; +import CenterAlignWrapper from './common/CenterAlignWrapper.js'; +import NavigationLink from './common/NavigationLink.js'; +import { ITEM_NAME_MAX_LENGTH } from './common/constants.js'; + +export interface ExtraItem { + name: string; + path: string; + icon?: JSX.Element; + menuItems?: MenuItemType[]; +} + +const ExtraItemsNavigation = ({ + extraItems, +}: { + extraItems: ExtraItem[]; +}): JSX.Element[] | null => { + return extraItems.map(({ icon, name, path, menuItems }) => ( + + + {icon} + + + {truncate(name, { length: ITEM_NAME_MAX_LENGTH })} + + + + {menuItems && menuItems.length > 0 && ( + + )} + + )); +}; + +export default ExtraItemsNavigation; diff --git a/src/components/ui/Navigator/HomeMenu.tsx b/src/components/ui/Navigator/HomeMenu.tsx new file mode 100644 index 000000000..933c8d7d3 --- /dev/null +++ b/src/components/ui/Navigator/HomeMenu.tsx @@ -0,0 +1,98 @@ +import { ChevronRightIcon, HomeIcon } from 'lucide-react'; + +import { + IconButton, + IconButtonProps, + Menu, + MenuItem, + MenuProps, + Typography, +} from '@mui/material'; + +import React from 'react'; +import { Link } from 'react-router-dom'; + +import NavigationLink from './common/NavigationLink.js'; + +const Separator = ; + +type Props = { + selected: { name: string; id: string; to: string }; + elements: { + name: string; + id: string; + to: string; + }[]; + menuId?: string; + buildMenuItemId?: (itemId: string) => string; + homeDropdownId?: string; +}; + +const HomeMenu = ({ + buildMenuItemId, + elements, + homeDropdownId, + menuId, + selected, +}: Props): JSX.Element => { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + + const handleClick: IconButtonProps['onClick'] = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = (): void => { + setAnchorEl(null); + }; + + const onClick: MenuProps['onClick'] = (): void => { + handleClose(); + }; + + return ( + <> + + + {Separator} + + + {elements.map(({ name, id, to }) => ( + + {name} + + ))} + + + {selected.name} + + + ); +}; + +export default HomeMenu; diff --git a/src/components/ui/Navigator/ItemMenu.tsx b/src/components/ui/Navigator/ItemMenu.tsx new file mode 100644 index 000000000..f6a1405f5 --- /dev/null +++ b/src/components/ui/Navigator/ItemMenu.tsx @@ -0,0 +1,101 @@ +import type { UseQueryResult } from '@tanstack/react-query'; +import { ChevronRightIcon } from 'lucide-react'; + +import { + IconButton, + IconButtonProps, + Menu, + MenuItem, + Typography, +} from '@mui/material'; + +import { useState } from 'react'; +import { Link, LinkProps } from 'react-router-dom'; + +import { DiscriminatedItem } from '@graasp/sdk'; + +export const Separator = ; + +export type ItemMenuProps = { + buildIconId?: (id: string) => string; + buildMenuId?: (itemId: string) => string; + buildMenuItemId?: (itemId: string) => string; + buildToItemPath: (itemId: string) => LinkProps['to']; + icon?: JSX.Element; + itemId: string; + useChildren: (...args: unknown[]) => UseQueryResult; + renderArrow?: boolean; +}; + +const ItemMenu = ({ + buildIconId, + buildMenuId, + buildMenuItemId, + buildToItemPath, + icon = Separator, + itemId, + useChildren, + renderArrow, +}: ItemMenuProps): JSX.Element | null => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const { data: items } = useChildren(itemId); + + const handleClick: IconButtonProps['onClick'] = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = (): void => { + setAnchorEl(null); + }; + + if (!items?.length && renderArrow) { + // to display icon as a separator specially if there's an extra items after items menu + return icon; + } + if (!items?.length) { + return null; + } + return ( + <> + + {icon} + + + {items?.map(({ name, id }) => ( + + {name} + + ))} + + + ); +}; + +export default ItemMenu; diff --git a/src/components/ui/Navigator/Navigator.stories.tsx b/src/components/ui/Navigator/Navigator.stories.tsx new file mode 100644 index 000000000..eaac43ea2 --- /dev/null +++ b/src/components/ui/Navigator/Navigator.stories.tsx @@ -0,0 +1,166 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Navigator } from './Navigator'; + +const meta = { + component: Navigator, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default = { + args: {}, +} satisfies Story; + +// export const HomeRoot = { +// args: {}, + +// play: async ({ canvasElement }) => { +// const canvas = within(canvasElement); + +// // 2 x Home +// expect(canvas.getAllByTestId(dataTestId)).toHaveLength(2); +// }, +// } satisfies Story; + +// export const FolderWithParents = { +// args: { +// buildToItemPath, +// useChildren, +// item: folder, + +// renderRoot: () => { +// return ( +// <> +// +// { +// return { +// data: [buildItem('Home item 1'), buildItem('Home item 2')], +// } as UseChildrenHookType; +// }} +// buildToItemPath={buildToItemPath} +// /> +// +// ); +// }, +// parents, +// }, + +// play: async ({ canvasElement }) => { +// const canvas = within(canvasElement); + +// // current item +// expect(canvas.getByText(folder.name)).toBeInTheDocument(); + +// // check parents +// for (const p of parents) { +// const b = canvas.getByText(p!.name); +// expect(b).toBeInTheDocument(); +// } + +// // 4 = 2 parents + 2 x Home + current item is a folder +// expect(canvas.getAllByTestId(dataTestId)).toHaveLength(5); +// }, +// } satisfies Story; + +// export const FileWithParents = { +// args: { +// buildToItemPath, +// useChildren, +// item, + +// renderRoot: () => { +// return ( +// <> +// +// { +// return { +// data: [buildItem('Home item 1'), buildItem('Home item 2')], +// } as UseChildrenHookType; +// }} +// buildToItemPath={buildToItemPath} +// /> +// +// ); +// }, +// parents, +// }, + +// play: async ({ canvasElement }) => { +// const canvas = within(canvasElement); + +// // current item +// expect(canvas.getByText(item.name)).toBeInTheDocument(); + +// // check parents +// for (const p of parents) { +// const b = canvas.getByText(p!.name); +// expect(b).toBeInTheDocument(); +// } + +// // 4 = 2 parents + 2 x Home +// expect(canvas.getAllByTestId(dataTestId)).toHaveLength(4); +// }, +// } satisfies Story; + +// const extraItems = [ +// { +// name: 'Settings', +// path: '/settings', +// icon: , +// menuItems: [ +// { name: 'Information', path: '/info' }, +// { name: 'Settings', path: '/settings' }, +// { name: 'Publish', path: '/publish' }, +// ], +// }, +// ]; + +// export const FolderWithParentsWithExtraItems = { +// args: { +// buildToItemPath, +// useChildren, +// item: folder, +// maxItems: 10, +// renderRoot: () => { +// return ( +// <> +// +// { +// return { +// data: [buildItem('Home item 1'), buildItem('Home item 2')], +// } as UseChildrenHookType; +// }} +// buildToItemPath={buildToItemPath} +// /> +// +// ); +// }, +// parents, +// extraItems, +// }, + +// play: async ({ canvasElement }) => { +// const canvas = within(canvasElement); + +// // current item +// expect(canvas.getByText(folder.name)).toBeInTheDocument(); + +// // check parents +// for (const p of parents) { +// const b = canvas.getByText(p!.name); +// expect(b).toBeInTheDocument(); +// } + +// // 4 = 2 parents + 2 x Home + current item is a folder + 1 extra item +// expect(canvas.getAllByTestId(dataTestId)).toHaveLength(6); +// }, +// } satisfies Story; diff --git a/src/components/ui/Navigator/Navigator.tsx b/src/components/ui/Navigator/Navigator.tsx new file mode 100644 index 000000000..568542a37 --- /dev/null +++ b/src/components/ui/Navigator/Navigator.tsx @@ -0,0 +1,85 @@ +import { Breadcrumbs } from '@mui/material'; + +export function Navigator({ parents }: {}) { + return ; +} + +// const StyledBreadcrumbs = styled(Breadcrumbs)(() => ({ +// '.MuiBreadcrumbs-separator': { +// margin: 0, +// }, +// })); + +// export type NavigationProps = { +// backgroundColor?: string; +// buildBreadcrumbsItemLinkId?: (id: string) => string; +// buildIconId?: (id: string) => string; +// buildMenuItemId?: (id: string) => string; +// buildToItemPath: (id: string) => string; +// buildMenuId?: (id: string) => string; +// id?: string; +// item?: DiscriminatedItem; +// parents?: DiscriminatedItem[]; +// renderRoot?: (item?: DiscriminatedItem) => JSX.Element | null; +// sx?: SxProps; +// useChildren: ItemMenuProps['useChildren']; +// maxItems?: number; +// extraItems?: ExtraItem[]; +// }; +// export interface MenuItemType { +// name: string; +// path: string; +// } + +// const Navigation = ({ +// backgroundColor, +// buildBreadcrumbsItemLinkId, +// buildIconId, +// buildMenuItemId, +// buildToItemPath, +// id, +// item, +// parents, +// renderRoot, +// sx, +// useChildren, +// buildMenuId, +// maxItems = 4, +// extraItems, +// }: NavigationProps): JSX.Element | null => { +// return ( +// +// {renderRoot?.(item)} +// {item?.id && parents && ( +// +// )} +// {item?.id && item && ( +// +// )} +// {extraItems && } +// +// ); +// }; + +// export default Navigation; diff --git a/src/components/ui/Navigator/ParentsNavigation.tsx b/src/components/ui/Navigator/ParentsNavigation.tsx new file mode 100644 index 000000000..1c21ed6ab --- /dev/null +++ b/src/components/ui/Navigator/ParentsNavigation.tsx @@ -0,0 +1,48 @@ +import truncate from 'lodash.truncate'; + +import { Stack, Typography } from '@mui/material'; + +import { DiscriminatedItem } from '@graasp/sdk'; + +import ItemMenu, { ItemMenuProps } from './ItemMenu.js'; +import NavigationLink from './common/NavigationLink.js'; +import { ITEM_NAME_MAX_LENGTH } from './common/constants.js'; + +export type ParentsProps = { + parents: DiscriminatedItem[]; + buildBreadcrumbsItemLinkId?: (id: string) => string; + useChildren: ItemMenuProps['useChildren']; + buildToItemPath: (id: string) => string; +}; +const ParentsNavigation = ({ + parents, + useChildren, + buildBreadcrumbsItemLinkId, + buildToItemPath, +}: ParentsProps): JSX.Element => ( + + {parents.map(({ name, id }) => ( + + + + {truncate(name, { length: ITEM_NAME_MAX_LENGTH })} + + + + + ))} + +); +export default ParentsNavigation; diff --git a/src/components/ui/Navigator/common/CenterAlignWrapper.tsx b/src/components/ui/Navigator/common/CenterAlignWrapper.tsx new file mode 100644 index 000000000..96cc24ca1 --- /dev/null +++ b/src/components/ui/Navigator/common/CenterAlignWrapper.tsx @@ -0,0 +1,8 @@ +import { Box, styled } from '@mui/material'; + +const CenterAlignWrapper = styled('div')({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}) as typeof Box; +export default CenterAlignWrapper; diff --git a/src/components/ui/Navigator/common/NavigationLink.tsx b/src/components/ui/Navigator/common/NavigationLink.tsx new file mode 100644 index 000000000..f81402711 --- /dev/null +++ b/src/components/ui/Navigator/common/NavigationLink.tsx @@ -0,0 +1,14 @@ +import { styled } from '@mui/material'; + +import { ReactNode } from 'react'; +import { Link, LinkProps } from 'react-router-dom'; + +const NavigationLink: (props: { + id?: string; + children: ReactNode; + to: LinkProps['to']; +}) => ReactNode = styled(Link)(({ theme }) => ({ + textDecoration: 'none', + color: theme.palette.text.primary, +})); +export default NavigationLink; diff --git a/src/components/ui/Navigator/common/constants.ts b/src/components/ui/Navigator/common/constants.ts new file mode 100644 index 000000000..34ee57d5b --- /dev/null +++ b/src/components/ui/Navigator/common/constants.ts @@ -0,0 +1 @@ +export const ITEM_NAME_MAX_LENGTH = 15; From 40325aa870a09c59145a4c1227f3f37128b47e72 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Fri, 10 Jan 2025 14:54:27 +0100 Subject: [PATCH 2/3] fix: make navigator work --- package.json | 1 + pnpm-lock.yaml | 24 ++- src/components/ui/MenuItemLink.tsx | 25 +++ .../ui/Navigator/CurrentItemNavigation.tsx | 28 ++-- .../ui/Navigator/ExtraItemsMenu.tsx | 32 +--- .../ui/Navigator/ExtraItemsNavigation.tsx | 34 ++-- src/components/ui/Navigator/ItemMenu.tsx | 41 ++--- src/components/ui/Navigator/Navigator.tsx | 154 +++++++++--------- .../ui/Navigator/ParentsNavigation.tsx | 60 +++---- .../ui/Navigator/common/NavigationLink.tsx | 14 -- src/modules/analytics/Navigator.tsx | 34 +--- src/routes/analytics/items/$itemId.lazy.tsx | 12 +- 12 files changed, 201 insertions(+), 258 deletions(-) create mode 100644 src/components/ui/MenuItemLink.tsx delete mode 100644 src/components/ui/Navigator/common/NavigationLink.tsx diff --git a/package.json b/package.json index 461e846a6..6a2bd917a 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@mui/lab": "6.0.0-beta.22", "@mui/material": "6.3.1", "@sentry/react": "8.48.0", + "@tanstack/react-query": "5.63.0", "@tanstack/react-router": "1.95.1", "@tanstack/router-devtools": "1.95.1", "@tanstack/zod-adapter": "1.95.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4c432939..3f930799d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: version: 5.1.1 '@graasp/chatbox': specifier: 3.3.1 - version: 3.3.1(htr2mqcqqbxuceudmn3hlrygky) + version: 3.3.1(hx4w7cipqfuoiuips3zqjs6csi) '@graasp/query-client': specifier: 5.7.0 version: 5.7.0(@graasp/sdk@5.5.0(date-fns@4.1.0)(uuid@11.0.4))(@graasp/translations@1.43.0(i18next@24.2.1(typescript@5.7.3)))(react@18.3.1) @@ -53,6 +53,9 @@ importers: '@sentry/react': specifier: 8.48.0 version: 8.48.0(react@18.3.1) + '@tanstack/react-query': + specifier: 5.63.0 + version: 5.63.0(react@18.3.1) '@tanstack/react-router': specifier: 1.95.1 version: 1.95.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2168,6 +2171,9 @@ packages: '@tanstack/query-core@5.61.5': resolution: {integrity: sha512-iG5vqurEOEbv+paP6kW3zPENa99kSIrd1THISJMaTwVlJ+N5yjVDNOUwp9McK2DWqWCXM3v13ubBbAyhxT78UQ==} + '@tanstack/query-core@5.62.16': + resolution: {integrity: sha512-9Sgft7Qavcd+sN0V25xVyo0nfmcZXBuODy3FVG7BMWTg1HMLm8wwG5tNlLlmSic1u7l1v786oavn+STiFaPH2g==} + '@tanstack/query-devtools@5.61.4': resolution: {integrity: sha512-21Tw+u8E3IJJj4A/Bct4H0uBaDTEu7zBrR79FeSyY+mS2gx5/m316oDtJiKkILc819VSTYt+sFzODoJNcpPqZQ==} @@ -2182,6 +2188,11 @@ packages: peerDependencies: react: ^18 || ^19 + '@tanstack/react-query@5.63.0': + resolution: {integrity: sha512-QWizLzSiog8xqIRYmuJRok9VELlXVBAwtINgVCgW1SNvamQwWDO5R0XFSkjoBEj53x9Of1KAthLRBUC5xmtVLQ==} + peerDependencies: + react: ^18 || ^19 + '@tanstack/react-router@1.95.1': resolution: {integrity: sha512-P5x4yNhcdkYsCEoYeGZP8Q9Jlxf0WXJa4G/xvbmM905seZc9FqJqvCSRvX3dWTPOXRABhl4g+8DHqfft0c/AvQ==} engines: {node: '>=12'} @@ -7642,7 +7653,7 @@ snapshots: '@googlemaps/js-api-loader@1.16.8': {} - '@graasp/chatbox@3.3.1(htr2mqcqqbxuceudmn3hlrygky)': + '@graasp/chatbox@3.3.1(hx4w7cipqfuoiuips3zqjs6csi)': dependencies: '@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1) '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1) @@ -7654,7 +7665,7 @@ snapshots: '@mui/icons-material': 6.3.1(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.18)(react@18.3.1) '@mui/lab': 6.0.0-beta.22(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/material': 6.3.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/react-query': 5.61.5(react@18.3.1) + '@tanstack/react-query': 5.63.0(react@18.3.1) date-fns: 4.1.0 i18next: 24.2.1(typescript@5.7.3) lodash.groupby: 4.6.0 @@ -8314,6 +8325,8 @@ snapshots: '@tanstack/query-core@5.61.5': {} + '@tanstack/query-core@5.62.16': {} + '@tanstack/query-devtools@5.61.4': {} '@tanstack/react-query-devtools@5.61.5(@tanstack/react-query@5.61.5(react@18.3.1))(react@18.3.1)': @@ -8327,6 +8340,11 @@ snapshots: '@tanstack/query-core': 5.61.5 react: 18.3.1 + '@tanstack/react-query@5.63.0(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.62.16 + react: 18.3.1 + '@tanstack/react-router@1.95.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/history': 1.95.0 diff --git a/src/components/ui/MenuItemLink.tsx b/src/components/ui/MenuItemLink.tsx new file mode 100644 index 000000000..b8a681e66 --- /dev/null +++ b/src/components/ui/MenuItemLink.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { MenuItem, MenuItemProps } from '@mui/material'; + +import { LinkComponent, createLink } from '@tanstack/react-router'; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface MUIMenuItemProps extends Omit { + // Add any additional props you want to pass to the typography +} + +const MUIMenuItemComponent = React.forwardRef< + HTMLAnchorElement, + MUIMenuItemProps +>((props, ref) => { + return ; +}); + +const CreatedLinkComponent = createLink(MUIMenuItemComponent); + +export const MenuItemLink: LinkComponent = ( + props, +) => { + return ; +}; diff --git a/src/components/ui/Navigator/CurrentItemNavigation.tsx b/src/components/ui/Navigator/CurrentItemNavigation.tsx index 833cd3688..0e599dc03 100644 --- a/src/components/ui/Navigator/CurrentItemNavigation.tsx +++ b/src/components/ui/Navigator/CurrentItemNavigation.tsx @@ -1,11 +1,8 @@ -import { Typography } from '@mui/material'; - import { DiscriminatedItem, ItemType } from '@graasp/sdk'; +import { TypographyLink } from '../TypographyLink.js'; import ItemMenu, { ItemMenuProps } from './ItemMenu.js'; import CenterAlignWrapper from './common/CenterAlignWrapper.js'; -import NavigationLink from './common/NavigationLink.js'; -import { ITEM_NAME_MAX_LENGTH } from './common/constants.js'; export type CurrentItemProps = { item: DiscriminatedItem; @@ -14,35 +11,32 @@ export type CurrentItemProps = { buildMenuId?: (id: string) => string; buildMenuItemId?: (id: string) => string; useChildren: ItemMenuProps['useChildren']; - buildToItemPath: (id: string) => LinkProps['to']; showArrow: boolean; }; -const CurrentItemNavigation = ({ + +export function CurrentItemNavigation({ item, buildBreadcrumbsItemLinkId, - buildToItemPath, useChildren, buildIconId, buildMenuId, buildMenuItemId, showArrow, -}: CurrentItemProps): JSX.Element | null => { +}: Readonly): JSX.Element | null { return ( - - - {truncate(item.name, { length: ITEM_NAME_MAX_LENGTH })} - - + {item.name} + {(item.type === ItemType.FOLDER || showArrow) && ( ); -}; - -export default CurrentItemNavigation; +} diff --git a/src/components/ui/Navigator/ExtraItemsMenu.tsx b/src/components/ui/Navigator/ExtraItemsMenu.tsx index c789d0aad..e140dbb25 100644 --- a/src/components/ui/Navigator/ExtraItemsMenu.tsx +++ b/src/components/ui/Navigator/ExtraItemsMenu.tsx @@ -1,35 +1,23 @@ import { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { - IconButton, - IconButtonProps, - Menu, - MenuItem, - Typography, -} from '@mui/material'; +import { IconButton, IconButtonProps, Menu, Typography } from '@mui/material'; import { ChevronRightIcon } from 'lucide-react'; -import { MenuItemType } from './Navigator.js'; +import { MenuItemLink } from '../MenuItemLink'; +import { MenuItemType } from './Navigator'; export type ExtraItemsMenuProps = { icon?: JSX.Element; menuItems: MenuItemType[]; - buildIconId?: (id: string) => string; - buildMenuId?: (itemId: string) => string; - name: string; }; const Separator = ; -const ExtraItemsMenu = ({ +export function ExtraItemsMenu({ icon = Separator, menuItems, - buildIconId, - buildMenuId, - name, -}: ExtraItemsMenuProps): JSX.Element => { +}: Readonly): JSX.Element { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const handleClick: IconButtonProps['onClick'] = (event) => { @@ -45,7 +33,6 @@ const ExtraItemsMenu = ({ {icon} @@ -53,7 +40,6 @@ const ExtraItemsMenu = ({ {menuItems?.map(({ name, path }) => ( - + {name} - + ))} ); -}; - -export default ExtraItemsMenu; +} diff --git a/src/components/ui/Navigator/ExtraItemsNavigation.tsx b/src/components/ui/Navigator/ExtraItemsNavigation.tsx index 93437e992..404a9741a 100644 --- a/src/components/ui/Navigator/ExtraItemsNavigation.tsx +++ b/src/components/ui/Navigator/ExtraItemsNavigation.tsx @@ -1,12 +1,8 @@ -import { Box, Typography } from '@mui/material'; +import { Box, Stack } from '@mui/material'; -import truncate from 'lodash.truncate'; - -import ExtraItemsMenu from './ExtraItemsMenu.js'; -import { MenuItemType } from './Navigator.js'; -import CenterAlignWrapper from './common/CenterAlignWrapper.js'; -import NavigationLink from './common/NavigationLink.js'; -import { ITEM_NAME_MAX_LENGTH } from './common/constants.js'; +import { TypographyLink } from '../TypographyLink'; +import { ExtraItemsMenu } from './ExtraItemsMenu'; +import { MenuItemType } from './Navigator'; export interface ExtraItem { name: string; @@ -15,26 +11,20 @@ export interface ExtraItem { menuItems?: MenuItemType[]; } -const ExtraItemsNavigation = ({ +export function ExtraItemsNavigation({ extraItems, -}: { +}: Readonly<{ extraItems: ExtraItem[]; -}): JSX.Element[] | null => { +}>): JSX.Element[] { return extraItems.map(({ icon, name, path, menuItems }) => ( - + {icon} - - - {truncate(name, { length: ITEM_NAME_MAX_LENGTH })} - - + {name} {menuItems && menuItems.length > 0 && ( - + )} - + )); -}; - -export default ExtraItemsNavigation; +} diff --git a/src/components/ui/Navigator/ItemMenu.tsx b/src/components/ui/Navigator/ItemMenu.tsx index f6a1405f5..8dfc04322 100644 --- a/src/components/ui/Navigator/ItemMenu.tsx +++ b/src/components/ui/Navigator/ItemMenu.tsx @@ -1,42 +1,35 @@ -import type { UseQueryResult } from '@tanstack/react-query'; -import { ChevronRightIcon } from 'lucide-react'; - -import { - IconButton, - IconButtonProps, - Menu, - MenuItem, - Typography, -} from '@mui/material'; - import { useState } from 'react'; -import { Link, LinkProps } from 'react-router-dom'; + +import { IconButton, IconButtonProps, Menu, Typography } from '@mui/material'; import { DiscriminatedItem } from '@graasp/sdk'; -export const Separator = ; +import { type UseQueryResult } from '@tanstack/react-query'; +import { ChevronRightIcon } from 'lucide-react'; + +import { MenuItemLink } from '../MenuItemLink'; + +export const Separator = ; export type ItemMenuProps = { buildIconId?: (id: string) => string; buildMenuId?: (itemId: string) => string; buildMenuItemId?: (itemId: string) => string; - buildToItemPath: (itemId: string) => LinkProps['to']; icon?: JSX.Element; itemId: string; - useChildren: (...args: unknown[]) => UseQueryResult; + useChildren: (itemId: string) => UseQueryResult; renderArrow?: boolean; }; -const ItemMenu = ({ +export function ItemMenu({ buildIconId, buildMenuId, buildMenuItemId, - buildToItemPath, icon = Separator, itemId, useChildren, renderArrow, -}: ItemMenuProps): JSX.Element | null => { +}: Readonly): JSX.Element | null { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); @@ -63,7 +56,7 @@ const ItemMenu = ({ onClick={handleClick} id={buildIconId?.(itemId)} aria-controls={open ? buildMenuId?.(itemId) : undefined} - aria-haspopup='true' + aria-haspopup="true" aria-expanded={open ? true : undefined} > {icon} @@ -84,18 +77,18 @@ const ItemMenu = ({ }} > {items?.map(({ name, id }) => ( - {name} - + ))} ); -}; +} export default ItemMenu; diff --git a/src/components/ui/Navigator/Navigator.tsx b/src/components/ui/Navigator/Navigator.tsx index 568542a37..75566d8df 100644 --- a/src/components/ui/Navigator/Navigator.tsx +++ b/src/components/ui/Navigator/Navigator.tsx @@ -1,85 +1,79 @@ -import { Breadcrumbs } from '@mui/material'; +import { Breadcrumbs, SxProps, styled } from '@mui/material'; -export function Navigator({ parents }: {}) { - return ; -} +import { DiscriminatedItem } from '@graasp/sdk'; -// const StyledBreadcrumbs = styled(Breadcrumbs)(() => ({ -// '.MuiBreadcrumbs-separator': { -// margin: 0, -// }, -// })); +import { CurrentItemNavigation } from './CurrentItemNavigation'; +import { ExtraItem, ExtraItemsNavigation } from './ExtraItemsNavigation'; +import { ItemMenuProps } from './ItemMenu'; +import { ParentsNavigation } from './ParentsNavigation'; -// export type NavigationProps = { -// backgroundColor?: string; -// buildBreadcrumbsItemLinkId?: (id: string) => string; -// buildIconId?: (id: string) => string; -// buildMenuItemId?: (id: string) => string; -// buildToItemPath: (id: string) => string; -// buildMenuId?: (id: string) => string; -// id?: string; -// item?: DiscriminatedItem; -// parents?: DiscriminatedItem[]; -// renderRoot?: (item?: DiscriminatedItem) => JSX.Element | null; -// sx?: SxProps; -// useChildren: ItemMenuProps['useChildren']; -// maxItems?: number; -// extraItems?: ExtraItem[]; -// }; -// export interface MenuItemType { -// name: string; -// path: string; -// } +const StyledBreadcrumbs = styled(Breadcrumbs)(() => ({ + '.MuiBreadcrumbs-separator': { + margin: 0, + }, +})); -// const Navigation = ({ -// backgroundColor, -// buildBreadcrumbsItemLinkId, -// buildIconId, -// buildMenuItemId, -// buildToItemPath, -// id, -// item, -// parents, -// renderRoot, -// sx, -// useChildren, -// buildMenuId, -// maxItems = 4, -// extraItems, -// }: NavigationProps): JSX.Element | null => { -// return ( -// -// {renderRoot?.(item)} -// {item?.id && parents && ( -// -// )} -// {item?.id && item && ( -// -// )} -// {extraItems && } -// -// ); -// }; +export type NavigationProps = { + backgroundColor?: string; + buildBreadcrumbsItemLinkId?: (id: string) => string; + buildIconId?: (id: string) => string; + buildMenuItemId?: (id: string) => string; + buildMenuId?: (id: string) => string; + id?: string; + item?: DiscriminatedItem; + parents?: DiscriminatedItem[]; + sx?: SxProps; + useChildren: ItemMenuProps['useChildren']; + maxItems?: number; + extraItems?: ExtraItem[]; +}; +export interface MenuItemType { + name: string; + path: string; +} -// export default Navigation; +export function AnalyticsNavigator({ + backgroundColor, + buildBreadcrumbsItemLinkId, + buildIconId, + buildMenuItemId, + id, + item, + parents, + sx, + useChildren, + buildMenuId, + maxItems = 4, + extraItems, +}: Readonly): JSX.Element | null { + return ( + + {item?.id && parents && ( + + )} + {item?.id && item && ( + + )} + {extraItems && } + + ); +} diff --git a/src/components/ui/Navigator/ParentsNavigation.tsx b/src/components/ui/Navigator/ParentsNavigation.tsx index 1c21ed6ab..7d05dd1a5 100644 --- a/src/components/ui/Navigator/ParentsNavigation.tsx +++ b/src/components/ui/Navigator/ParentsNavigation.tsx @@ -1,48 +1,40 @@ -import truncate from 'lodash.truncate'; - -import { Stack, Typography } from '@mui/material'; +import { Stack } from '@mui/material'; import { DiscriminatedItem } from '@graasp/sdk'; +import { TypographyLink } from '../TypographyLink.js'; import ItemMenu, { ItemMenuProps } from './ItemMenu.js'; -import NavigationLink from './common/NavigationLink.js'; -import { ITEM_NAME_MAX_LENGTH } from './common/constants.js'; export type ParentsProps = { parents: DiscriminatedItem[]; buildBreadcrumbsItemLinkId?: (id: string) => string; useChildren: ItemMenuProps['useChildren']; - buildToItemPath: (id: string) => string; }; -const ParentsNavigation = ({ + +export function ParentsNavigation({ parents, useChildren, buildBreadcrumbsItemLinkId, - buildToItemPath, -}: ParentsProps): JSX.Element => ( - - {parents.map(({ name, id }) => ( - - ): JSX.Element { + return ( + + {parents.map(({ name, id }) => ( + - - {truncate(name, { length: ITEM_NAME_MAX_LENGTH })} - - - - - ))} - -); -export default ParentsNavigation; + + {name} + + + + ))} + + ); +} diff --git a/src/components/ui/Navigator/common/NavigationLink.tsx b/src/components/ui/Navigator/common/NavigationLink.tsx deleted file mode 100644 index f81402711..000000000 --- a/src/components/ui/Navigator/common/NavigationLink.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { styled } from '@mui/material'; - -import { ReactNode } from 'react'; -import { Link, LinkProps } from 'react-router-dom'; - -const NavigationLink: (props: { - id?: string; - children: ReactNode; - to: LinkProps['to']; -}) => ReactNode = styled(Link)(({ theme }) => ({ - textDecoration: 'none', - color: theme.palette.text.primary, -})); -export default NavigationLink; diff --git a/src/modules/analytics/Navigator.tsx b/src/modules/analytics/Navigator.tsx index b0bdd81aa..a31afbee8 100644 --- a/src/modules/analytics/Navigator.tsx +++ b/src/modules/analytics/Navigator.tsx @@ -1,11 +1,4 @@ -import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; -import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'; -import { IconButton } from '@mui/material'; - -import { Navigation } from '@graasp/ui'; - -import { useNavigate } from '@tanstack/react-router'; - +import { AnalyticsNavigator } from '@/components/ui/Navigator/Navigator'; import { hooks } from '@/config/queryClient'; import { @@ -16,16 +9,13 @@ import { } from './config/selectors'; import { NAVIGATOR_BACKGROUND_COLOR } from './constants'; -const { useItem, useParents, useCurrentMember, useChildren } = hooks; +const { useItem, useParents, useChildren } = hooks; export function Navigator({ itemId, }: Readonly<{ itemId: string }>): JSX.Element | null { - const { data: currentMember } = useCurrentMember(); const { data: item, isLoading: isItemLoading } = useItem(itemId); - const navigate = useNavigate(); - const { data: parents, isLoading: areParentsLoading } = useParents({ id: itemId, }); @@ -34,30 +24,12 @@ export function Navigator({ return null; } - const renderRoot = () => { - // no root access if signed out - if (!currentMember) { - return null; - } - - return ( - <> - navigate({ to: '/analytics' })}> - - - - - ); - }; - return ( - `/analytics/items/${id}`} parents={parents} - renderRoot={renderRoot} backgroundColor={NAVIGATOR_BACKGROUND_COLOR} buildBreadcrumbsItemLinkId={buildBreadcrumbsItemLink} buildMenuItemId={buildMenuItemId} diff --git a/src/routes/analytics/items/$itemId.lazy.tsx b/src/routes/analytics/items/$itemId.lazy.tsx index 2ee7f75b7..9c3d26f47 100644 --- a/src/routes/analytics/items/$itemId.lazy.tsx +++ b/src/routes/analytics/items/$itemId.lazy.tsx @@ -1,12 +1,13 @@ import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import { Box, Grid2, Skeleton, Stack, Typography } from '@mui/material'; +import { Box, Grid2, Skeleton, Stack } from '@mui/material'; import { Outlet, createLazyFileRoute } from '@tanstack/react-router'; import { NS } from '@/config/constants'; +import { Navigator } from '~analytics/Navigator'; import { PageWrapper } from '~analytics/PageWrapper'; import ActionsLegend from '~analytics/charts-layout/ActionsLegend'; import StyledAlert from '~analytics/common/StyledAlert'; @@ -26,13 +27,8 @@ function RouteComponent() { return ( - { - // temp disable as we need to adapt it to the current router - - here we should have the navigator - - /* */ - } + + From 7d98b4044efc0cc1fe2b80fc16fd000a37325be1 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Fri, 10 Jan 2025 15:00:13 +0100 Subject: [PATCH 3/3] fix: build issue --- src/components/ui/Navigator/HomeMenu.tsx | 30 ++-- .../ui/Navigator/Navigator.stories.tsx | 166 ------------------ 2 files changed, 12 insertions(+), 184 deletions(-) delete mode 100644 src/components/ui/Navigator/Navigator.stories.tsx diff --git a/src/components/ui/Navigator/HomeMenu.tsx b/src/components/ui/Navigator/HomeMenu.tsx index 933c8d7d3..e8b58dc48 100644 --- a/src/components/ui/Navigator/HomeMenu.tsx +++ b/src/components/ui/Navigator/HomeMenu.tsx @@ -1,20 +1,19 @@ -import { ChevronRightIcon, HomeIcon } from 'lucide-react'; +import { useState } from 'react'; import { IconButton, IconButtonProps, Menu, - MenuItem, MenuProps, Typography, } from '@mui/material'; -import React from 'react'; -import { Link } from 'react-router-dom'; +import { ChevronRightIcon, HomeIcon } from 'lucide-react'; -import NavigationLink from './common/NavigationLink.js'; +import { MenuItemLink } from '../MenuItemLink'; +import { TypographyLink } from '../TypographyLink'; -const Separator = ; +const Separator = ; type Props = { selected: { name: string; id: string; to: string }; @@ -35,7 +34,7 @@ const HomeMenu = ({ menuId, selected, }: Props): JSX.Element => { - const [anchorEl, setAnchorEl] = React.useState(null); + const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const handleClick: IconButtonProps['onClick'] = (event) => { @@ -57,7 +56,7 @@ const HomeMenu = ({ onClick={handleClick} id={homeDropdownId} aria-controls={open ? 'root' : undefined} - aria-haspopup='true' + aria-haspopup="true" aria-expanded={open ? true : undefined} > {Separator} @@ -78,19 +77,14 @@ const HomeMenu = ({ }} > {elements.map(({ name, id, to }) => ( - + {name} - + ))} - - {selected.name} - + + {selected.name} + ); }; diff --git a/src/components/ui/Navigator/Navigator.stories.tsx b/src/components/ui/Navigator/Navigator.stories.tsx deleted file mode 100644 index eaac43ea2..000000000 --- a/src/components/ui/Navigator/Navigator.stories.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import { Navigator } from './Navigator'; - -const meta = { - component: Navigator, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -export const Default = { - args: {}, -} satisfies Story; - -// export const HomeRoot = { -// args: {}, - -// play: async ({ canvasElement }) => { -// const canvas = within(canvasElement); - -// // 2 x Home -// expect(canvas.getAllByTestId(dataTestId)).toHaveLength(2); -// }, -// } satisfies Story; - -// export const FolderWithParents = { -// args: { -// buildToItemPath, -// useChildren, -// item: folder, - -// renderRoot: () => { -// return ( -// <> -// -// { -// return { -// data: [buildItem('Home item 1'), buildItem('Home item 2')], -// } as UseChildrenHookType; -// }} -// buildToItemPath={buildToItemPath} -// /> -// -// ); -// }, -// parents, -// }, - -// play: async ({ canvasElement }) => { -// const canvas = within(canvasElement); - -// // current item -// expect(canvas.getByText(folder.name)).toBeInTheDocument(); - -// // check parents -// for (const p of parents) { -// const b = canvas.getByText(p!.name); -// expect(b).toBeInTheDocument(); -// } - -// // 4 = 2 parents + 2 x Home + current item is a folder -// expect(canvas.getAllByTestId(dataTestId)).toHaveLength(5); -// }, -// } satisfies Story; - -// export const FileWithParents = { -// args: { -// buildToItemPath, -// useChildren, -// item, - -// renderRoot: () => { -// return ( -// <> -// -// { -// return { -// data: [buildItem('Home item 1'), buildItem('Home item 2')], -// } as UseChildrenHookType; -// }} -// buildToItemPath={buildToItemPath} -// /> -// -// ); -// }, -// parents, -// }, - -// play: async ({ canvasElement }) => { -// const canvas = within(canvasElement); - -// // current item -// expect(canvas.getByText(item.name)).toBeInTheDocument(); - -// // check parents -// for (const p of parents) { -// const b = canvas.getByText(p!.name); -// expect(b).toBeInTheDocument(); -// } - -// // 4 = 2 parents + 2 x Home -// expect(canvas.getAllByTestId(dataTestId)).toHaveLength(4); -// }, -// } satisfies Story; - -// const extraItems = [ -// { -// name: 'Settings', -// path: '/settings', -// icon: , -// menuItems: [ -// { name: 'Information', path: '/info' }, -// { name: 'Settings', path: '/settings' }, -// { name: 'Publish', path: '/publish' }, -// ], -// }, -// ]; - -// export const FolderWithParentsWithExtraItems = { -// args: { -// buildToItemPath, -// useChildren, -// item: folder, -// maxItems: 10, -// renderRoot: () => { -// return ( -// <> -// -// { -// return { -// data: [buildItem('Home item 1'), buildItem('Home item 2')], -// } as UseChildrenHookType; -// }} -// buildToItemPath={buildToItemPath} -// /> -// -// ); -// }, -// parents, -// extraItems, -// }, - -// play: async ({ canvasElement }) => { -// const canvas = within(canvasElement); - -// // current item -// expect(canvas.getByText(folder.name)).toBeInTheDocument(); - -// // check parents -// for (const p of parents) { -// const b = canvas.getByText(p!.name); -// expect(b).toBeInTheDocument(); -// } - -// // 4 = 2 parents + 2 x Home + current item is a folder + 1 extra item -// expect(canvas.getAllByTestId(dataTestId)).toHaveLength(6); -// }, -// } satisfies Story;