Compare commits

...

18 Commits

Author SHA1 Message Date
Saumya Pandey 78dd038d2f Scroll smooth 2021-09-12 18:36:41 +05:30
Saumya Pandey 2e0e89b596 Add key prop 2021-09-12 16:05:06 +05:30
Saumya Pandey dbb2f315c3 Remove tabbing 2021-09-12 15:38:22 +05:30
Saumya Pandey 2c89b48b47 Use onMouseEnter 2021-09-12 13:50:02 +05:30
Saumya Pandey 8edd5101f7 Fix tabbing 2021-09-12 13:05:27 +05:30
Saumya Pandey 45fdef9bce Add animation 2021-09-12 12:38:44 +05:30
Saumya Pandey 3a67d94eb0 Restructure the QuickMenuStore 2021-09-11 18:17:59 +05:30
Saumya Pandey 2070b1ffc5 Create converter utility 2021-09-11 18:15:42 +05:30
Saumya Pandey cf69c4949e wip 2021-09-11 02:24:24 +05:30
Saumya Pandey bff1351e66 Merge branch 'main' of https://github.com/outline/outline into feat/issue-2137-iamsaumya 2021-09-01 08:20:48 +05:30
Tom Moor cd9aecf238 debugging 2021-07-04 18:27:26 -04:00
Tom Moor 6012da1405 Merge branch 'main' into feat/issue-2137 2021-07-04 17:48:52 -04:00
Tom Moor 9fb99b925c wip 2021-06-29 19:08:04 -04:00
Tom Moor 405dbc48a6 fix up headers 2021-06-27 18:24:08 -07:00
Tom Moor 8f018a2eee types, self contained 2021-06-27 18:11:53 -07:00
Tom Moor 3a30f80063 wip: First pass menu registration 2021-06-27 16:15:37 -07:00
Tom Moor 400e2b99d0 Merge branch 'main' into feat/issue-2137 2021-06-27 14:56:25 -07:00
Tom Moor 0974f7e296 stash 2021-06-07 22:54:54 -07:00
14 changed files with 813 additions and 268 deletions
+2 -1
View File
@@ -19,6 +19,7 @@ import { type MenuItem as TMenuItem } from "types";
type Props = {|
items: TMenuItem[],
hide?: Function,
|};
const Disclosure = styled(ExpandedIcon)`
@@ -144,7 +145,7 @@ function Template({ items, ...menu }: Props): React.Node {
}
if (item.type === "heading") {
return <Header>{item.title}</Header>;
return <Header key={index}>{item.title}</Header>;
}
console.warn("Unrecognized menu item", item);
+1 -1
View File
@@ -9,7 +9,7 @@ type Props = {|
...InputProps,
placeholder?: string,
value?: string,
onChange: (event: SyntheticInputEvent<>) => mixed,
onChange?: (event: SyntheticInputEvent<>) => mixed,
onKeyDown?: (event: SyntheticKeyboardEvent<HTMLInputElement>) => mixed,
|};
+3 -1
View File
@@ -25,6 +25,7 @@ import Button from "components/Button";
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";
@@ -83,7 +84,7 @@ class Layout extends React.Component<Props> {
this.keyboardShortcutsOpen = false;
};
@keydown(["t", "/", `${meta}+k`])
@keydown(["t", "/"])
goToSearch(ev: SyntheticEvent<>) {
ev.preventDefault();
ev.stopPropagation();
@@ -175,6 +176,7 @@ class Layout extends React.Component<Props> {
>
<KeyboardShortcuts />
</Guide>
<QuickMenu />
</Container>
);
}
+2 -1
View File
@@ -61,7 +61,8 @@ const Modal = ({
<Dialog
{...dialog}
aria-label={title}
preventBodyScrollhideOnEsc
preventBodyScroll
hideOnEsc
hide={onRequestClose}
>
{(props) => (
+292
View File
@@ -0,0 +1,292 @@
// @flow
import { motion } from "framer-motion";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import AutoSizer from "react-virtualized-auto-sizer";
import { Dialog, DialogBackdrop, useDialogState } from "reakit/Dialog";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import styled from "styled-components";
import Header from "components/ContextMenu/Header";
import Flex from "components/Flex";
import Scrollable from "components/Scrollable";
import InputSearch from "../InputSearch";
import useStores from "hooks/useStores";
import { fadeAndSlideUp } from "styles/animations";
function QuickMenu() {
const { quickMenu } = useStores();
const dialog = useDialogState({ modal: true, animated: 250 });
const [isClicked, setIsClicked] = React.useState(false);
const [activeCommand, setActiveCommand] = React.useState<number>(1);
const activeCommandRef = React.useRef();
const { t } = useTranslation();
let order = 0;
React.useEffect(() => {
setActiveCommand(1);
}, [quickMenu.resolvedMenuItems]);
React.useEffect(() => {
if (!dialog.visible) {
quickMenu.reset();
setActiveCommand(1);
}
}, [quickMenu, dialog.visible]);
React.useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === "k") {
dialog.show();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
});
React.useEffect(() => {
if (activeCommandRef.current) {
scrollIntoView(activeCommandRef.current, {
scrollMode: "if-needed",
behavior: "smooth",
});
}
});
const handleChange = (event) => {
event.preventDefault();
event.stopPropagation();
quickMenu.setSearchTerm(event.target.value);
};
const handleKeyDown = (event) => {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
dialog.hide();
quickMenu.reset();
}
if (event.key === "ArrowDown") {
setActiveCommand((prev) => (prev < order ? ++prev : prev));
}
if (event.key === "ArrowUp") {
setActiveCommand((prev) => (prev > 1 ? --prev : prev));
}
if (event.key === "Enter" && activeCommandRef.current) {
activeCommandRef.current.click();
}
};
const handleAnimation = () => {
setIsClicked(true);
setTimeout(() => setIsClicked(false), 100);
};
const constructBlock = (item, order, setActiveCommand) => {
return (
<CommandItem
tabIndex="-1"
data-order={order}
key={order}
role="option"
ref={activeCommand === order ? activeCommandRef : undefined}
onMouseEnter={() => setActiveCommand(order)}
aria-selected={activeCommand === order}
selected={activeCommand === order}
onClick={(e) => {
if (item.items) {
handleAnimation();
quickMenu.handleNestedItems(item);
} else {
dialog.hide();
item.onClick(e);
}
}}
>
<Container align="center">
<MenuIconWrapper>{item.icon}</MenuIconWrapper>
{item.title}
</Container>
</CommandItem>
);
};
const data = quickMenu.resolvedMenuItems?.map((context) => {
return (
<div key={context.title}>
<Header>{context.title}</Header>
<CommandList role="group">
{context.items.map((item) =>
constructBlock(item, ++order, setActiveCommand)
)}
</CommandList>
</div>
);
});
const term = quickMenu.searchTerm;
const variant = {
open: { scale: 0.9, transition: "ease" },
closed: { scale: 1, transition: "ease" },
};
return (
<DialogBackdrop {...dialog}>
{(props) => (
<Backdrop {...props}>
<Dialog
{...dialog}
aria-label={t("Quick menu")}
hideOnEsc
onKeyDown={handleKeyDown}
>
{(props) => (
<motion.div
animate={isClicked ? "open" : "closed"}
variants={variant}
>
<Content {...props} column>
<Badges>
{quickMenu.path.map((b) => (
<Path
onClick={() => {
handleAnimation();
quickMenu.handlePathClick(b);
}}
key={b}
>
{b}
</Path>
))}
</Badges>
<InputWrapper>
<InputSearch onChange={handleChange} value={term} />
</InputWrapper>
<Results>
<AutoSizer>
{({ width, height }) => (
<Wrapper width={width} height={height}>
<Scrollable topShadow>{data}</Scrollable>
</Wrapper>
)}
</AutoSizer>
</Results>
</Content>
</motion.div>
)}
</Dialog>
</Backdrop>
)}
</DialogBackdrop>
);
}
const Path = styled.span`
margin-left: 10px;
padding: 1px 5px 2px;
border: 1px solid;
background-color: ${({ theme }) => theme.slateLight};
color: ${({ theme }) => theme.slateDark};
border: 1px solid ${({ theme }) => theme.slateLight};
border-radius: 4px;
font-size: 14px;
font-weight: 500;
user-select: none;
cursor: pointer;
`;
const Badges = styled.div`
margin-top: 10px;
margin-left: 10px;
`;
const InputWrapper = styled.div`
padding: 16px;
`;
const Results = styled.div`
height: calc(100% - 64px);
width: 100%;
`;
const MenuIconWrapper = styled.span`
width: 24px;
height: 24px;
margin-right: 12px;
`;
const Container = styled(Flex)`
width: 100%;
height: 100%;
`;
const CommandItem = styled.li`
display: flex;
align-items: center;
height: 48px;
font-family: var(--font-main);
font-size: 14px;
border-radius: 4px;
cursor: pointer;
margin: 8px 16px;
padding: 4px;
background: transparent;
color: var(--gray6);
white-space: nowrap;
-webkit-transition: color 0.1s cubic-bezier(0, 0, 0.2, 1);
transition: color 0.1s cubic-bezier(0, 0, 0.2, 1);
cursor: pointer;
outline: none;
${(props) =>
props.selected && {
color: `${props.theme.white}`,
background: `${props.theme.primary}`,
boxShadow: "none",
cursor: "pointer",
svg: {
fill: `${props.theme.white}`,
},
}}
`;
const CommandList = styled.ul`
list-style-type: none;
margin: 0;
padding: 0;
`;
const Wrapper = styled(Flex)`
height: ${(props) => props.height + "px"};
width: ${(props) => props.width + "px"};
flex: 1 1 auto;
`;
const Content = styled(Flex)`
background: ${(props) => props.theme.background};
width: 40vw;
height: 50vh;
border-radius: 8px;
flex: 1;
overflow: hidden;
margin: 20vh auto;
box-shadow: ${(props) => props.theme.menuShadow};
animation: ${fadeAndSlideUp} 200ms ease-in-out;
transform-origin: 25% 0;
`;
const Backdrop = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${(props) => props.theme.backdrop};
z-index: ${(props) => props.theme.depths.modalOverlay};
`;
export default observer(QuickMenu);
+1
View File
@@ -55,6 +55,7 @@ const Wrapper = styled.div`
display: ${(props) => (props.$flex ? "flex" : "block")};
flex-direction: column;
height: 100%;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: none;
+16 -4
View File
@@ -1,9 +1,10 @@
// @flow
import { observer } from "mobx-react";
import { MoonIcon, SunIcon } from "outline-icons";
import { KeyboardIcon, MoonIcon, SunIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { MenuButton, useMenuState } from "reakit/Menu";
import { useHistory } from "react-router";
import { useMenuState, MenuButton } from "reakit/Menu";
import styled from "styled-components";
import {
changelog,
@@ -16,6 +17,7 @@ import KeyboardShortcuts from "scenes/KeyboardShortcuts";
import ContextMenu from "components/ContextMenu";
import Template from "components/ContextMenu/Template";
import Guide from "components/Guide";
import convertToCommandItem from "../utils/convertToCommandItem";
import useBoolean from "hooks/useBoolean";
import useCurrentTeam from "hooks/useCurrentTeam";
import usePrevious from "hooks/usePrevious";
@@ -28,15 +30,16 @@ type Props = {|
function AccountMenu(props: Props) {
const [sessions] = useSessions();
const history = useHistory();
const menu = useMenuState({
unstable_offset: [8, 0],
placement: "bottom-start",
modal: true,
});
const { auth, ui } = useStores();
const { auth, ui, quickMenu } = useStores();
const previousTheme = usePrevious(ui.theme);
const { theme, resolvedTheme } = ui;
const team = useCurrentTeam();
const previousTheme = usePrevious(theme);
const { t } = useTranslation();
const [
keyboardShortcutsOpen,
@@ -63,6 +66,7 @@ function AccountMenu(props: Props) {
{
title: t("Keyboard shortcuts"),
onClick: handleKeyboardShortcutsOpen,
icon: <KeyboardIcon />,
},
{
title: t("API documentation"),
@@ -136,6 +140,14 @@ function AccountMenu(props: Props) {
ui,
]);
React.useEffect(() => {
quickMenu.addContext({
id: "account",
items: convertToCommandItem(items, history),
title: t("Account"),
});
}, [quickMenu, items, t, history]);
return (
<>
<Guide
+54 -44
View File
@@ -22,6 +22,7 @@ import ContextMenu from "components/ContextMenu";
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
import Template from "components/ContextMenu/Template";
import Modal from "components/Modal";
import convertToCommandItem from "../utils/convertToCommandItem";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
@@ -46,9 +47,8 @@ function CollectionMenu({
onClose,
}: Props) {
const menu = useMenuState({ modal, placement });
const [renderModals, setRenderModals] = React.useState(false);
const { ui, documents, policies, quickMenu } = useStores();
const team = useCurrentTeam();
const { documents, policies } = useStores();
const { showToast } = useToasts();
const { t } = useTranslation();
const history = useHistory();
@@ -63,7 +63,6 @@ function CollectionMenu({
const [showCollectionExport, setShowCollectionExport] = React.useState(false);
const handleOpen = React.useCallback(() => {
setRenderModals(true);
if (onOpen) {
onOpen();
}
@@ -180,6 +179,20 @@ function CollectionMenu({
]
);
React.useEffect(() => {
const id = `collection-${collection.id}`;
if (ui.activeCollectionId === collection.id) {
quickMenu.addContext({
id,
items: convertToCommandItem(items, history),
title: t("Collection"),
});
}
return () => quickMenu.removeContext(id);
}, [quickMenu, items, collection.id, ui.activeCollectionId, t, history]);
if (!items.length) {
return null;
}
@@ -209,47 +222,44 @@ function CollectionMenu({
>
<Template {...menu} items={items} />
</ContextMenu>
{renderModals && (
<>
<Modal
title={t("Collection permissions")}
onRequestClose={() => setShowCollectionPermissions(false)}
isOpen={showCollectionPermissions}
>
<CollectionPermissions collection={collection} />
</Modal>
<Modal
title={t("Edit collection")}
isOpen={showCollectionEdit}
onRequestClose={() => setShowCollectionEdit(false)}
>
<CollectionEdit
onSubmit={() => setShowCollectionEdit(false)}
collection={collection}
/>
</Modal>
<Modal
title={t("Delete collection")}
isOpen={showCollectionDelete}
onRequestClose={() => setShowCollectionDelete(false)}
>
<CollectionDelete
onSubmit={() => setShowCollectionDelete(false)}
collection={collection}
/>
</Modal>
<Modal
title={t("Export collection")}
isOpen={showCollectionExport}
onRequestClose={() => setShowCollectionExport(false)}
>
<CollectionExport
onSubmit={() => setShowCollectionExport(false)}
collection={collection}
/>
</Modal>
</>
)}
<Modal
title={t("Collection permissions")}
onRequestClose={() => setShowCollectionPermissions(false)}
isOpen={showCollectionPermissions}
>
<CollectionPermissions collection={collection} />
</Modal>
<Modal
title={t("Edit collection")}
isOpen={showCollectionEdit}
onRequestClose={() => setShowCollectionEdit(false)}
>
<CollectionEdit
onSubmit={() => setShowCollectionEdit(false)}
collection={collection}
/>
</Modal>
<Modal
title={t("Delete collection")}
isOpen={showCollectionDelete}
onRequestClose={() => setShowCollectionDelete(false)}
>
<CollectionDelete
onSubmit={() => setShowCollectionDelete(false)}
collection={collection}
/>
</Modal>
<Modal
title={t("Export collection")}
isOpen={showCollectionExport}
onRequestClose={() => setShowCollectionExport(false)}
>
<CollectionExport
onSubmit={() => setShowCollectionExport(false)}
collection={collection}
/>
</Modal>
</>
);
}
+257 -214
View File
@@ -37,6 +37,7 @@ import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
import Template from "components/ContextMenu/Template";
import Flex from "components/Flex";
import Modal from "components/Modal";
import convertToCommandItem from "../utils/convertToCommandItem";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
import getDataTransferFiles from "utils/getDataTransferFiles";
@@ -72,7 +73,7 @@ function DocumentMenu({
onOpen,
onClose,
}: Props) {
const { policies, collections, documents } = useStores();
const { policies, collections, documents, quickMenu, ui } = useStores();
const { showToast } = useToasts();
const menu = useMenuState({
modal,
@@ -82,7 +83,6 @@ function DocumentMenu({
});
const history = useHistory();
const { t } = useTranslation();
const [renderModals, setRenderModals] = React.useState(false);
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
const [
showPermanentDeleteModal,
@@ -93,7 +93,6 @@ function DocumentMenu({
const file = React.useRef<?HTMLInputElement>();
const handleOpen = React.useCallback(() => {
setRenderModals(true);
if (onOpen) {
onOpen();
}
@@ -234,6 +233,207 @@ function DocumentMenu({
[history, showToast, collection, documents, document.id]
);
const items = React.useMemo(
() => [
{
title: t("Restore 1"),
visible: !!collection && (can.restore || can.unarchive),
onClick: handleRestore,
icon: <RestoreIcon />,
},
{
title: t("Restore 2"),
visible:
!collection &&
(can.restore || can.unarchive) &&
restoreItems.length !== 0,
style: {
left: -170,
position: "relative",
top: -40,
},
icon: <RestoreIcon />,
hover: true,
items: [
{
type: "heading",
title: t("Choose a collection"),
},
...restoreItems,
],
},
{
title: t("Unpin"),
onClick: document.unpin,
visible: !!(showPin && document.pinned && can.unpin),
icon: <PinIcon />,
},
{
title: t("Pin to collection"),
onClick: document.pin,
visible: !!(showPin && !document.pinned && can.pin),
icon: <PinIcon />,
},
{
title: t("Unstar"),
onClick: handleUnstar,
visible: document.isStarred && !!can.unstar,
icon: <UnstarredIcon />,
},
{
title: t("Star"),
onClick: handleStar,
visible: !document.isStarred && !!can.star,
icon: <StarredIcon />,
},
{
type: "separator",
},
{
title: t("Edit"),
to: editDocumentUrl(document),
visible: !!can.update,
icon: <EditIcon />,
},
{
title: t("New nested document"),
to: newDocumentUrl(document.collectionId, {
parentDocumentId: document.id,
}),
visible: !!can.createChildDocument,
icon: <NewDocumentIcon />,
},
{
title: t("Import document"),
visible: can.createChildDocument,
onClick: handleImportDocument,
icon: <ImportIcon />,
},
{
title: `${t("Create template")}`,
onClick: () => setShowTemplateModal(true),
visible: !!can.update && !document.isTemplate,
icon: <ShapesIcon />,
},
{
title: t("Duplicate"),
onClick: handleDuplicate,
visible: !!can.update,
icon: <DuplicateIcon />,
},
{
title: t("Unpublish"),
onClick: handleUnpublish,
visible: !!can.unpublish,
icon: <UnpublishIcon />,
},
{
title: t("Archive"),
onClick: handleArchive,
visible: !!can.archive,
icon: <ArchiveIcon />,
},
{
title: `${t("Delete")}`,
onClick: () => setShowDeleteModal(true),
visible: !!can.delete,
icon: <TrashIcon />,
},
{
title: `${t("Permanently delete")}`,
onClick: () => setShowPermanentDeleteModal(true),
visible: can.permanentDelete,
icon: <CrossIcon />,
},
{
title: `${t("Move")}`,
onClick: () => setShowMoveModal(true),
visible: !!can.move,
icon: <MoveIcon />,
},
{
title: t("Enable embeds"),
onClick: document.enableEmbeds,
visible: !!showToggleEmbeds && document.embedsDisabled,
icon: <BuildingBlocksIcon />,
},
{
title: t("Disable embeds"),
onClick: document.disableEmbeds,
visible: !!showToggleEmbeds && !document.embedsDisabled,
icon: <BuildingBlocksIcon />,
},
{
type: "separator",
},
{
title: t("History"),
to: isRevision ? documentUrl(document) : documentHistoryUrl(document),
visible: canViewHistory,
icon: <HistoryIcon />,
},
{
title: t("Download"),
onClick: document.download,
visible: !!can.download,
icon: <DownloadIcon />,
},
{
title: t("Print"),
onClick: handlePrint,
visible: !!showPrint,
icon: <PrintIcon />,
},
],
[
can.archive,
can.createChildDocument,
can.delete,
can.download,
can.move,
can.permanentDelete,
can.pin,
can.restore,
can.star,
can.unarchive,
can.unpin,
can.unpublish,
can.unstar,
can.update,
canViewHistory,
collection,
document,
handleArchive,
handleDuplicate,
handleImportDocument,
handlePrint,
handleRestore,
handleStar,
handleUnpublish,
handleUnstar,
isRevision,
restoreItems,
showPin,
showPrint,
showToggleEmbeds,
t,
]
);
React.useEffect(() => {
const id = `document-${document.id}`;
if (ui.activeDocumentId === document.id) {
quickMenu.addContext({
id,
items: convertToCommandItem(items, history),
title: t("Document"),
});
}
return () => quickMenu.removeContext(id);
}, [quickMenu, items, document.id, t, ui.activeDocumentId, history]);
return (
<>
<VisuallyHidden>
@@ -261,218 +461,61 @@ function DocumentMenu({
onOpen={handleOpen}
onClose={onClose}
>
<Template
{...menu}
items={[
{
title: t("Restore"),
visible: (!!collection && can.restore) || can.unarchive,
onClick: handleRestore,
icon: <RestoreIcon />,
},
{
title: t("Restore"),
visible:
!collection && !!can.restore && restoreItems.length !== 0,
style: {
left: -170,
position: "relative",
top: -40,
},
icon: <RestoreIcon />,
hover: true,
items: [
{
type: "heading",
title: t("Choose a collection"),
},
...restoreItems,
],
},
{
title: t("Unpin"),
onClick: document.unpin,
visible: !!(showPin && document.pinned && can.unpin),
icon: <PinIcon />,
},
{
title: t("Pin to collection"),
onClick: document.pin,
visible: !!(showPin && !document.pinned && can.pin),
icon: <PinIcon />,
},
{
title: t("Unstar"),
onClick: handleUnstar,
visible: document.isStarred && !!can.unstar,
icon: <UnstarredIcon />,
},
{
title: t("Star"),
onClick: handleStar,
visible: !document.isStarred && !!can.star,
icon: <StarredIcon />,
},
{
type: "separator",
},
{
title: t("Edit"),
to: editDocumentUrl(document),
visible: !!can.update,
icon: <EditIcon />,
},
{
title: t("New nested document"),
to: newDocumentUrl(document.collectionId, {
parentDocumentId: document.id,
}),
visible: !!can.createChildDocument,
icon: <NewDocumentIcon />,
},
{
title: t("Import document"),
visible: can.createChildDocument,
onClick: handleImportDocument,
icon: <ImportIcon />,
},
{
title: `${t("Create template")}`,
onClick: () => setShowTemplateModal(true),
visible: !!can.update && !document.isTemplate,
icon: <ShapesIcon />,
},
{
title: t("Duplicate"),
onClick: handleDuplicate,
visible: !!can.update,
icon: <DuplicateIcon />,
},
{
title: t("Unpublish"),
onClick: handleUnpublish,
visible: !!can.unpublish,
icon: <UnpublishIcon />,
},
{
title: t("Archive"),
onClick: handleArchive,
visible: !!can.archive,
icon: <ArchiveIcon />,
},
{
title: `${t("Delete")}`,
onClick: () => setShowDeleteModal(true),
visible: !!can.delete,
icon: <TrashIcon />,
},
{
title: `${t("Permanently delete")}`,
onClick: () => setShowPermanentDeleteModal(true),
visible: can.permanentDelete,
icon: <CrossIcon />,
},
{
title: `${t("Move")}`,
onClick: () => setShowMoveModal(true),
visible: !!can.move,
icon: <MoveIcon />,
},
{
title: t("Enable embeds"),
onClick: document.enableEmbeds,
visible: !!showToggleEmbeds && document.embedsDisabled,
icon: <BuildingBlocksIcon />,
},
{
title: t("Disable embeds"),
onClick: document.disableEmbeds,
visible: !!showToggleEmbeds && !document.embedsDisabled,
icon: <BuildingBlocksIcon />,
},
{
type: "separator",
},
{
title: t("History"),
to: isRevision
? documentUrl(document)
: documentHistoryUrl(document),
visible: canViewHistory,
icon: <HistoryIcon />,
},
{
title: t("Download"),
onClick: document.download,
visible: !!can.download,
icon: <DownloadIcon />,
},
{
title: t("Print"),
onClick: handlePrint,
visible: !!showPrint,
icon: <PrintIcon />,
},
]}
/>
<Template {...menu} items={items} />
</ContextMenu>
{renderModals && (
<>
{can.move && (
<Modal
title={t("Move {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowMoveModal(false)}
isOpen={showMoveModal}
>
<DocumentMove
document={document}
onRequestClose={() => setShowMoveModal(false)}
/>
</Modal>
)}
{can.delete && (
<Modal
title={t("Delete {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowDeleteModal(false)}
isOpen={showDeleteModal}
>
<DocumentDelete
document={document}
onSubmit={() => setShowDeleteModal(false)}
/>
</Modal>
)}
{can.permanentDelete && (
<Modal
title={t("Permanently delete {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowPermanentDeleteModal(false)}
isOpen={showPermanentDeleteModal}
>
<DocumentPermanentDelete
document={document}
onSubmit={() => setShowPermanentDeleteModal(false)}
/>
</Modal>
)}
{can.update && (
<Modal
title={t("Create template")}
onRequestClose={() => setShowTemplateModal(false)}
isOpen={showTemplateModal}
>
<DocumentTemplatize
document={document}
onSubmit={() => setShowTemplateModal(false)}
/>
</Modal>
)}
</>
{can.move && (
<Modal
title={t("Move {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowMoveModal(false)}
isOpen={showMoveModal}
>
<DocumentMove
document={document}
onRequestClose={() => setShowMoveModal(false)}
/>
</Modal>
)}
{can.delete && (
<Modal
title={t("Delete {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowDeleteModal(false)}
isOpen={showDeleteModal}
>
<DocumentDelete
document={document}
onSubmit={() => setShowDeleteModal(false)}
/>
</Modal>
)}
{can.permanentDelete && (
<Modal
title={t("Permanently delete {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowPermanentDeleteModal(false)}
isOpen={showPermanentDeleteModal}
>
<DocumentPermanentDelete
document={document}
onSubmit={() => setShowPermanentDeleteModal(false)}
/>
</Modal>
)}
{can.update && (
<Modal
title={t("Create template")}
onRequestClose={() => setShowTemplateModal(false)}
isOpen={showTemplateModal}
>
<DocumentTemplatize
document={document}
onSubmit={() => setShowTemplateModal(false)}
/>
</Modal>
)}
</>
);
+103
View File
@@ -0,0 +1,103 @@
// @flow
import { observable, computed, action } from "mobx";
type CommandItem = {|
title: React.Node,
icon?: React.Node,
onClick: (event: SyntheticEvent<>) => void | Promise<void>,
items?: CommandItem[],
|};
type QuickMenuContext = {|
id: string,
title: string,
items: CommandItem[],
priority?: number,
|};
class QuickMenuStore {
@observable searchTerm: string = "";
@observable contextItems: Map<string, QuickMenuContext> = new Map();
@observable states: Map<string, any> = new Map();
@observable path: string[] = ["Home"];
@computed
get resolvedMenuItems(): QuickMenuContext[] {
const currentPath = this.path[this.path.length - 1];
if (currentPath === "Home") {
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()))
);
if (items.length) {
filtered.push({
...context,
items,
});
}
});
return filtered;
} else {
let items = this.states.get(currentPath);
if (!items) return [];
const filtered = items.filter(
(item) =>
!this.searchTerm ||
(typeof item.title === "string" &&
item.title.toLowerCase().includes(this.searchTerm.toLowerCase()))
);
return [{ id: currentPath, title: currentPath, items: filtered }];
}
}
@action
handleNestedItems(item: CommandItem) {
this.states.set(item.title, item.items);
this.path = [...this.path, item.title];
}
@action
setSearchTerm(searchTerm: string): void {
this.searchTerm = searchTerm;
}
@action
addContext(context: QuickMenuContext): void {
this.contextItems.set(context.id, context);
}
@action
removeContext(id: string): void {
this.contextItems.delete(id);
}
@action
reset() {
this.setSearchTerm("");
this.states.clear();
this.path = ["Home"];
}
@action
handlePathClick(toPath: string) {
if (toPath === "Home") {
this.states.clear();
this.path = ["Home"];
} else {
const pathsToRemove = this.path.slice(this.path.indexOf(toPath) + 1);
pathsToRemove.forEach((path) => this.states.delete(path));
this.path = this.path.slice(0, this.path.indexOf(toPath) + 1);
}
}
}
export default QuickMenuStore;
+3
View File
@@ -13,6 +13,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 ToastsStore from "./ToastsStore";
@@ -34,6 +35,7 @@ export default class RootStore {
notificationSettings: NotificationSettingsStore;
presence: DocumentPresenceStore;
policies: PoliciesStore;
quickMenu: QuickMenuStore;
revisions: RevisionsStore;
shares: SharesStore;
ui: UiStore;
@@ -57,6 +59,7 @@ export default class RootStore {
this.memberships = new MembershipsStore(this);
this.notificationSettings = new NotificationSettingsStore(this);
this.presence = new DocumentPresenceStore();
this.quickMenu = new QuickMenuStore();
this.revisions = new RevisionsStore(this);
this.shares = new SharesStore(this);
this.ui = new UiStore();
+3
View File
@@ -15,6 +15,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;
+71
View File
@@ -0,0 +1,71 @@
// @flow
import History from "history";
import { CheckmarkIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import { type MenuItem as TMenuItem } from "types";
type CommandItem = {|
title: React.Node,
icon?: React.Node,
onClick: (event: SyntheticEvent<>) => void | Promise<void>,
items?: CommandItem[],
|};
const convertToCommandItem = (data: TMenuItem[], history: History<>) => {
return data.reduce((agg: CommandItem[], item) => {
if (
item.type === "separator" ||
item.visible === false ||
item.disabled === true ||
item.type === "heading"
) {
return agg;
}
const newItem: CommandItem = {
title: item.title,
icon: item.icon || <MenuIconWrapper />,
onClick: () => {},
};
if (item.selected) {
newItem.icon = (
<MenuIconWrapper>
<CheckmarkIcon color="currentColor" />
</MenuIconWrapper>
);
}
if (item.to) {
newItem.onClick = () => {
history.push(item.to);
};
}
if (item.href) {
newItem.onClick = () => {
window.location.href = item.href;
};
}
if (item.onClick) {
newItem.onClick = item.onClick;
}
if (item.items) {
newItem.onClick = () => {};
newItem.items = convertToCommandItem(item.items);
}
return [...agg, newItem];
}, []);
};
const MenuIconWrapper = styled.span`
width: 24px;
height: 24px;
margin-right: 12px;
`;
export default convertToCommandItem;
+5 -2
View File
@@ -139,6 +139,7 @@
"Dismiss": "Dismiss",
"Keyboard shortcuts": "Keyboard shortcuts",
"Back": "Back",
"Quick menu": "Quick menu",
"Document archived": "Document archived",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "New collection",
@@ -205,8 +206,8 @@
"Document duplicated": "Document duplicated",
"Document restored": "Document restored",
"Document unpublished": "Document unpublished",
"Document options": "Document options",
"Restore": "Restore",
"Restore 1": "Restore 1",
"Restore 2": "Restore 2",
"Choose a collection": "Choose a collection",
"Unpin": "Unpin",
"Pin to collection": "Pin to collection",
@@ -220,6 +221,8 @@
"Disable embeds": "Disable embeds",
"Download": "Download",
"Print": "Print",
"Document": "Document",
"Document options": "Document options",
"Move {{ documentName }}": "Move {{ documentName }}",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "Edit group",