diff --git a/app/actions/index.ts b/app/actions/index.ts index 590d70eba8..3f74231f97 100644 --- a/app/actions/index.ts +++ b/app/actions/index.ts @@ -268,7 +268,7 @@ export function actionV2ToMenuItem( switch (action.type) { case "action": { const title = resolve(action.name, context); - const visible = resolve(action.visible, context); + const visible = resolve(action.visible, context) ?? true; const disabled = resolve(action.disabled, context); const icon = !!action.icon && action.iconInContextMenu !== false diff --git a/app/components/Breadcrumb.tsx b/app/components/Breadcrumb.tsx index 06ebe877bd..f46916d98e 100644 --- a/app/components/Breadcrumb.tsx +++ b/app/components/Breadcrumb.tsx @@ -6,55 +6,89 @@ import { s, ellipsis } from "@shared/styles"; import Flex from "~/components/Flex"; import BreadcrumbMenu from "~/menus/BreadcrumbMenu"; import { undraggableOnDesktop } from "~/styles"; -import { MenuInternalLink } from "~/types"; +import { InternalLinkActionV2, MenuInternalLink } from "~/types"; +import { actionV2ToMenuItem } from "~/actions"; +import useActionContext from "~/hooks/useActionContext"; +import { useComputed } from "~/hooks/useComputed"; + +type TopLevelAction = + | InternalLinkActionV2 + | { type: "menu"; actions: InternalLinkActionV2[] }; type Props = React.PropsWithChildren<{ - items: MenuInternalLink[]; + actions: InternalLinkActionV2[]; max?: number; highlightFirstItem?: boolean; }>; function Breadcrumb( - { items, highlightFirstItem, children, max = 2 }: Props, + { actions, highlightFirstItem, children, max = 2 }: Props, ref: React.RefObject | null ) { - const totalItems = items.length; - const topLevelItems: MenuInternalLink[] = [...items]; - let overflowItems; + const actionContext = useActionContext({ isContextMenu: true }); + + const visibleActions = useComputed( + () => + actions.filter((action) => + typeof action.visible === "function" + ? action.visible(actionContext) + : (action.visible ?? true) + ), + [actions, actionContext] + ); + const totalVisibleActions = visibleActions.length; + + const topLevelActions: TopLevelAction[] = [...visibleActions]; // chop middle breadcrumbs and present a "..." menu instead - if (totalItems > max) { + if (totalVisibleActions > max) { const halfMax = Math.floor(max / 2); - overflowItems = topLevelItems.splice(halfMax, totalItems - max); + const menuActions = topLevelActions.splice( + halfMax, + totalVisibleActions - max + ) as InternalLinkActionV2[]; - topLevelItems.splice(halfMax, 0, { - to: "", - type: "route", - title: , + topLevelActions.splice(halfMax, 0, { + type: "menu", + actions: menuActions, }); } + const toBreadcrumb = React.useCallback( + (action: TopLevelAction, index: number) => { + if (action.type === "menu") { + return ; + } + + const item = actionV2ToMenuItem( + action, + actionContext + ) as MenuInternalLink; + + return ( + <> + {item.icon} + + {item.title} + + + ); + }, + [actionContext, highlightFirstItem] + ); + return ( - {topLevelItems.map((item, index) => ( - - {item.icon} - {item.to ? ( - - {item.title} - - ) : ( - item.title - )} - {index !== topLevelItems.length - 1 || !!children ? : null} + {topLevelActions.map((action, index) => ( + + {toBreadcrumb(action, index)} + {index !== topLevelActions.length - 1 || !!children ? ( + + ) : null} ))} {children} diff --git a/app/components/CollectionBreadcrumb.tsx b/app/components/CollectionBreadcrumb.tsx index bf8c59648f..bd388ca1ff 100644 --- a/app/components/CollectionBreadcrumb.tsx +++ b/app/components/CollectionBreadcrumb.tsx @@ -3,9 +3,10 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import Collection from "~/models/Collection"; import CollectionIcon from "~/components/Icons/CollectionIcon"; -import { MenuInternalLink } from "~/types"; import { archivePath, collectionPath } from "~/utils/routeHelpers"; import Breadcrumb from "./Breadcrumb"; +import { createInternalLinkActionV2 } from "~/actions"; +import { ActiveCollectionSection } from "~/actions/sections"; type Props = { collection: Collection; @@ -14,32 +15,24 @@ type Props = { export const CollectionBreadcrumb: React.FC = ({ collection }) => { const { t } = useTranslation(); - const items = React.useMemo(() => { - const collectionNode: MenuInternalLink = { - type: "route", - title: collection.name, - icon: , - to: collectionPath(collection.path), - }; + const actions = React.useMemo( + () => [ + createInternalLinkActionV2({ + name: t("Archive"), + section: ActiveCollectionSection, + icon: , + visible: collection.isArchived, + to: archivePath(), + }), + createInternalLinkActionV2({ + name: collection.name, + section: ActiveCollectionSection, + icon: , + to: collectionPath(collection.path), + }), + ], + [collection, t] + ); - const category: MenuInternalLink | undefined = collection.isArchived - ? { - type: "route", - icon: , - title: t("Archive"), - to: archivePath(), - } - : undefined; - - const output = []; - if (category) { - output.push(category); - } - - output.push(collectionNode); - - return output; - }, [collection, t]); - - return ; + return ; }; diff --git a/app/components/DocumentBreadcrumb.tsx b/app/components/DocumentBreadcrumb.tsx index c2f851affc..dcd3360869 100644 --- a/app/components/DocumentBreadcrumb.tsx +++ b/app/components/DocumentBreadcrumb.tsx @@ -11,8 +11,9 @@ import CollectionIcon from "~/components/Icons/CollectionIcon"; import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; -import { MenuInternalLink } from "~/types"; import { archivePath, settingsPath, trashPath } from "~/utils/routeHelpers"; +import { createInternalLinkActionV2 } from "~/actions"; +import { ActiveDocumentSection } from "~/actions/sections"; type Props = { children?: React.ReactNode; @@ -27,46 +28,12 @@ type Props = { maxDepth?: number; }; -function useCategory(document: Document): MenuInternalLink | null { - const { t } = useTranslation(); - - if (document.isDeleted) { - return { - type: "route", - icon: , - title: t("Trash"), - to: trashPath(), - }; - } - - if (document.isArchived) { - return { - type: "route", - icon: , - title: t("Archive"), - to: archivePath(), - }; - } - - if (document.template) { - return { - type: "route", - icon: , - title: t("Templates"), - to: settingsPath("templates"), - }; - } - - return null; -} - function DocumentBreadcrumb( { document, children, onlyText, reverse = false, maxDepth }: Props, ref: React.RefObject | null ) { const { collections } = useStores(); const { t } = useTranslation(); - const category = useCategory(document); const sidebarContext = useLocationSidebarContext(); const collection = document.collectionId ? collections.get(document.collectionId) @@ -78,69 +45,91 @@ function DocumentBreadcrumb( void document.loadRelations({ withoutPolicies: true }); }, [document]); - let collectionNode: MenuInternalLink | undefined; - - if (collection && can.readDocument) { - collectionNode = { - type: "route", - title: collection.name, - icon: , - to: { - pathname: collection.path, - state: { sidebarContext }, - }, - }; - } else if (document.isCollectionDeleted) { - collectionNode = { - type: "route", - title: t("Deleted Collection"), - icon: undefined, - to: "", - }; - } - const path = document.pathTo.slice(0, -1); - const items = React.useMemo(() => { - const output: MenuInternalLink[] = []; - + const actions = React.useMemo(() => { if (depth === 0) { - return output; + return []; } - if (category) { - output.push(category); - } - if (collectionNode) { - output.push(collectionNode); - } - - path.forEach((node: NavigationNode) => { - const title = node.title || t("Untitled"); - output.push({ - type: "route", - title: node.icon ? ( - <> - {title} - - ) : ( - title - ), - to: { - pathname: node.url, - state: { sidebarContext }, - }, - }); - }); + const outputActions = [ + createInternalLinkActionV2({ + name: t("Trash"), + section: ActiveDocumentSection, + icon: , + visible: document.isDeleted, + to: trashPath(), + }), + createInternalLinkActionV2({ + name: t("Archive"), + section: ActiveDocumentSection, + icon: , + visible: document.isArchived, + to: archivePath(), + }), + createInternalLinkActionV2({ + name: t("Templates"), + section: ActiveDocumentSection, + icon: , + visible: document.template, + to: settingsPath("templates"), + }), + createInternalLinkActionV2({ + name: collection?.name, + section: ActiveDocumentSection, + icon: collection ? ( + + ) : undefined, + visible: !!(collection && can.readDocument), + to: collection + ? { + pathname: collection.path, + state: { sidebarContext }, + } + : "", + }), + createInternalLinkActionV2({ + name: t("Deleted Collection"), + section: ActiveDocumentSection, + visible: document.isCollectionDeleted, + to: "", + }), + ...path.map((node) => { + const title = node.title || t("Untitled"); + return createInternalLinkActionV2({ + name: node.icon ? ( + <> + {title} + + ) : ( + title + ), + section: ActiveDocumentSection, + to: { + pathname: node.url, + state: { sidebarContext }, + }, + }); + }), + ]; return reverse ? depth !== undefined - ? output.slice(-depth) - : output + ? outputActions.slice(-depth) + : outputActions : depth !== undefined - ? output.slice(0, depth) - : output; - }, [t, path, category, sidebarContext, collectionNode, reverse, depth]); + ? outputActions.slice(0, depth) + : outputActions; + }, [ + t, + document, + collection, + can.readDocument, + sidebarContext, + path, + reverse, + depth, + ]); if (!collections.isLoaded) { return null; @@ -176,7 +165,7 @@ function DocumentBreadcrumb( } return ( - + {children} ); diff --git a/app/menus/BreadcrumbMenu.tsx b/app/menus/BreadcrumbMenu.tsx index 65b4fed231..b4e6bac273 100644 --- a/app/menus/BreadcrumbMenu.tsx +++ b/app/menus/BreadcrumbMenu.tsx @@ -1,27 +1,21 @@ import { useTranslation } from "react-i18next"; -import ContextMenu from "~/components/ContextMenu"; -import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton"; -import Template from "~/components/ContextMenu/Template"; -import { useMenuState } from "~/hooks/useMenuState"; -import { MenuInternalLink } from "~/types"; +import { DropdownMenu } from "~/components/Menu/DropdownMenu"; +import { OverflowMenuButton } from "~/components/Menu/OverflowMenuButton"; +import { useMenuAction } from "~/hooks/useMenuAction"; +import { InternalLinkActionV2 } from "~/types"; type Props = { - items: MenuInternalLink[]; + actions: InternalLinkActionV2[]; }; -export default function BreadcrumbMenu({ items }: Props) { +export default function BreadcrumbMenu({ actions }: Props) { const { t } = useTranslation(); - const menu = useMenuState({ - modal: true, - placement: "bottom", - }); + + const rootAction = useMenuAction(actions); return ( - <> - - -