feat: Add context menus to document breadcrumb items (#11910)

Wrap collection and document names in the header breadcrumb with
ContextMenu components, enabling right-click menus with relevant
actions. Each breadcrumb item type has its own component to scope
hooks. Breadcrumb links prevent navigation when a context menu is open.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-03-30 19:44:44 -04:00
committed by GitHub
parent b70950627e
commit 6e95aa441b
3 changed files with 98 additions and 14 deletions
+11 -1
View File
@@ -55,6 +55,15 @@ function Breadcrumb(
});
}
const handleClick = React.useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
if (event.currentTarget.querySelector('[data-state="open"]')) {
event.preventDefault();
}
},
[]
);
const toBreadcrumb = React.useCallback(
(action: TopLevelAction, index: number) => {
if (action.type === "menu") {
@@ -68,6 +77,7 @@ function Breadcrumb(
{item.icon}
<Item
to={item.to}
onClick={handleClick}
$withIcon={!!item.icon}
$highlight={!!highlightFirstItem && index === 0}
>
@@ -76,7 +86,7 @@ function Breadcrumb(
</>
);
},
[actionContext, highlightFirstItem]
[actionContext, handleClick, highlightFirstItem]
);
return (
+85 -12
View File
@@ -5,9 +5,14 @@ import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Icon from "@shared/components/Icon";
import type { NavigationNode } from "@shared/types";
import type Collection from "~/models/Collection";
import type Document from "~/models/Document";
import Breadcrumb from "~/components/Breadcrumb";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { ContextMenu } from "~/components/Menu/ContextMenu";
import { ActionContextProvider } from "~/hooks/useActionContext";
import { useCollectionMenuAction } from "~/hooks/useCollectionMenuAction";
import { useDocumentMenuAction } from "~/hooks/useDocumentMenuAction";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
@@ -68,7 +73,9 @@ function DocumentBreadcrumb(
to: archivePath(),
}),
createInternalLinkAction({
name: collection?.name,
name: collection ? (
<CollectionName collection={collection} />
) : undefined,
section: ActiveDocumentSection,
icon: collection ? (
<CollectionIcon collection={collection} expanded />
@@ -90,17 +97,14 @@ function DocumentBreadcrumb(
...path.map((node) => {
const title = node.title || t("Untitled");
return createInternalLinkAction({
name: node.icon ? (
<>
<StyledIcon
value={node.icon}
color={node.color}
initial={node.title.charAt(0).toUpperCase()}
/>{" "}
{title}
</>
) : (
title
name: (
<DocumentName
documentId={node.id}
collection={collection}
icon={node.icon}
color={node.color}
title={title}
/>
),
section: ActiveDocumentSection,
to: {
@@ -169,6 +173,75 @@ function DocumentBreadcrumb(
);
}
/** Renders a collection name wrapped in a context menu. */
const CollectionName = observer(function CollectionName_({
collection,
}: {
collection: Collection;
}) {
const { t } = useTranslation();
const menuAction = useCollectionMenuAction({
collectionId: collection.id,
});
return (
<ActionContextProvider value={{ activeModels: [collection] }}>
<ContextMenu action={menuAction} ariaLabel={t("Collection options")}>
<span>{collection.name}</span>
</ContextMenu>
</ActionContextProvider>
);
});
/** Renders a document name wrapped in a context menu. */
const DocumentName = observer(function DocumentName_({
documentId,
collection,
icon,
color,
title,
}: {
documentId: string;
collection: Collection | undefined;
icon: string | undefined;
color: string | undefined;
title: string;
}) {
const { t } = useTranslation();
const { documents } = useStores();
const doc = documents.get(documentId);
const menuAction = useDocumentMenuAction({ documentId });
const content = icon ? (
<>
<StyledIcon
value={icon}
color={color}
initial={title.charAt(0).toUpperCase()}
/>{" "}
{title}
</>
) : (
title
);
if (!doc) {
return <>{content}</>;
}
return (
<ActionContextProvider
value={{
activeModels: [doc, ...(collection ? [collection] : [])],
}}
>
<ContextMenu action={menuAction} ariaLabel={t("Document options")}>
<span>{content}</span>
</ContextMenu>
</ActionContextProvider>
);
});
const StyledIcon = styled(Icon)`
margin-right: 2px;
`;
+2 -1
View File
@@ -244,6 +244,8 @@
"Install now": "Install now",
"Deleted Collection": "Deleted Collection",
"Untitled": "Untitled",
"Collection options": "Collection options",
"Document options": "Document options",
"Unpin": "Unpin",
"Export started": "Export started",
"A link to your file will be sent through email soon": "A link to your file will be sent through email soon",
@@ -268,7 +270,6 @@
"Couldnt move the document, try again?": "Couldnt move the document, try again?",
"Move to <em>{{ location }}</em>": "Move to <em>{{ location }}</em>",
"Couldnt move the template, try again?": "Couldnt move the template, try again?",
"Document options": "Document options",
"New": "New",
"Only visible to you": "Only visible to you",
"Draft": "Draft",