Compare commits

...

8 Commits

Author SHA1 Message Date
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
13 changed files with 559 additions and 318 deletions
+7 -40
View File
@@ -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;
});
}
+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
@@ -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>
);
}
+2 -1
View File
@@ -61,7 +61,8 @@ const Modal = ({
<Dialog
{...dialog}
aria-label={title}
preventBodyScrollhideOnEsc
preventBodyScroll
hideOnEsc
hide={onRequestClose}
>
{(props) => (
+119
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 && (
<>
+81
View File
@@ -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;
+3
View File
@@ -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();
+3
View File
@@ -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;
+40
View File
@@ -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,
|};
+9 -6
View File
@@ -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 }}",