mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
chore: Move more dropdowns to Radix (part 2) (#9880)
* revision menu * new child document menu; dropdown trigger now supports tooltip * import menu * oauth authentication * oauth client * tsc
This commit is contained in:
@@ -3,7 +3,7 @@ import { LinkIcon, RestoreIcon, TrashIcon } from "outline-icons";
|
||||
import { matchPath } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import stores from "~/stores";
|
||||
import { createAction } from "~/actions";
|
||||
import { createAction, createActionV2 } from "~/actions";
|
||||
import { RevisionSection } from "~/actions/sections";
|
||||
import history from "~/utils/history";
|
||||
import {
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
matchDocumentHistory,
|
||||
} from "~/utils/routeHelpers";
|
||||
|
||||
export const restoreRevision = createAction({
|
||||
export const restoreRevision = createActionV2({
|
||||
name: ({ t }) => t("Restore"),
|
||||
analyticsName: "Restore revision",
|
||||
icon: <RestoreIcon />,
|
||||
@@ -73,7 +73,7 @@ export const deleteRevision = createAction({
|
||||
},
|
||||
});
|
||||
|
||||
export const copyLinkToRevision = createAction({
|
||||
export const copyLinkToRevision = createActionV2({
|
||||
name: ({ t }) => t("Copy link"),
|
||||
analyticsName: "Copy link to revision",
|
||||
icon: <LinkIcon />,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import styled from "styled-components";
|
||||
import Scrollable from "~/components/Scrollable";
|
||||
import {
|
||||
@@ -43,101 +44,110 @@ type Props = {
|
||||
onOpen?: () => void;
|
||||
/** Callback when menu is closed */
|
||||
onClose?: () => void;
|
||||
};
|
||||
// TODO: Invert the dependency chain by forwarding dropdown ref and props to Tooltip component
|
||||
} & React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>;
|
||||
|
||||
export const DropdownMenu = observer(function DropdownMenu({
|
||||
action,
|
||||
context,
|
||||
children,
|
||||
align = "start",
|
||||
ariaLabel,
|
||||
onOpen,
|
||||
onClose,
|
||||
append,
|
||||
}: Props) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const isMobile = useMobile();
|
||||
const contentRef =
|
||||
React.useRef<React.ElementRef<typeof DropdownMenuContent>>(null);
|
||||
export const DropdownMenu = observer(
|
||||
React.forwardRef<React.ElementRef<typeof TooltipPrimitive.Trigger>, Props>(
|
||||
(
|
||||
{
|
||||
action,
|
||||
context,
|
||||
children,
|
||||
align = "start",
|
||||
ariaLabel,
|
||||
append,
|
||||
onOpen,
|
||||
onClose,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const isMobile = useMobile();
|
||||
const contentRef =
|
||||
React.useRef<React.ElementRef<typeof DropdownMenuContent>>(null);
|
||||
|
||||
const actionContext =
|
||||
context ??
|
||||
useActionContext({
|
||||
isContextMenu: true,
|
||||
});
|
||||
const actionContext =
|
||||
context ??
|
||||
useActionContext({
|
||||
isContextMenu: true,
|
||||
});
|
||||
|
||||
const menuItems = useComputed(() => {
|
||||
if (!open) {
|
||||
return [];
|
||||
}
|
||||
const menuItems = useComputed(() => {
|
||||
if (!open) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (action.children as ActionV2Variant[]).map((childAction) =>
|
||||
actionV2ToMenuItem(childAction, actionContext)
|
||||
);
|
||||
}, [open, action.children, actionContext]);
|
||||
return (action.children as ActionV2Variant[]).map((childAction) =>
|
||||
actionV2ToMenuItem(childAction, actionContext)
|
||||
);
|
||||
}, [open, action.children, actionContext]);
|
||||
|
||||
const handleOpenChange = React.useCallback(
|
||||
(open: boolean) => {
|
||||
setOpen(open);
|
||||
if (open) {
|
||||
onOpen?.();
|
||||
} else {
|
||||
onClose?.();
|
||||
const handleOpenChange = React.useCallback(
|
||||
(open: boolean) => {
|
||||
setOpen(open);
|
||||
if (open) {
|
||||
onOpen?.();
|
||||
} else {
|
||||
onClose?.();
|
||||
}
|
||||
},
|
||||
[onOpen, onClose]
|
||||
);
|
||||
|
||||
const enablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "auto";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const disablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "none";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCloseAutoFocus = React.useCallback(
|
||||
(e: Event) => e.preventDefault(),
|
||||
[]
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<MobileDropdown
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
items={menuItems}
|
||||
trigger={children}
|
||||
ariaLabel={ariaLabel}
|
||||
append={append}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
[onOpen, onClose]
|
||||
);
|
||||
|
||||
const enablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "auto";
|
||||
const content = toDropdownMenuItems(menuItems);
|
||||
|
||||
return (
|
||||
<DropdownMenuRoot open={open} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger ref={ref} aria-label={ariaLabel} {...rest}>
|
||||
{children}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align={align}
|
||||
aria-label={ariaLabel}
|
||||
onAnimationStart={disablePointerEvents}
|
||||
onAnimationEnd={enablePointerEvents}
|
||||
onCloseAutoFocus={handleCloseAutoFocus}
|
||||
>
|
||||
{content}
|
||||
{append}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuRoot>
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const disablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "none";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCloseAutoFocus = React.useCallback(
|
||||
(e: Event) => e.preventDefault(),
|
||||
[]
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<MobileDropdown
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
items={menuItems}
|
||||
trigger={children}
|
||||
ariaLabel={ariaLabel}
|
||||
append={append}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const content = toDropdownMenuItems(menuItems);
|
||||
|
||||
return (
|
||||
<DropdownMenuRoot open={open} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger aria-label={ariaLabel}>
|
||||
{children}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align={align}
|
||||
aria-label={ariaLabel}
|
||||
onAnimationStart={disablePointerEvents}
|
||||
onAnimationEnd={enablePointerEvents}
|
||||
onCloseAutoFocus={handleCloseAutoFocus}
|
||||
>
|
||||
{content}
|
||||
{append}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuRoot>
|
||||
);
|
||||
});
|
||||
)
|
||||
);
|
||||
|
||||
type MobileDropdownProps = {
|
||||
open: boolean;
|
||||
|
||||
+31
-35
@@ -3,12 +3,13 @@ import { CrossIcon, TrashIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Import from "~/models/Import";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import { useMenuState } from "~/hooks/useMenuState";
|
||||
import { DropdownMenu } from "~/components/Menu/DropdownMenu";
|
||||
import { OverflowMenuButton } from "~/components/Menu/OverflowMenuButton";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import { MenuItem } from "~/types";
|
||||
import { createActionV2 } from "~/actions";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
|
||||
const Section = "Imports";
|
||||
|
||||
type Props = {
|
||||
/** Import to which actions will be applied. */
|
||||
@@ -23,40 +24,35 @@ export const ImportMenu = observer(
|
||||
({ importModel, onCancel, onDelete }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const can = usePolicy(importModel);
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
|
||||
const items = React.useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
type: "button",
|
||||
title: t("Cancel"),
|
||||
visible: can.cancel,
|
||||
icon: <CrossIcon />,
|
||||
dangerous: true,
|
||||
onClick: onCancel,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Delete"),
|
||||
visible: can.delete,
|
||||
icon: <TrashIcon />,
|
||||
dangerous: true,
|
||||
onClick: onDelete,
|
||||
},
|
||||
] satisfies MenuItem[],
|
||||
[t, can.delete, can.cancel, onCancel, onDelete]
|
||||
const actions = React.useMemo(
|
||||
() => [
|
||||
createActionV2({
|
||||
name: t("Cancel"),
|
||||
section: Section,
|
||||
visible: !!can.cancel,
|
||||
icon: <CrossIcon />,
|
||||
dangerous: true,
|
||||
perform: onCancel,
|
||||
}),
|
||||
createActionV2({
|
||||
name: t("Delete"),
|
||||
section: Section,
|
||||
visible: !!can.delete,
|
||||
icon: <TrashIcon />,
|
||||
dangerous: true,
|
||||
perform: onDelete,
|
||||
}),
|
||||
],
|
||||
[t, can.cancel, can.delete, onCancel, onDelete]
|
||||
);
|
||||
|
||||
const rootAction = useMenuAction(actions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
|
||||
<ContextMenu {...menu} aria-label={t("Import menu options")}>
|
||||
<Template {...menu} items={items} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
<DropdownMenu action={rootAction} ariaLabel={t("Import menu options")}>
|
||||
<OverflowMenuButton />
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,40 +1,36 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { MenuButton, MenuButtonHTMLProps } from "reakit/Menu";
|
||||
import Document from "~/models/Document";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import { useMenuState } from "~/hooks/useMenuState";
|
||||
import { DropdownMenu } from "~/components/Menu/DropdownMenu";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { MenuItem } from "~/types";
|
||||
import { newDocumentPath, newNestedDocumentPath } from "~/utils/routeHelpers";
|
||||
import { createInternalLinkActionV2 } from "~/actions";
|
||||
import { ActiveDocumentSection } from "~/actions/sections";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
import Button from "~/components/Button";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
|
||||
type Props = {
|
||||
label?: (props: MenuButtonHTMLProps) => React.ReactNode;
|
||||
document: Document;
|
||||
};
|
||||
|
||||
function NewChildDocumentMenu({ document, label }: Props) {
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
function NewChildDocumentMenu({ document }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const canCollection = usePolicy(document.collectionId);
|
||||
const { collections } = useStores();
|
||||
|
||||
const items: MenuItem[] = [];
|
||||
const collection = document.collectionId
|
||||
? collections.get(document.collectionId)
|
||||
: undefined;
|
||||
const collectionName = collection ? collection.name : t("collection");
|
||||
|
||||
if (canCollection.createDocument) {
|
||||
const collection = document.collectionId
|
||||
? collections.get(document.collectionId)
|
||||
: undefined;
|
||||
const collectionName = collection ? collection.name : t("collection");
|
||||
items.push({
|
||||
type: "route",
|
||||
title: (
|
||||
<span>
|
||||
const actions = React.useMemo(
|
||||
() => [
|
||||
createInternalLinkActionV2({
|
||||
name: (
|
||||
<Trans
|
||||
defaults="New document in <em>{{ collectionName }}</em>"
|
||||
values={{
|
||||
@@ -44,37 +40,51 @@ function NewChildDocumentMenu({ document, label }: Props) {
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
to: newDocumentPath(document.collectionId),
|
||||
});
|
||||
}
|
||||
),
|
||||
section: ActiveDocumentSection,
|
||||
visible: !!canCollection.createDocument,
|
||||
to: newDocumentPath(document.collectionId),
|
||||
}),
|
||||
createInternalLinkActionV2({
|
||||
name: (
|
||||
<Trans
|
||||
defaults="New document in <em>{{ collectionName }}</em>"
|
||||
values={{
|
||||
collectionName: document.titleWithDefault,
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
section: ActiveDocumentSection,
|
||||
visible: true,
|
||||
to: newNestedDocumentPath(document.id),
|
||||
}),
|
||||
],
|
||||
[
|
||||
collectionName,
|
||||
canCollection.createDocument,
|
||||
document.id,
|
||||
document.titleWithDefault,
|
||||
document.collectionId,
|
||||
]
|
||||
);
|
||||
|
||||
items.push({
|
||||
type: "route",
|
||||
title: (
|
||||
<span>
|
||||
<Trans
|
||||
defaults="New document in <em>{{ collectionName }}</em>"
|
||||
values={{
|
||||
collectionName: document.title,
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
to: newNestedDocumentPath(document.id),
|
||||
});
|
||||
const rootAction = useMenuAction(actions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuButton {...menu}>{label}</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("New child document")}>
|
||||
<Template {...menu} items={items} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
<Tooltip content={t("New document")} shortcut="n" placement="bottom">
|
||||
<DropdownMenu
|
||||
action={rootAction}
|
||||
align="end"
|
||||
ariaLabel={t("New child document")}
|
||||
>
|
||||
<Button icon={<PlusIcon />} neutral>
|
||||
{t("New doc")}
|
||||
</Button>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import OAuthAuthentication from "~/models/oauth/OAuthAuthentication";
|
||||
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import MenuItem from "~/components/ContextMenu/MenuItem";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import { useMenuState } from "~/hooks/useMenuState";
|
||||
import { DropdownMenu } from "~/components/Menu/DropdownMenu";
|
||||
import { OverflowMenuButton } from "~/components/Menu/OverflowMenuButton";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { createActionV2 } from "~/actions";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
|
||||
type Props = {
|
||||
/** The OAuthAuthentication to associate with the menu */
|
||||
@@ -15,9 +15,6 @@ type Props = {
|
||||
};
|
||||
|
||||
function OAuthAuthenticationMenu({ oauthAuthentication }: Props) {
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
const { dialogs } = useStores();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -42,15 +39,24 @@ function OAuthAuthenticationMenu({ oauthAuthentication }: Props) {
|
||||
});
|
||||
}, [t, dialogs, oauthAuthentication]);
|
||||
|
||||
const actions = useMemo(
|
||||
() => [
|
||||
createActionV2({
|
||||
name: t("Revoke"),
|
||||
section: "OAuth",
|
||||
dangerous: true,
|
||||
perform: handleRevoke,
|
||||
}),
|
||||
],
|
||||
[t, handleRevoke]
|
||||
);
|
||||
|
||||
const rootAction = useMenuAction(actions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
|
||||
<ContextMenu {...menu}>
|
||||
<MenuItem {...menu} onClick={handleRevoke} dangerous>
|
||||
{t("Revoke")}
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</>
|
||||
<DropdownMenu action={rootAction} ariaLabel={t("Show menu")}>
|
||||
<OverflowMenuButton />
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import OAuthClient from "~/models/oauth/OAuthClient";
|
||||
import OAuthClientDeleteDialog from "~/scenes/Settings/components/OAuthClientDeleteDialog";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import { useMenuState } from "~/hooks/useMenuState";
|
||||
import { DropdownMenu } from "~/components/Menu/DropdownMenu";
|
||||
import { OverflowMenuButton } from "~/components/Menu/OverflowMenuButton";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { settingsPath } from "~/utils/routeHelpers";
|
||||
import {
|
||||
ActionV2Separator,
|
||||
createActionV2,
|
||||
createInternalLinkActionV2,
|
||||
} from "~/actions";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
|
||||
const Section = "OAuth";
|
||||
|
||||
type Props = {
|
||||
/** The oauthClient to associate with the menu */
|
||||
@@ -18,9 +24,6 @@ type Props = {
|
||||
};
|
||||
|
||||
function OAuthClientMenu({ oauthClient, showEdit }: Props) {
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
const { dialogs } = useStores();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -36,32 +39,31 @@ function OAuthClientMenu({ oauthClient, showEdit }: Props) {
|
||||
});
|
||||
}, [t, dialogs, oauthClient]);
|
||||
|
||||
const actions = useMemo(
|
||||
() => [
|
||||
createInternalLinkActionV2({
|
||||
name: `${t("Edit")}…`,
|
||||
section: Section,
|
||||
visible: showEdit,
|
||||
to: settingsPath("applications", oauthClient.id),
|
||||
}),
|
||||
ActionV2Separator,
|
||||
createActionV2({
|
||||
name: `${t("Delete")}…`,
|
||||
section: Section,
|
||||
dangerous: true,
|
||||
perform: handleDelete,
|
||||
}),
|
||||
],
|
||||
[t, showEdit, oauthClient.id, handleDelete]
|
||||
);
|
||||
|
||||
const rootAction = useMenuAction(actions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
|
||||
<ContextMenu {...menu}>
|
||||
<Template
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
type: "route",
|
||||
title: `${t("Edit")}…`,
|
||||
visible: showEdit,
|
||||
to: settingsPath("applications", oauthClient.id),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
dangerous: true,
|
||||
title: `${t("Delete")}…`,
|
||||
onClick: handleDelete,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ContextMenu>
|
||||
</>
|
||||
<DropdownMenu action={rootAction} ariaLabel={t("Show menu")}>
|
||||
<OverflowMenuButton />
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+22
-28
@@ -1,51 +1,45 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Document from "~/models/Document";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import { actionToMenuItem } from "~/actions";
|
||||
import { DropdownMenu } from "~/components/Menu/DropdownMenu";
|
||||
import { OverflowMenuButton } from "~/components/Menu/OverflowMenuButton";
|
||||
import { ActionV2Separator } from "~/actions";
|
||||
import {
|
||||
copyLinkToRevision,
|
||||
restoreRevision,
|
||||
} from "~/actions/definitions/revisions";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import { useMenuState } from "~/hooks/useMenuState";
|
||||
import separator from "./separator";
|
||||
import { useMemo } from "react";
|
||||
import { useMenuAction } from "~/hooks/useMenuAction";
|
||||
|
||||
type Props = {
|
||||
document: Document;
|
||||
revisionId: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function RevisionMenu({ document, className }: Props) {
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
function RevisionMenu({ document }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const context = useActionContext({
|
||||
isContextMenu: true,
|
||||
activeDocumentId: document.id,
|
||||
});
|
||||
|
||||
const actions = useMemo(
|
||||
() => [restoreRevision, ActionV2Separator, copyLinkToRevision],
|
||||
[]
|
||||
);
|
||||
|
||||
const rootAction = useMenuAction(actions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverflowMenuButton
|
||||
className={className}
|
||||
aria-label={t("Show menu")}
|
||||
{...menu}
|
||||
/>
|
||||
<ContextMenu {...menu} aria-label={t("Revision options")}>
|
||||
<Template
|
||||
{...menu}
|
||||
items={[
|
||||
actionToMenuItem(restoreRevision, context),
|
||||
separator(),
|
||||
actionToMenuItem(copyLinkToRevision, context),
|
||||
]}
|
||||
/>
|
||||
</ContextMenu>
|
||||
</>
|
||||
<DropdownMenu
|
||||
action={rootAction}
|
||||
context={context}
|
||||
align="end"
|
||||
ariaLabel={t("Revision options")}
|
||||
>
|
||||
<OverflowMenuButton />
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
TableOfContentsIcon,
|
||||
EditIcon,
|
||||
PlusIcon,
|
||||
MoreIcon,
|
||||
} from "outline-icons";
|
||||
import { TableOfContentsIcon, EditIcon, MoreIcon } from "outline-icons";
|
||||
import { useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -306,20 +301,7 @@ function DocumentHeader({
|
||||
!isCompact &&
|
||||
!isMobile && (
|
||||
<Action>
|
||||
<NewChildDocumentMenu
|
||||
document={document}
|
||||
label={(props) => (
|
||||
<Tooltip
|
||||
content={t("New document")}
|
||||
shortcut="n"
|
||||
placement="bottom"
|
||||
>
|
||||
<Button icon={<PlusIcon />} {...props} neutral>
|
||||
{t("New doc")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
<NewChildDocumentMenu document={document} />
|
||||
</Action>
|
||||
)}
|
||||
{revision && revision.createdAt !== document.updatedAt && (
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ type BaseActionV2 = {
|
||||
type: "action";
|
||||
id: string;
|
||||
analyticsName?: string;
|
||||
name: ((context: ActionContext) => string) | string;
|
||||
name: ((context: ActionContext) => React.ReactNode) | React.ReactNode;
|
||||
section: ((context: ActionContext) => string) | string;
|
||||
shortcut?: string[];
|
||||
keywords?: string;
|
||||
|
||||
Reference in New Issue
Block a user