mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
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:
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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 @@
|
||||
"Couldn’t move the document, try again?": "Couldn’t move the document, try again?",
|
||||
"Move to <em>{{ location }}</em>": "Move to <em>{{ location }}</em>",
|
||||
"Couldn’t move the template, try again?": "Couldn’t move the template, try again?",
|
||||
"Document options": "Document options",
|
||||
"New": "New",
|
||||
"Only visible to you": "Only visible to you",
|
||||
"Draft": "Draft",
|
||||
|
||||
Reference in New Issue
Block a user