mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd9aecf238 | |||
| 6012da1405 | |||
| 9fb99b925c | |||
| 405dbc48a6 | |||
| 8f018a2eee | |||
| 3a30f80063 | |||
| 400e2b99d0 | |||
| 0974f7e296 |
@@ -9,52 +9,15 @@ import {
|
||||
MenuItem as BaseMenuItem,
|
||||
} from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import Header from "./Header";
|
||||
import MenuItem, { MenuAnchor } from "./MenuItem";
|
||||
import Separator from "./Separator";
|
||||
import ContextMenu from ".";
|
||||
|
||||
type TMenuItem =
|
||||
| {|
|
||||
title: React.Node,
|
||||
to: string,
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
onClick: (event: SyntheticEvent<>) => void | Promise<void>,
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
href: string,
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
visible?: boolean,
|
||||
disabled?: boolean,
|
||||
style?: Object,
|
||||
hover?: boolean,
|
||||
items: TMenuItem[],
|
||||
|}
|
||||
| {|
|
||||
type: "separator",
|
||||
visible?: boolean,
|
||||
|}
|
||||
| {|
|
||||
type: "heading",
|
||||
visible?: boolean,
|
||||
title: React.Node,
|
||||
|};
|
||||
import { type MenuItem as TMenuItem } from "types";
|
||||
|
||||
type Props = {|
|
||||
items: TMenuItem[],
|
||||
hide?: Function,
|
||||
|};
|
||||
|
||||
const Disclosure = styled(ExpandedIcon)`
|
||||
@@ -163,6 +126,10 @@ function Template({ items, ...menu }: Props): React.Node {
|
||||
return <Separator key={index} />;
|
||||
}
|
||||
|
||||
if (item.type === "heading") {
|
||||
return <Header key={index}>{item.title}</Header>;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ type Props = {|
|
||||
...InputProps,
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
onChange: (event: SyntheticInputEvent<>) => mixed,
|
||||
onChange?: (event: SyntheticInputEvent<>) => mixed,
|
||||
onKeyDown?: (event: SyntheticKeyboardEvent<HTMLInputElement>) => mixed,
|
||||
|};
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import DocumentHistory from "components/DocumentHistory";
|
||||
import Flex from "components/Flex";
|
||||
import Guide from "components/Guide";
|
||||
import { LoadingIndicatorBar } from "components/LoadingIndicator";
|
||||
import QuickMenu from "components/QuickMenu";
|
||||
import Sidebar from "components/Sidebar";
|
||||
import SettingsSidebar from "components/Sidebar/Settings";
|
||||
import SkipNavContent from "components/SkipNavContent";
|
||||
@@ -78,7 +79,7 @@ class Layout extends React.Component<Props> {
|
||||
this.keyboardShortcutsOpen = false;
|
||||
};
|
||||
|
||||
@keydown(["t", "/", `${meta}+k`])
|
||||
@keydown(["t", "/"])
|
||||
goToSearch(ev: SyntheticEvent<>) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -168,6 +169,7 @@ class Layout extends React.Component<Props> {
|
||||
>
|
||||
<KeyboardShortcuts />
|
||||
</Guide>
|
||||
<QuickMenu />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ const Modal = ({
|
||||
<Dialog
|
||||
{...dialog}
|
||||
aria-label={title}
|
||||
preventBodyScrollhideOnEsc
|
||||
preventBodyScroll
|
||||
hideOnEsc
|
||||
hide={onRequestClose}
|
||||
>
|
||||
{(props) => (
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dialog, DialogBackdrop, useDialogState } from "reakit/Dialog";
|
||||
import styled from "styled-components";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Scrollable from "components/Scrollable";
|
||||
import useStores from "hooks/useStores";
|
||||
|
||||
function QuickMenu() {
|
||||
const { quickMenu } = useStores();
|
||||
console.log("render");
|
||||
const dialog = useDialogState({ modal: true, animated: 250 });
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSearchChange = React.useCallback(
|
||||
(event) => {
|
||||
quickMenu.setSearchTerm(event.target.value);
|
||||
},
|
||||
[quickMenu]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!dialog.visible) {
|
||||
quickMenu.setSearchTerm("");
|
||||
}
|
||||
}, [quickMenu, dialog.visible]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "k" && event.metaKey) {
|
||||
dialog.visible ? dialog.hide() : dialog.show();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
});
|
||||
|
||||
return (
|
||||
<DialogBackdrop {...dialog}>
|
||||
{(props) => (
|
||||
<Backdrop {...props}>
|
||||
<Dialog
|
||||
{...dialog}
|
||||
aria-label={t("Quick menu")}
|
||||
preventBodyScroll
|
||||
hideOnEsc
|
||||
>
|
||||
{(props) => (
|
||||
<Content {...props}>
|
||||
<InputWrapper>
|
||||
<input
|
||||
type="search"
|
||||
onChange={handleSearchChange}
|
||||
placeholder={`${t("Search actions")}…`}
|
||||
value={quickMenu.searchTerm}
|
||||
/>
|
||||
</InputWrapper>
|
||||
<Results>
|
||||
<Scrollable topShadow>
|
||||
{quickMenu.resolvedMenuItems.map((context) => (
|
||||
<Template
|
||||
key={context.id}
|
||||
{...dialog}
|
||||
items={[
|
||||
{
|
||||
type: "heading",
|
||||
title: context.title,
|
||||
visible: true,
|
||||
},
|
||||
...context.items.filter(
|
||||
// $FlowFixMe
|
||||
(item) => item.type !== "separator"
|
||||
),
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</Scrollable>
|
||||
</Results>
|
||||
</Content>
|
||||
)}
|
||||
</Dialog>
|
||||
</Backdrop>
|
||||
)}
|
||||
</DialogBackdrop>
|
||||
);
|
||||
}
|
||||
|
||||
const InputWrapper = styled.div`
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
const Results = styled.div`
|
||||
height: calc(100% - 64px);
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
background: ${(props) => props.theme.background};
|
||||
width: 40vw;
|
||||
max-height: 50vh;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 20vh auto;
|
||||
box-shadow: ${(props) => props.theme.menuShadow};
|
||||
`;
|
||||
|
||||
const Backdrop = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: ${(props) => props.theme.depths.modalOverlay};
|
||||
`;
|
||||
|
||||
export default observer(QuickMenu);
|
||||
+79
-80
@@ -1,11 +1,9 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { SunIcon, MoonIcon } from "outline-icons";
|
||||
// import { SunIcon, MoonIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
developers,
|
||||
changelog,
|
||||
@@ -15,9 +13,7 @@ import {
|
||||
} from "shared/utils/routeHelpers";
|
||||
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import MenuItem, { MenuAnchor } from "components/ContextMenu/MenuItem";
|
||||
import Separator from "components/ContextMenu/Separator";
|
||||
import Flex from "components/Flex";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Guide from "components/Guide";
|
||||
import usePrevious from "hooks/usePrevious";
|
||||
import useStores from "hooks/useStores";
|
||||
@@ -26,56 +22,12 @@ type Props = {|
|
||||
children: (props: any) => React.Node,
|
||||
|};
|
||||
|
||||
const AppearanceMenu = React.forwardRef((props, ref) => {
|
||||
const { ui } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const menu = useMenuState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuButton ref={ref} {...menu} {...props} onClick={menu.show}>
|
||||
{(props) => (
|
||||
<MenuAnchor {...props}>
|
||||
<ChangeTheme justify="space-between">
|
||||
{t("Appearance")}
|
||||
{ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />}
|
||||
</ChangeTheme>
|
||||
</MenuAnchor>
|
||||
)}
|
||||
</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("Appearance")}>
|
||||
<MenuItem
|
||||
{...menu}
|
||||
onClick={() => ui.setTheme("system")}
|
||||
selected={ui.theme === "system"}
|
||||
>
|
||||
{t("System")}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
{...menu}
|
||||
onClick={() => ui.setTheme("light")}
|
||||
selected={ui.theme === "light"}
|
||||
>
|
||||
{t("Light")}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
{...menu}
|
||||
onClick={() => ui.setTheme("dark")}
|
||||
selected={ui.theme === "dark"}
|
||||
>
|
||||
{t("Dark")}
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function AccountMenu(props: Props) {
|
||||
const menu = useMenuState({
|
||||
placement: "bottom-start",
|
||||
modal: true,
|
||||
});
|
||||
const { auth, ui } = useStores();
|
||||
const { auth, ui, quickMenu } = useStores();
|
||||
const previousTheme = usePrevious(ui.theme);
|
||||
const { t } = useTranslation();
|
||||
const [keyboardShortcutsOpen, setKeyboardShortcutsOpen] = React.useState(
|
||||
@@ -88,6 +40,81 @@ function AccountMenu(props: Props) {
|
||||
}
|
||||
}, [menu, ui.theme, previousTheme]);
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: t("Settings"),
|
||||
to: settings(),
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
title: t("Keyboard shortcuts"),
|
||||
onClick: () => setKeyboardShortcutsOpen(true),
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
title: t("API documentation"),
|
||||
href: developers(),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: t("Changelog"),
|
||||
href: changelog(),
|
||||
},
|
||||
{
|
||||
title: t("Send us feedback"),
|
||||
href: mailToUrl(),
|
||||
},
|
||||
{
|
||||
title: t("Report a bug"),
|
||||
href: githubIssuesUrl(),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: t("Appearance"),
|
||||
// icon: ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />,
|
||||
items: [
|
||||
{
|
||||
title: t("System"),
|
||||
selected: ui.theme === "system",
|
||||
onClick: () => ui.setTheme("system"),
|
||||
},
|
||||
{
|
||||
title: t("Light"),
|
||||
selected: ui.theme === "light",
|
||||
onClick: () => ui.setTheme("light"),
|
||||
},
|
||||
{
|
||||
title: t("Dark"),
|
||||
selected: ui.theme === "dark",
|
||||
onClick: () => ui.setTheme("dark"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: t("Log out"),
|
||||
onClick: auth.logout,
|
||||
},
|
||||
|
||||
// <MenuItem {...menu} as={AppearanceMenu} />
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
quickMenu.addContext({
|
||||
id: "account",
|
||||
items,
|
||||
title: t("Account"),
|
||||
});
|
||||
|
||||
return () => quickMenu.removeContext("account");
|
||||
}, [quickMenu, items, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Guide
|
||||
@@ -99,38 +126,10 @@ function AccountMenu(props: Props) {
|
||||
</Guide>
|
||||
<MenuButton {...menu}>{props.children}</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("Account")}>
|
||||
<MenuItem {...menu} as={Link} to={settings()}>
|
||||
{t("Settings")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} onClick={() => setKeyboardShortcutsOpen(true)}>
|
||||
{t("Keyboard shortcuts")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} href={developers()} target="_blank">
|
||||
{t("API documentation")}
|
||||
</MenuItem>
|
||||
<Separator {...menu} />
|
||||
<MenuItem {...menu} href={changelog()} target="_blank">
|
||||
{t("Changelog")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} href={mailToUrl()} target="_blank">
|
||||
{t("Send us feedback")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} href={githubIssuesUrl()} target="_blank">
|
||||
{t("Report a bug")}
|
||||
</MenuItem>
|
||||
<Separator {...menu} />
|
||||
<MenuItem {...menu} as={AppearanceMenu} />
|
||||
<Separator {...menu} />
|
||||
<MenuItem {...menu} onClick={auth.logout}>
|
||||
{t("Log out")}
|
||||
</MenuItem>
|
||||
<Template {...menu} items={items} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ChangeTheme = styled(Flex)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export default observer(AccountMenu);
|
||||
|
||||
+55
-42
@@ -37,7 +37,7 @@ function CollectionMenu({
|
||||
}: Props) {
|
||||
const menu = useMenuState({ modal, placement });
|
||||
const [renderModals, setRenderModals] = React.useState(false);
|
||||
const { ui, documents, policies } = useStores();
|
||||
const { ui, documents, policies, quickMenu } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
@@ -111,6 +111,59 @@ function CollectionMenu({
|
||||
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: t("New document"),
|
||||
visible: can.update,
|
||||
onClick: handleNewDocument,
|
||||
},
|
||||
{
|
||||
title: t("Import document"),
|
||||
visible: can.update,
|
||||
onClick: handleImportDocument,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: `${t("Edit")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionEdit(true),
|
||||
},
|
||||
{
|
||||
title: `${t("Permissions")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionPermissions(true),
|
||||
},
|
||||
{
|
||||
title: `${t("Export")}…`,
|
||||
visible: !!(collection && can.export),
|
||||
onClick: () => setShowCollectionExport(true),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: `${t("Delete")}…`,
|
||||
visible: !!(collection && can.delete),
|
||||
onClick: () => setShowCollectionDelete(true),
|
||||
},
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
const id = `collection-${collection.id}`;
|
||||
|
||||
if (ui.activeCollectionId === collection.id) {
|
||||
quickMenu.addContext({
|
||||
id,
|
||||
items,
|
||||
title: t("Collection"),
|
||||
});
|
||||
}
|
||||
|
||||
return () => quickMenu.removeContext(id);
|
||||
}, [quickMenu, items, collection.id, ui.activeCollectionId, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VisuallyHidden>
|
||||
@@ -134,47 +187,7 @@ function CollectionMenu({
|
||||
onClose={onClose}
|
||||
aria-label={t("Collection")}
|
||||
>
|
||||
<Template
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
title: t("New document"),
|
||||
visible: can.update,
|
||||
onClick: handleNewDocument,
|
||||
},
|
||||
{
|
||||
title: t("Import document"),
|
||||
visible: can.update,
|
||||
onClick: handleImportDocument,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: `${t("Edit")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionEdit(true),
|
||||
},
|
||||
{
|
||||
title: `${t("Permissions")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionPermissions(true),
|
||||
},
|
||||
{
|
||||
title: `${t("Export")}…`,
|
||||
visible: !!(collection && can.export),
|
||||
onClick: () => setShowCollectionExport(true),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: `${t("Delete")}…`,
|
||||
visible: !!(collection && can.delete),
|
||||
onClick: () => setShowCollectionDelete(true),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Template {...menu} items={items} />
|
||||
</ContextMenu>
|
||||
{renderModals && (
|
||||
<>
|
||||
|
||||
+157
-147
@@ -51,7 +51,7 @@ function DocumentMenu({
|
||||
onOpen,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const { policies, collections, ui, documents } = useStores();
|
||||
const { policies, collections, ui, documents, quickMenu } = useStores();
|
||||
const menu = useMenuState({
|
||||
modal,
|
||||
unstable_preventOverflow: true,
|
||||
@@ -191,6 +191,161 @@ function DocumentMenu({
|
||||
[history, ui, collection, documents, document.id]
|
||||
);
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: t("Restore"),
|
||||
visible: (!!collection && can.restore) || can.unarchive,
|
||||
onClick: handleRestore,
|
||||
},
|
||||
{
|
||||
title: t("Restore"),
|
||||
visible: !collection && !!can.restore,
|
||||
style: {
|
||||
left: -170,
|
||||
position: "relative",
|
||||
top: -40,
|
||||
},
|
||||
hover: true,
|
||||
items: [
|
||||
{
|
||||
type: "heading",
|
||||
title: t("Choose a collection"),
|
||||
},
|
||||
...collections.orderedData.map((collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
return {
|
||||
title: (
|
||||
<Flex align="center">
|
||||
<CollectionIcon collection={collection} />
|
||||
<CollectionName>{collection.name}</CollectionName>
|
||||
</Flex>
|
||||
),
|
||||
onClick: (ev) => handleRestore(ev, { collectionId: collection.id }),
|
||||
disabled: !can.update,
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("Unpin"),
|
||||
onClick: document.unpin,
|
||||
visible: !!(showPin && document.pinned && can.unpin),
|
||||
},
|
||||
{
|
||||
title: t("Pin to collection"),
|
||||
onClick: document.pin,
|
||||
visible: !!(showPin && !document.pinned && can.pin),
|
||||
},
|
||||
{
|
||||
title: t("Unstar"),
|
||||
onClick: handleUnstar,
|
||||
visible: document.isStarred && !!can.unstar,
|
||||
},
|
||||
{
|
||||
title: t("Star"),
|
||||
onClick: handleStar,
|
||||
visible: !document.isStarred && !!can.star,
|
||||
},
|
||||
{
|
||||
title: t("Enable embeds"),
|
||||
onClick: document.enableEmbeds,
|
||||
visible: !!showToggleEmbeds && document.embedsDisabled,
|
||||
},
|
||||
{
|
||||
title: t("Disable embeds"),
|
||||
onClick: document.disableEmbeds,
|
||||
visible: !!showToggleEmbeds && !document.embedsDisabled,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: t("New nested document"),
|
||||
to: newDocumentUrl(document.collectionId, {
|
||||
parentDocumentId: document.id,
|
||||
}),
|
||||
visible: !!can.createChildDocument,
|
||||
},
|
||||
{
|
||||
title: t("Import document"),
|
||||
visible: can.createChildDocument,
|
||||
onClick: handleImportDocument,
|
||||
},
|
||||
{
|
||||
title: `${t("Create template")}…`,
|
||||
onClick: () => setShowTemplateModal(true),
|
||||
visible: !!can.update && !document.isTemplate,
|
||||
},
|
||||
{
|
||||
title: t("Edit"),
|
||||
to: editDocumentUrl(document),
|
||||
visible: !!can.update,
|
||||
},
|
||||
{
|
||||
title: t("Duplicate"),
|
||||
onClick: handleDuplicate,
|
||||
visible: !!can.update,
|
||||
},
|
||||
{
|
||||
title: t("Unpublish"),
|
||||
onClick: handleUnpublish,
|
||||
visible: !!can.unpublish,
|
||||
},
|
||||
{
|
||||
title: t("Archive"),
|
||||
onClick: handleArchive,
|
||||
visible: !!can.archive,
|
||||
},
|
||||
{
|
||||
title: `${t("Delete")}…`,
|
||||
onClick: () => setShowDeleteModal(true),
|
||||
visible: !!can.delete,
|
||||
},
|
||||
{
|
||||
title: `${t("Permanently delete")}…`,
|
||||
onClick: () => setShowPermanentDeleteModal(true),
|
||||
visible: can.permanentDelete,
|
||||
},
|
||||
{
|
||||
title: `${t("Move")}…`,
|
||||
onClick: () => setShowMoveModal(true),
|
||||
visible: !!can.move,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: t("History"),
|
||||
to: isRevision ? documentUrl(document) : documentHistoryUrl(document),
|
||||
visible: canViewHistory,
|
||||
},
|
||||
{
|
||||
title: t("Download"),
|
||||
onClick: document.download,
|
||||
visible: !!can.download,
|
||||
},
|
||||
{
|
||||
title: t("Print"),
|
||||
onClick: handlePrint,
|
||||
visible: !!showPrint,
|
||||
},
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
const id = `document-${document.id}`;
|
||||
|
||||
if (ui.activeDocumentId === document.id) {
|
||||
quickMenu.addContext({
|
||||
id,
|
||||
items,
|
||||
title: t("Document"),
|
||||
});
|
||||
}
|
||||
|
||||
return () => quickMenu.removeContext(id);
|
||||
}, [quickMenu, items, document.id, ui.activeDocumentId, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VisuallyHidden>
|
||||
@@ -218,152 +373,7 @@ function DocumentMenu({
|
||||
onOpen={handleOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Template
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
title: t("Restore"),
|
||||
visible: (!!collection && can.restore) || can.unarchive,
|
||||
onClick: handleRestore,
|
||||
},
|
||||
{
|
||||
title: t("Restore"),
|
||||
visible: !collection && !!can.restore,
|
||||
style: {
|
||||
left: -170,
|
||||
position: "relative",
|
||||
top: -40,
|
||||
},
|
||||
hover: true,
|
||||
items: [
|
||||
{
|
||||
type: "heading",
|
||||
title: t("Choose a collection"),
|
||||
},
|
||||
...collections.orderedData.map((collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
return {
|
||||
title: (
|
||||
<Flex align="center">
|
||||
<CollectionIcon collection={collection} />
|
||||
<CollectionName>{collection.name}</CollectionName>
|
||||
</Flex>
|
||||
),
|
||||
onClick: (ev) =>
|
||||
handleRestore(ev, { collectionId: collection.id }),
|
||||
disabled: !can.update,
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("Unpin"),
|
||||
onClick: document.unpin,
|
||||
visible: !!(showPin && document.pinned && can.unpin),
|
||||
},
|
||||
{
|
||||
title: t("Pin to collection"),
|
||||
onClick: document.pin,
|
||||
visible: !!(showPin && !document.pinned && can.pin),
|
||||
},
|
||||
{
|
||||
title: t("Unstar"),
|
||||
onClick: handleUnstar,
|
||||
visible: document.isStarred && !!can.unstar,
|
||||
},
|
||||
{
|
||||
title: t("Star"),
|
||||
onClick: handleStar,
|
||||
visible: !document.isStarred && !!can.star,
|
||||
},
|
||||
{
|
||||
title: t("Enable embeds"),
|
||||
onClick: document.enableEmbeds,
|
||||
visible: !!showToggleEmbeds && document.embedsDisabled,
|
||||
},
|
||||
{
|
||||
title: t("Disable embeds"),
|
||||
onClick: document.disableEmbeds,
|
||||
visible: !!showToggleEmbeds && !document.embedsDisabled,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: t("New nested document"),
|
||||
to: newDocumentUrl(document.collectionId, {
|
||||
parentDocumentId: document.id,
|
||||
}),
|
||||
visible: !!can.createChildDocument,
|
||||
},
|
||||
{
|
||||
title: t("Import document"),
|
||||
visible: can.createChildDocument,
|
||||
onClick: handleImportDocument,
|
||||
},
|
||||
{
|
||||
title: `${t("Create template")}…`,
|
||||
onClick: () => setShowTemplateModal(true),
|
||||
visible: !!can.update && !document.isTemplate,
|
||||
},
|
||||
{
|
||||
title: t("Edit"),
|
||||
to: editDocumentUrl(document),
|
||||
visible: !!can.update,
|
||||
},
|
||||
{
|
||||
title: t("Duplicate"),
|
||||
onClick: handleDuplicate,
|
||||
visible: !!can.update,
|
||||
},
|
||||
{
|
||||
title: t("Unpublish"),
|
||||
onClick: handleUnpublish,
|
||||
visible: !!can.unpublish,
|
||||
},
|
||||
{
|
||||
title: t("Archive"),
|
||||
onClick: handleArchive,
|
||||
visible: !!can.archive,
|
||||
},
|
||||
{
|
||||
title: `${t("Delete")}…`,
|
||||
onClick: () => setShowDeleteModal(true),
|
||||
visible: !!can.delete,
|
||||
},
|
||||
{
|
||||
title: `${t("Permanently delete")}…`,
|
||||
onClick: () => setShowPermanentDeleteModal(true),
|
||||
visible: can.permanentDelete,
|
||||
},
|
||||
{
|
||||
title: `${t("Move")}…`,
|
||||
onClick: () => setShowMoveModal(true),
|
||||
visible: !!can.move,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: t("History"),
|
||||
to: isRevision
|
||||
? documentUrl(document)
|
||||
: documentHistoryUrl(document),
|
||||
visible: canViewHistory,
|
||||
},
|
||||
{
|
||||
title: t("Download"),
|
||||
onClick: document.download,
|
||||
visible: !!can.download,
|
||||
},
|
||||
{
|
||||
title: t("Print"),
|
||||
onClick: handlePrint,
|
||||
visible: !!showPrint,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Template {...menu} items={items} />
|
||||
</ContextMenu>
|
||||
{renderModals && (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// @flow
|
||||
import { observable, computed, action } from "mobx";
|
||||
import * as React from "react";
|
||||
import { type MenuItem } from "types";
|
||||
|
||||
type QuickMenuContext = {|
|
||||
id: string,
|
||||
title: string,
|
||||
items: MenuItem[],
|
||||
priority?: number,
|
||||
|};
|
||||
|
||||
class QuickMenuStore {
|
||||
@observable searchTerm: string = "";
|
||||
@observable contextItems: Map<string, QuickMenuContext> = new Map();
|
||||
|
||||
@computed
|
||||
get resolvedMenuItems(): QuickMenuContext[] {
|
||||
let filtered = [];
|
||||
|
||||
this.contextItems.forEach((context) => {
|
||||
const items = context.items.filter(
|
||||
(item) =>
|
||||
!this.searchTerm ||
|
||||
(typeof item.title === "string" &&
|
||||
item.title.toLowerCase().includes(this.searchTerm.toLowerCase()))
|
||||
);
|
||||
|
||||
items.forEach((item, index) => {
|
||||
if (item.items && item.title) {
|
||||
items.splice(
|
||||
index,
|
||||
1,
|
||||
item.items.map((child: MenuItem) => {
|
||||
if (!child.title) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return {
|
||||
...child,
|
||||
title: (
|
||||
<>
|
||||
{item.title} > {child.title}
|
||||
</>
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (items.length) {
|
||||
filtered.push({
|
||||
...context,
|
||||
items,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log({ filtered });
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@action
|
||||
setSearchTerm(searchTerm: string): void {
|
||||
console.log({ searchTerm });
|
||||
this.searchTerm = searchTerm;
|
||||
}
|
||||
|
||||
@action
|
||||
addContext(context: QuickMenuContext): void {
|
||||
this.contextItems.set(context.id, context);
|
||||
}
|
||||
|
||||
@action
|
||||
removeContext(id: string): void {
|
||||
this.contextItems.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
export default QuickMenuStore;
|
||||
@@ -11,6 +11,7 @@ import IntegrationsStore from "./IntegrationsStore";
|
||||
import MembershipsStore from "./MembershipsStore";
|
||||
import NotificationSettingsStore from "./NotificationSettingsStore";
|
||||
import PoliciesStore from "./PoliciesStore";
|
||||
import QuickMenuStore from "./QuickMenuStore";
|
||||
import RevisionsStore from "./RevisionsStore";
|
||||
import SharesStore from "./SharesStore";
|
||||
import UiStore from "./UiStore";
|
||||
@@ -30,6 +31,7 @@ export default class RootStore {
|
||||
notificationSettings: NotificationSettingsStore;
|
||||
presence: DocumentPresenceStore;
|
||||
policies: PoliciesStore;
|
||||
quickMenu: QuickMenuStore;
|
||||
revisions: RevisionsStore;
|
||||
shares: SharesStore;
|
||||
ui: UiStore;
|
||||
@@ -49,6 +51,7 @@ export default class RootStore {
|
||||
this.notificationSettings = new NotificationSettingsStore(this);
|
||||
this.presence = new DocumentPresenceStore();
|
||||
this.policies = new PoliciesStore(this);
|
||||
this.quickMenu = new QuickMenuStore();
|
||||
this.revisions = new RevisionsStore(this);
|
||||
this.shares = new SharesStore(this);
|
||||
this.ui = new UiStore();
|
||||
|
||||
@@ -18,6 +18,9 @@ class UiStore {
|
||||
|
||||
// systemTheme represents the system UI theme (Settings -> General in macOS)
|
||||
@observable systemTheme: "light" | "dark";
|
||||
|
||||
// highlighted items in the sidebar, unfortunately this cannot be accurately
|
||||
// inferred from the route at all times so manually controlled in stoer
|
||||
@observable activeDocumentId: ?string;
|
||||
@observable activeCollectionId: ?string;
|
||||
@observable progressBarVisible: boolean = false;
|
||||
|
||||
@@ -58,3 +58,43 @@ export type SearchResult = {
|
||||
context: string,
|
||||
document: Document,
|
||||
};
|
||||
|
||||
export type MenuItem =
|
||||
| {|
|
||||
title: React.Node,
|
||||
to: string,
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
onClick: (event: SyntheticEvent<>) => void | Promise<void>,
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
href: string,
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
visible?: boolean,
|
||||
disabled?: boolean,
|
||||
style?: Object,
|
||||
hover?: boolean,
|
||||
items: MenuItem[],
|
||||
|}
|
||||
| {|
|
||||
type: "separator",
|
||||
visible?: boolean,
|
||||
|}
|
||||
| {|
|
||||
type: "heading",
|
||||
visible?: boolean,
|
||||
title: React.Node,
|
||||
|};
|
||||
|
||||
@@ -109,6 +109,8 @@
|
||||
"Dismiss": "Dismiss",
|
||||
"Keyboard shortcuts": "Keyboard shortcuts",
|
||||
"Back": "Back",
|
||||
"Quick menu": "Quick menu",
|
||||
"Search actions": "Search actions",
|
||||
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
|
||||
"New collection": "New collection",
|
||||
"Collections": "Collections",
|
||||
@@ -139,25 +141,25 @@
|
||||
"Previous page": "Previous page",
|
||||
"Next page": "Next page",
|
||||
"Could not import file": "Could not import file",
|
||||
"Appearance": "Appearance",
|
||||
"System": "System",
|
||||
"Light": "Light",
|
||||
"Dark": "Dark",
|
||||
"API documentation": "API documentation",
|
||||
"Changelog": "Changelog",
|
||||
"Send us feedback": "Send us feedback",
|
||||
"Report a bug": "Report a bug",
|
||||
"Appearance": "Appearance",
|
||||
"System": "System",
|
||||
"Light": "Light",
|
||||
"Dark": "Dark",
|
||||
"Log out": "Log out",
|
||||
"Show path to document": "Show path to document",
|
||||
"Path to document": "Path to document",
|
||||
"Group member options": "Group member options",
|
||||
"Remove": "Remove",
|
||||
"Collection": "Collection",
|
||||
"New document": "New document",
|
||||
"Import document": "Import document",
|
||||
"Edit": "Edit",
|
||||
"Permissions": "Permissions",
|
||||
"Delete": "Delete",
|
||||
"Collection": "Collection",
|
||||
"Collection permissions": "Collection permissions",
|
||||
"Edit collection": "Edit collection",
|
||||
"Delete collection": "Delete collection",
|
||||
@@ -170,7 +172,6 @@
|
||||
"Document archived": "Document archived",
|
||||
"Document restored": "Document restored",
|
||||
"Document unpublished": "Document unpublished",
|
||||
"Document options": "Document options",
|
||||
"Restore": "Restore",
|
||||
"Choose a collection": "Choose a collection",
|
||||
"Unpin": "Unpin",
|
||||
@@ -186,6 +187,8 @@
|
||||
"History": "History",
|
||||
"Download": "Download",
|
||||
"Print": "Print",
|
||||
"Document": "Document",
|
||||
"Document options": "Document options",
|
||||
"Move {{ documentName }}": "Move {{ documentName }}",
|
||||
"Delete {{ documentName }}": "Delete {{ documentName }}",
|
||||
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
|
||||
|
||||
Reference in New Issue
Block a user