mirror of
https://github.com/outline/outline.git
synced 2025-04-14 15:40:59 +00:00
feat: Command Bar (#2669)
This commit is contained in:
67
app/actions/definitions/collections.js
Normal file
67
app/actions/definitions/collections.js
Normal file
@ -0,0 +1,67 @@
|
||||
// @flow
|
||||
import { CollectionIcon, EditIcon, PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import stores from "stores";
|
||||
import CollectionEdit from "scenes/CollectionEdit";
|
||||
import CollectionNew from "scenes/CollectionNew";
|
||||
import DynamicCollectionIcon from "components/CollectionIcon";
|
||||
import { createAction } from "actions";
|
||||
import { CollectionSection } from "actions/sections";
|
||||
import history from "utils/history";
|
||||
|
||||
export const openCollection = createAction({
|
||||
name: ({ t }) => t("Open collection"),
|
||||
section: CollectionSection,
|
||||
shortcut: ["o", "c"],
|
||||
icon: <CollectionIcon />,
|
||||
children: ({ stores }) => {
|
||||
const collections = stores.collections.orderedData;
|
||||
|
||||
return collections.map((collection) => ({
|
||||
id: collection.id,
|
||||
name: collection.name,
|
||||
icon: <DynamicCollectionIcon collection={collection} />,
|
||||
section: CollectionSection,
|
||||
perform: () => history.push(collection.url),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export const createCollection = createAction({
|
||||
name: ({ t }) => t("New collection"),
|
||||
section: CollectionSection,
|
||||
icon: <PlusIcon />,
|
||||
visible: ({ stores }) =>
|
||||
stores.policies.abilities(stores.auth.team?.id || "").createCollection,
|
||||
perform: ({ t, event }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Create a collection"),
|
||||
content: <CollectionNew onSubmit={stores.dialogs.closeAllModals} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const editCollection = createAction({
|
||||
name: ({ t }) => t("Edit collection"),
|
||||
section: CollectionSection,
|
||||
icon: <EditIcon />,
|
||||
visible: ({ stores, activeCollectionId }) =>
|
||||
!!activeCollectionId &&
|
||||
stores.policies.abilities(activeCollectionId).update,
|
||||
perform: ({ t, activeCollectionId }) => {
|
||||
stores.dialogs.openModal({
|
||||
title: t("Edit collection"),
|
||||
content: (
|
||||
<CollectionEdit
|
||||
onSubmit={stores.dialogs.closeAllModals}
|
||||
collectionId={activeCollectionId}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const rootCollectionActions = [openCollection, createCollection];
|
33
app/actions/definitions/debug.js
Normal file
33
app/actions/definitions/debug.js
Normal file
@ -0,0 +1,33 @@
|
||||
// @flow
|
||||
import { ToolsIcon, TrashIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import stores from "stores";
|
||||
import { createAction } from "actions";
|
||||
import { DebugSection } from "actions/sections";
|
||||
import env from "env";
|
||||
import { deleteAllDatabases } from "utils/developer";
|
||||
|
||||
export const clearIndexedDB = createAction({
|
||||
name: ({ t }) => t("Delete IndexedDB cache"),
|
||||
icon: <TrashIcon />,
|
||||
keywords: "cache clear database",
|
||||
section: DebugSection,
|
||||
perform: async ({ t }) => {
|
||||
await deleteAllDatabases();
|
||||
stores.toasts.showToast(t("IndexedDB cache deleted"));
|
||||
},
|
||||
});
|
||||
|
||||
export const development = createAction({
|
||||
name: ({ t }) => t("Development"),
|
||||
keywords: "debug",
|
||||
icon: <ToolsIcon />,
|
||||
iconInContextMenu: false,
|
||||
section: DebugSection,
|
||||
visible: ({ event }) =>
|
||||
env.ENVIRONMENT === "development" ||
|
||||
(event instanceof KeyboardEvent && event.altKey),
|
||||
children: [clearIndexedDB],
|
||||
});
|
||||
|
||||
export const rootDebugActions = [development];
|
88
app/actions/definitions/documents.js
Normal file
88
app/actions/definitions/documents.js
Normal file
@ -0,0 +1,88 @@
|
||||
// @flow
|
||||
import {
|
||||
StarredIcon,
|
||||
DocumentIcon,
|
||||
NewDocumentIcon,
|
||||
ImportIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { createAction } from "actions";
|
||||
import { DocumentSection } from "actions/sections";
|
||||
import getDataTransferFiles from "utils/getDataTransferFiles";
|
||||
import history from "utils/history";
|
||||
import { newDocumentPath } from "utils/routeHelpers";
|
||||
|
||||
export const openDocument = createAction({
|
||||
name: ({ t }) => t("Open document"),
|
||||
section: DocumentSection,
|
||||
shortcut: ["o", "d"],
|
||||
icon: <DocumentIcon />,
|
||||
children: ({ stores }) => {
|
||||
const paths = stores.collections.pathsToDocuments;
|
||||
|
||||
return paths
|
||||
.filter((path) => path.type === "document")
|
||||
.map((path) => ({
|
||||
id: path.id,
|
||||
name: path.title,
|
||||
icon: () =>
|
||||
stores.documents.get(path.id)?.isStarred ? (
|
||||
<StarredIcon />
|
||||
) : undefined,
|
||||
section: DocumentSection,
|
||||
perform: () => history.push(path.url),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export const createDocument = createAction({
|
||||
name: ({ t }) => t("New document"),
|
||||
section: DocumentSection,
|
||||
icon: <NewDocumentIcon />,
|
||||
visible: ({ activeCollectionId, stores }) =>
|
||||
!!activeCollectionId &&
|
||||
stores.policies.abilities(activeCollectionId).update,
|
||||
perform: ({ activeCollectionId }) =>
|
||||
activeCollectionId && history.push(newDocumentPath(activeCollectionId)),
|
||||
});
|
||||
|
||||
export const importDocument = createAction({
|
||||
name: ({ t }) => t("Import document"),
|
||||
section: DocumentSection,
|
||||
icon: <ImportIcon />,
|
||||
visible: ({ activeCollectionId, stores }) =>
|
||||
!!activeCollectionId &&
|
||||
stores.policies.abilities(activeCollectionId).update,
|
||||
perform: ({ activeCollectionId, stores }) => {
|
||||
const { documents, toasts } = stores;
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = documents.importFileTypes.join(", ");
|
||||
input.onchange = async (ev: SyntheticEvent<>) => {
|
||||
const files = getDataTransferFiles(ev);
|
||||
|
||||
try {
|
||||
const file = files[0];
|
||||
const document = await documents.import(
|
||||
file,
|
||||
null,
|
||||
activeCollectionId,
|
||||
{
|
||||
publish: true,
|
||||
}
|
||||
);
|
||||
history.push(document.url);
|
||||
} catch (err) {
|
||||
toasts.showToast(err.message, {
|
||||
type: "error",
|
||||
});
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
},
|
||||
});
|
||||
|
||||
export const rootDocumentActions = [openDocument, importDocument];
|
159
app/actions/definitions/navigation.js
Normal file
159
app/actions/definitions/navigation.js
Normal file
@ -0,0 +1,159 @@
|
||||
// @flow
|
||||
import {
|
||||
HomeIcon,
|
||||
SearchIcon,
|
||||
ArchiveIcon,
|
||||
TrashIcon,
|
||||
EditIcon,
|
||||
OpenIcon,
|
||||
SettingsIcon,
|
||||
ShapesIcon,
|
||||
KeyboardIcon,
|
||||
EmailIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import {
|
||||
developersUrl,
|
||||
changelogUrl,
|
||||
mailToUrl,
|
||||
githubIssuesUrl,
|
||||
} from "shared/utils/routeHelpers";
|
||||
import stores from "stores";
|
||||
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||
import { createAction } from "actions";
|
||||
import { NavigationSection } from "actions/sections";
|
||||
import history from "utils/history";
|
||||
import {
|
||||
settingsPath,
|
||||
homePath,
|
||||
searchUrl,
|
||||
draftsPath,
|
||||
templatesPath,
|
||||
archivePath,
|
||||
trashPath,
|
||||
} from "utils/routeHelpers";
|
||||
|
||||
export const navigateToHome = createAction({
|
||||
name: ({ t }) => t("Home"),
|
||||
section: NavigationSection,
|
||||
shortcut: ["d"],
|
||||
icon: <HomeIcon />,
|
||||
perform: () => history.push(homePath()),
|
||||
visible: ({ location }) => location.pathname !== homePath(),
|
||||
});
|
||||
|
||||
export const navigateToSearch = createAction({
|
||||
name: ({ t }) => t("Search"),
|
||||
section: NavigationSection,
|
||||
shortcut: ["/"],
|
||||
icon: <SearchIcon />,
|
||||
perform: () => history.push(searchUrl()),
|
||||
visible: ({ location }) => location.pathname !== searchUrl(),
|
||||
});
|
||||
|
||||
export const navigateToDrafts = createAction({
|
||||
name: ({ t }) => t("Drafts"),
|
||||
section: NavigationSection,
|
||||
icon: <EditIcon />,
|
||||
perform: () => history.push(draftsPath()),
|
||||
visible: ({ location }) => location.pathname !== draftsPath(),
|
||||
});
|
||||
|
||||
export const navigateToTemplates = createAction({
|
||||
name: ({ t }) => t("Templates"),
|
||||
section: NavigationSection,
|
||||
icon: <ShapesIcon />,
|
||||
perform: () => history.push(templatesPath()),
|
||||
visible: ({ location }) => location.pathname !== templatesPath(),
|
||||
});
|
||||
|
||||
export const navigateToArchive = createAction({
|
||||
name: ({ t }) => t("Archive"),
|
||||
section: NavigationSection,
|
||||
icon: <ArchiveIcon />,
|
||||
perform: () => history.push(archivePath()),
|
||||
visible: ({ location }) => location.pathname !== archivePath(),
|
||||
});
|
||||
|
||||
export const navigateToTrash = createAction({
|
||||
name: ({ t }) => t("Trash"),
|
||||
section: NavigationSection,
|
||||
icon: <TrashIcon />,
|
||||
perform: () => history.push(trashPath()),
|
||||
visible: ({ location }) => location.pathname !== trashPath(),
|
||||
});
|
||||
|
||||
export const navigateToSettings = createAction({
|
||||
name: ({ t }) => t("Settings"),
|
||||
section: NavigationSection,
|
||||
shortcut: ["g", "s"],
|
||||
iconInContextMenu: false,
|
||||
icon: <SettingsIcon />,
|
||||
perform: () => history.push(settingsPath()),
|
||||
});
|
||||
|
||||
export const openAPIDocumentation = createAction({
|
||||
name: ({ t }) => t("API documentation"),
|
||||
section: NavigationSection,
|
||||
iconInContextMenu: false,
|
||||
icon: <OpenIcon />,
|
||||
perform: () => window.open(developersUrl()),
|
||||
});
|
||||
|
||||
export const openFeedbackUrl = createAction({
|
||||
name: ({ t }) => t("Send us feedback"),
|
||||
section: NavigationSection,
|
||||
iconInContextMenu: false,
|
||||
icon: <EmailIcon />,
|
||||
perform: () => window.open(mailToUrl()),
|
||||
});
|
||||
|
||||
export const openBugReportUrl = createAction({
|
||||
name: ({ t }) => t("Report a bug"),
|
||||
section: NavigationSection,
|
||||
perform: () => window.open(githubIssuesUrl()),
|
||||
});
|
||||
|
||||
export const openChangelog = createAction({
|
||||
name: ({ t }) => t("Changelog"),
|
||||
section: NavigationSection,
|
||||
iconInContextMenu: false,
|
||||
icon: <OpenIcon />,
|
||||
perform: () => window.open(changelogUrl()),
|
||||
});
|
||||
|
||||
export const openKeyboardShortcuts = createAction({
|
||||
name: ({ t }) => t("Keyboard shortcuts"),
|
||||
section: NavigationSection,
|
||||
shortcut: ["?"],
|
||||
iconInContextMenu: false,
|
||||
icon: <KeyboardIcon />,
|
||||
perform: ({ t }) => {
|
||||
stores.dialogs.openGuide({
|
||||
title: t("Keyboard shortcuts"),
|
||||
content: <KeyboardShortcuts />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const logout = createAction({
|
||||
name: ({ t }) => t("Log out"),
|
||||
section: NavigationSection,
|
||||
perform: () => stores.auth.logout(),
|
||||
});
|
||||
|
||||
export const rootNavigationActions = [
|
||||
navigateToHome,
|
||||
navigateToSearch,
|
||||
navigateToDrafts,
|
||||
navigateToTemplates,
|
||||
navigateToArchive,
|
||||
navigateToTrash,
|
||||
navigateToSettings,
|
||||
openAPIDocumentation,
|
||||
openFeedbackUrl,
|
||||
openBugReportUrl,
|
||||
openChangelog,
|
||||
openKeyboardShortcuts,
|
||||
logout,
|
||||
];
|
48
app/actions/definitions/settings.js
Normal file
48
app/actions/definitions/settings.js
Normal file
@ -0,0 +1,48 @@
|
||||
// @flow
|
||||
import { SunIcon, MoonIcon, BrowserIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import stores from "stores";
|
||||
import { createAction } from "actions";
|
||||
import { SettingsSection } from "actions/sections";
|
||||
|
||||
export const changeToDarkTheme = createAction({
|
||||
name: ({ t }) => t("Dark"),
|
||||
icon: <MoonIcon />,
|
||||
iconInContextMenu: false,
|
||||
keywords: "theme dark night",
|
||||
section: SettingsSection,
|
||||
selected: () => stores.ui.theme === "dark",
|
||||
perform: () => stores.ui.setTheme("dark"),
|
||||
});
|
||||
|
||||
export const changeToLightTheme = createAction({
|
||||
name: ({ t }) => t("Light"),
|
||||
icon: <SunIcon />,
|
||||
iconInContextMenu: false,
|
||||
keywords: "theme light day",
|
||||
section: SettingsSection,
|
||||
selected: () => stores.ui.theme === "light",
|
||||
perform: () => stores.ui.setTheme("light"),
|
||||
});
|
||||
|
||||
export const changeToSystemTheme = createAction({
|
||||
name: ({ t }) => t("System"),
|
||||
icon: <BrowserIcon />,
|
||||
iconInContextMenu: false,
|
||||
keywords: "theme system default",
|
||||
section: SettingsSection,
|
||||
selected: () => stores.ui.theme === "system",
|
||||
perform: () => stores.ui.setTheme("system"),
|
||||
});
|
||||
|
||||
export const changeTheme = createAction({
|
||||
name: ({ t }) => t("Change theme"),
|
||||
placeholder: ({ t }) => t("Change theme to"),
|
||||
icon: () =>
|
||||
stores.ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />,
|
||||
keywords: "appearance display",
|
||||
section: SettingsSection,
|
||||
children: [changeToLightTheme, changeToDarkTheme, changeToSystemTheme],
|
||||
});
|
||||
|
||||
export const rootSettingsActions = [changeTheme];
|
24
app/actions/definitions/users.js
Normal file
24
app/actions/definitions/users.js
Normal file
@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import stores from "stores";
|
||||
import Invite from "scenes/Invite";
|
||||
import { createAction } from "actions";
|
||||
import { UserSection } from "actions/sections";
|
||||
|
||||
export const inviteUser = createAction({
|
||||
name: ({ t }) => `${t("Invite people")}…`,
|
||||
icon: <PlusIcon />,
|
||||
keywords: "team member user",
|
||||
section: UserSection,
|
||||
visible: ({ stores }) =>
|
||||
stores.policies.abilities(stores.auth.team?.id || "").inviteUser,
|
||||
perform: ({ t }) => {
|
||||
stores.dialogs.openModal({
|
||||
title: t("Invite people"),
|
||||
content: <Invite onSubmit={stores.dialogs.closeAllModals} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const rootUserActions = [inviteUser];
|
117
app/actions/index.js
Normal file
117
app/actions/index.js
Normal file
@ -0,0 +1,117 @@
|
||||
// @flow
|
||||
import { flattenDeep } from "lodash";
|
||||
import * as React from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import type {
|
||||
Action,
|
||||
ActionContext,
|
||||
CommandBarAction,
|
||||
MenuItemClickable,
|
||||
MenuItemWithChildren,
|
||||
} from "types";
|
||||
|
||||
export function createAction(
|
||||
definition: $Diff<Action, { id?: string }>
|
||||
): Action {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
...definition,
|
||||
};
|
||||
}
|
||||
|
||||
export function actionToMenuItem(
|
||||
action: Action,
|
||||
context: ActionContext
|
||||
): MenuItemClickable | MenuItemWithChildren {
|
||||
function resolve<T>(value: any): T {
|
||||
if (typeof value === "function") {
|
||||
return value(context);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
const resolvedIcon = resolve<React.Element<any>>(action.icon);
|
||||
const resolvedChildren = resolve<Action[]>(action.children);
|
||||
|
||||
const visible = action.visible ? action.visible(context) : true;
|
||||
const title = resolve<string>(action.name);
|
||||
const icon =
|
||||
resolvedIcon && action.iconInContextMenu !== false
|
||||
? React.cloneElement(resolvedIcon, { color: "currentColor" })
|
||||
: undefined;
|
||||
|
||||
if (resolvedChildren) {
|
||||
return {
|
||||
title,
|
||||
icon,
|
||||
items: resolvedChildren
|
||||
.map((a) => actionToMenuItem(a, context))
|
||||
.filter((a) => !!a),
|
||||
visible,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
icon,
|
||||
visible,
|
||||
onClick: () => action.perform && action.perform(context),
|
||||
selected: action.selected ? action.selected(context) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function actionToKBar(
|
||||
action: Action,
|
||||
context: ActionContext
|
||||
): CommandBarAction[] {
|
||||
function resolve<T>(value: any): T {
|
||||
if (typeof value === "function") {
|
||||
return value(context);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof action.visible === "function" && !action.visible(context)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const resolvedIcon = resolve<React.Element<any>>(action.icon);
|
||||
const resolvedChildren = resolve<Action[]>(action.children);
|
||||
const resolvedSection = resolve<string>(action.section);
|
||||
const resolvedName = resolve<string>(action.name);
|
||||
const resolvedPlaceholder = resolve<string>(action.placeholder);
|
||||
|
||||
const children = resolvedChildren
|
||||
? flattenDeep(resolvedChildren.map((a) => actionToKBar(a, context))).filter(
|
||||
(a) => !!a
|
||||
)
|
||||
: [];
|
||||
|
||||
return [
|
||||
{
|
||||
id: action.id,
|
||||
name: resolvedName,
|
||||
section: resolvedSection,
|
||||
placeholder: resolvedPlaceholder,
|
||||
keywords: `${action.keywords || ""} ${children
|
||||
.filter((c) => !!c.keywords)
|
||||
.map((c) => c.keywords)
|
||||
.join(" ")}`,
|
||||
shortcut: action.shortcut,
|
||||
icon: resolvedIcon
|
||||
? React.cloneElement(resolvedIcon, { color: "currentColor" })
|
||||
: undefined,
|
||||
perform: action.perform
|
||||
? () => action.perform && action.perform(context)
|
||||
: undefined,
|
||||
children: children.length ? children.map((a) => a.id) : undefined,
|
||||
},
|
||||
].concat(
|
||||
children.map((child) => ({
|
||||
...child,
|
||||
parent: action.id,
|
||||
}))
|
||||
);
|
||||
}
|
16
app/actions/root.js
Normal file
16
app/actions/root.js
Normal file
@ -0,0 +1,16 @@
|
||||
// @flow
|
||||
import { rootCollectionActions } from "./definitions/collections";
|
||||
import { rootDebugActions } from "./definitions/debug";
|
||||
import { rootDocumentActions } from "./definitions/documents";
|
||||
import { rootNavigationActions } from "./definitions/navigation";
|
||||
import { rootSettingsActions } from "./definitions/settings";
|
||||
import { rootUserActions } from "./definitions/users";
|
||||
|
||||
export default [
|
||||
...rootCollectionActions,
|
||||
...rootDocumentActions,
|
||||
...rootUserActions,
|
||||
...rootNavigationActions,
|
||||
...rootSettingsActions,
|
||||
...rootDebugActions,
|
||||
];
|
14
app/actions/sections.js
Normal file
14
app/actions/sections.js
Normal file
@ -0,0 +1,14 @@
|
||||
// @flow
|
||||
import { type ActionContext } from "types";
|
||||
|
||||
export const CollectionSection = ({ t }: ActionContext) => t("Collection");
|
||||
|
||||
export const DebugSection = ({ t }: ActionContext) => t("Debug");
|
||||
|
||||
export const DocumentSection = ({ t }: ActionContext) => t("Document");
|
||||
|
||||
export const SettingsSection = ({ t }: ActionContext) => t("Settings");
|
||||
|
||||
export const NavigationSection = ({ t }: ActionContext) => t("Navigation");
|
||||
|
||||
export const UserSection = ({ t }: ActionContext) => t("People");
|
87
app/components/CommandBar.js
Normal file
87
app/components/CommandBar.js
Normal file
@ -0,0 +1,87 @@
|
||||
// @flow
|
||||
import { useKBar, KBarPositioner, KBarAnimator, KBarSearch } from "kbar";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Portal } from "react-portal";
|
||||
import styled from "styled-components";
|
||||
import CommandBarResults from "components/CommandBarResults";
|
||||
import rootActions from "actions/root";
|
||||
import useCommandBarActions from "hooks/useCommandBarActions";
|
||||
|
||||
export const CommandBarOptions = {
|
||||
animations: {
|
||||
enterMs: 250,
|
||||
exitMs: 200,
|
||||
},
|
||||
};
|
||||
|
||||
function CommandBar() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useCommandBarActions(rootActions);
|
||||
|
||||
const { rootAction } = useKBar((state) => ({
|
||||
rootAction: state.actions[state.currentRootActionId],
|
||||
}));
|
||||
|
||||
return (
|
||||
<KBarPortal>
|
||||
<Positioner>
|
||||
<Animator>
|
||||
<SearchInput
|
||||
placeholder={`${
|
||||
rootAction?.placeholder ||
|
||||
rootAction?.name ||
|
||||
t("Type a command or search")
|
||||
}…`}
|
||||
/>
|
||||
<CommandBarResults />
|
||||
</Animator>
|
||||
</Positioner>
|
||||
</KBarPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function KBarPortal({ children }: { children: React.Node }) {
|
||||
const { showing } = useKBar((state) => ({
|
||||
showing: state.visualState !== "hidden",
|
||||
}));
|
||||
|
||||
if (!showing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Portal>{children}</Portal>;
|
||||
}
|
||||
|
||||
const Positioner = styled(KBarPositioner)`
|
||||
z-index: ${(props) => props.theme.depths.commandBar};
|
||||
`;
|
||||
|
||||
const SearchInput = styled(KBarSearch)`
|
||||
padding: 16px 20px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: ${(props) => props.theme.menuBackground};
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
&:disabled,
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.placeholder};
|
||||
}
|
||||
`;
|
||||
|
||||
const Animator = styled(KBarAnimator)`
|
||||
max-width: 540px;
|
||||
max-height: 75vh;
|
||||
width: 90vw;
|
||||
background: ${(props) => props.theme.menuBackground};
|
||||
color: ${(props) => props.theme.text};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: rgb(0 0 0 / 40%) 0px 16px 60px;
|
||||
`;
|
||||
|
||||
export default observer(CommandBar);
|
60
app/components/CommandBarItem.js
Normal file
60
app/components/CommandBarItem.js
Normal file
@ -0,0 +1,60 @@
|
||||
// @flow
|
||||
import { BackIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import Key from "components/Key";
|
||||
import type { CommandBarAction } from "types";
|
||||
|
||||
type Props = {|
|
||||
action: CommandBarAction,
|
||||
active: Boolean,
|
||||
|};
|
||||
|
||||
function CommandBarItem({ action, active }: Props, ref) {
|
||||
return (
|
||||
<Item active={active} ref={ref}>
|
||||
<Flex align="center" gap={8}>
|
||||
<Icon>
|
||||
{action.icon ? (
|
||||
React.cloneElement(action.icon, { size: 22 })
|
||||
) : (
|
||||
<ForwardIcon color="currentColor" size={22} />
|
||||
)}
|
||||
</Icon>
|
||||
{action.name}
|
||||
{action.children?.length ? "…" : ""}
|
||||
</Flex>
|
||||
{action.shortcut?.length ? (
|
||||
<div style={{ display: "grid", gridAutoFlow: "column", gap: "4px" }}>
|
||||
{action.shortcut.map((sc) => (
|
||||
<Key key={sc}>{sc}</Key>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</Item>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = styled.div`
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: ${(props) => props.theme.textSecondary};
|
||||
`;
|
||||
|
||||
const Item = styled.div`
|
||||
font-size: 15px;
|
||||
padding: 12px 16px;
|
||||
background: ${(props) =>
|
||||
props.active ? props.theme.menuItemSelected : "none"};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const ForwardIcon = styled(BackIcon)`
|
||||
transform: rotate(180deg);
|
||||
`;
|
||||
|
||||
export default React.forwardRef<Props, HTMLDivElement>(CommandBarItem);
|
44
app/components/CommandBarResults.js
Normal file
44
app/components/CommandBarResults.js
Normal file
@ -0,0 +1,44 @@
|
||||
// @flow
|
||||
import { useMatches, KBarResults, NO_GROUP } from "kbar";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import CommandBarItem from "components/CommandBarItem";
|
||||
|
||||
export default function CommandBarResults() {
|
||||
const matches = useMatches();
|
||||
const items = React.useMemo(
|
||||
() =>
|
||||
matches
|
||||
.reduce((acc, curr) => {
|
||||
const { actions, name } = curr;
|
||||
acc.push(name);
|
||||
acc.push(...actions);
|
||||
return acc;
|
||||
}, [])
|
||||
.filter((i) => i !== NO_GROUP),
|
||||
[matches]
|
||||
);
|
||||
|
||||
return (
|
||||
<KBarResults
|
||||
items={items}
|
||||
maxHeight={400}
|
||||
onRender={({ item, active }) =>
|
||||
typeof item === "string" ? (
|
||||
<Header>{item}</Header>
|
||||
) : (
|
||||
<CommandBarItem action={item} active={active} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Header = styled.h3`
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.04em;
|
||||
margin: 0;
|
||||
padding: 16px 0 4px 20px;
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
height: 36px;
|
||||
`;
|
@ -2,7 +2,7 @@
|
||||
import { ExpandedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import {
|
||||
useMenuState,
|
||||
MenuButton,
|
||||
@ -15,10 +15,20 @@ import Header from "./Header";
|
||||
import MenuItem, { MenuAnchor } from "./MenuItem";
|
||||
import Separator from "./Separator";
|
||||
import ContextMenu from ".";
|
||||
import { type MenuItem as TMenuItem } from "types";
|
||||
import { actionToMenuItem } from "actions";
|
||||
import useStores from "hooks/useStores";
|
||||
import type {
|
||||
MenuItem as TMenuItem,
|
||||
Action,
|
||||
ActionContext,
|
||||
MenuSeparator,
|
||||
MenuHeading,
|
||||
} from "types";
|
||||
|
||||
type Props = {|
|
||||
items: TMenuItem[],
|
||||
actions: (Action | MenuSeparator | MenuHeading)[],
|
||||
context?: $Shape<ActionContext>,
|
||||
|};
|
||||
|
||||
const Disclosure = styled(ExpandedIcon)`
|
||||
@ -68,8 +78,30 @@ export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function Template({ items, ...menu }: Props): React.Node {
|
||||
const filteredTemplates = filterTemplateItems(items);
|
||||
function Template({ items, actions, context, ...menu }: Props): React.Node {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const stores = useStores();
|
||||
const { ui } = stores;
|
||||
|
||||
const ctx = {
|
||||
t,
|
||||
isCommandBar: false,
|
||||
isContextMenu: true,
|
||||
activeCollectionId: ui.activeCollectionId,
|
||||
activeDocumentId: ui.activeDocumentId,
|
||||
location,
|
||||
stores,
|
||||
...context,
|
||||
};
|
||||
|
||||
const filteredTemplates = filterTemplateItems(
|
||||
actions
|
||||
? actions.map((action) =>
|
||||
action.type ? action : actionToMenuItem(action, ctx)
|
||||
)
|
||||
: items
|
||||
);
|
||||
const iconIsPresentInAnyMenuItem = filteredTemplates.find(
|
||||
(item) => !item.type && !!item.icon
|
||||
);
|
||||
|
@ -123,6 +123,7 @@ export const Background = styled.div`
|
||||
border-radius: 6px;
|
||||
padding: 6px 0;
|
||||
min-width: 180px;
|
||||
min-height: 44px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 75vh;
|
||||
|
37
app/components/Dialogs.js
Normal file
37
app/components/Dialogs.js
Normal file
@ -0,0 +1,37 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react-lite";
|
||||
import * as React from "react";
|
||||
import Guide from "components/Guide";
|
||||
import Modal from "components/Modal";
|
||||
import useStores from "hooks/useStores";
|
||||
|
||||
function Dialogs() {
|
||||
const { dialogs } = useStores();
|
||||
const { guide, modalStack } = dialogs;
|
||||
|
||||
return (
|
||||
<>
|
||||
{guide ? (
|
||||
<Guide
|
||||
isOpen={guide.isOpen}
|
||||
onRequestClose={dialogs.closeGuide}
|
||||
title={guide.title}
|
||||
>
|
||||
{guide.content}
|
||||
</Guide>
|
||||
) : undefined}
|
||||
{[...modalStack].map(([id, modal]) => (
|
||||
<Modal
|
||||
key={id}
|
||||
isOpen={modal.isOpen}
|
||||
onRequestClose={() => dialogs.closeModal(id)}
|
||||
title={modal.title}
|
||||
>
|
||||
{modal.content}
|
||||
</Modal>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Dialogs);
|
@ -20,7 +20,7 @@ import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useCurrentUser from "hooks/useCurrentUser";
|
||||
import useStores from "hooks/useStores";
|
||||
import DocumentMenu from "menus/DocumentMenu";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
import { newDocumentPath } from "utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
@ -132,7 +132,7 @@ function DocumentListItem(props: Props, ref) {
|
||||
<>
|
||||
<Button
|
||||
as={Link}
|
||||
to={newDocumentUrl(document.collectionId, {
|
||||
to={newDocumentPath(document.collectionId, {
|
||||
templateId: document.id,
|
||||
})}
|
||||
icon={<PlusIcon />}
|
||||
|
@ -162,8 +162,6 @@ const CardContent = styled.div`
|
||||
const Card = styled.div`
|
||||
backdrop-filter: blur(10px);
|
||||
background: ${(props) => props.theme.background};
|
||||
border: ${(props) =>
|
||||
props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 30px 90px -20px rgba(0, 0, 0, 0.3),
|
||||
0 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
|
@ -4,12 +4,11 @@ import { observer, inject } from "mobx-react";
|
||||
import { MenuIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { withTranslation, type TFunction } from "react-i18next";
|
||||
import { withTranslation } from "react-i18next";
|
||||
import keydown from "react-keydown";
|
||||
import {
|
||||
Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
withRouter,
|
||||
type RouterHistory,
|
||||
} from "react-router-dom";
|
||||
@ -20,21 +19,20 @@ import DocumentsStore from "stores/DocumentsStore";
|
||||
import PoliciesStore from "stores/PoliciesStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
import ErrorSuspended from "scenes/ErrorSuspended";
|
||||
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||
import Button from "components/Button";
|
||||
import Flex from "components/Flex";
|
||||
import Guide from "components/Guide";
|
||||
import { LoadingIndicatorBar } from "components/LoadingIndicator";
|
||||
import Sidebar from "components/Sidebar";
|
||||
import SettingsSidebar from "components/Sidebar/Settings";
|
||||
import SkipNavContent from "components/SkipNavContent";
|
||||
import SkipNavLink from "components/SkipNavLink";
|
||||
import env from "env";
|
||||
import { meta } from "utils/keyboard";
|
||||
import {
|
||||
homeUrl,
|
||||
searchUrl,
|
||||
matchDocumentSlug as slug,
|
||||
newDocumentUrl,
|
||||
newDocumentPath,
|
||||
settingsPath,
|
||||
} from "utils/routeHelpers";
|
||||
|
||||
const DocumentHistory = React.lazy(() =>
|
||||
@ -43,6 +41,13 @@ const DocumentHistory = React.lazy(() =>
|
||||
)
|
||||
);
|
||||
|
||||
const CommandBar = React.lazy(() =>
|
||||
import(
|
||||
/* webpackChunkName: "command-bar" */
|
||||
"components/CommandBar"
|
||||
)
|
||||
);
|
||||
|
||||
type Props = {
|
||||
documents: DocumentsStore,
|
||||
children?: ?React.Node,
|
||||
@ -53,46 +58,27 @@ type Props = {
|
||||
history: RouterHistory,
|
||||
policies: PoliciesStore,
|
||||
notifications?: React.Node,
|
||||
i18n: Object,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Layout extends React.Component<Props> {
|
||||
scrollable: ?HTMLDivElement;
|
||||
@observable redirectTo: ?string;
|
||||
@observable keyboardShortcutsOpen: boolean = false;
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.redirectTo) {
|
||||
this.redirectTo = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@keydown(`${meta}+.`)
|
||||
handleToggleSidebar() {
|
||||
this.props.ui.toggleCollapsedSidebar();
|
||||
}
|
||||
|
||||
@keydown("shift+/")
|
||||
handleOpenKeyboardShortcuts() {
|
||||
this.keyboardShortcutsOpen = true;
|
||||
}
|
||||
|
||||
handleCloseKeyboardShortcuts = () => {
|
||||
this.keyboardShortcutsOpen = false;
|
||||
};
|
||||
|
||||
@keydown(["t", "/", `${meta}+k`])
|
||||
@keydown([
|
||||
"t",
|
||||
"/",
|
||||
env.ENVIRONMENT === "development" ? undefined : `${meta}+k`,
|
||||
])
|
||||
goToSearch(ev: SyntheticEvent<>) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.redirectTo = searchUrl();
|
||||
}
|
||||
|
||||
@keydown("d")
|
||||
goToDashboard() {
|
||||
this.redirectTo = homeUrl();
|
||||
this.props.history.push(searchUrl());
|
||||
}
|
||||
|
||||
@keydown("n")
|
||||
@ -103,17 +89,16 @@ class Layout extends React.Component<Props> {
|
||||
const can = this.props.policies.abilities(activeCollectionId);
|
||||
if (!can.update) return;
|
||||
|
||||
this.props.history.push(newDocumentUrl(activeCollectionId));
|
||||
this.props.history.push(newDocumentPath(activeCollectionId));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { auth, t, ui } = this.props;
|
||||
const { auth, ui } = this.props;
|
||||
const { user, team } = auth;
|
||||
const showSidebar = auth.authenticated && user && team;
|
||||
const sidebarCollapsed = ui.isEditing || ui.sidebarCollapsed;
|
||||
|
||||
if (auth.isSuspended) return <ErrorSuspended />;
|
||||
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
|
||||
|
||||
return (
|
||||
<Container column auto>
|
||||
@ -139,7 +124,7 @@ class Layout extends React.Component<Props> {
|
||||
<Container auto>
|
||||
{showSidebar && (
|
||||
<Switch>
|
||||
<Route path="/settings" component={SettingsSidebar} />
|
||||
<Route path={settingsPath()} component={SettingsSidebar} />
|
||||
<Route component={Sidebar} />
|
||||
</Switch>
|
||||
)}
|
||||
@ -168,13 +153,7 @@ class Layout extends React.Component<Props> {
|
||||
</Switch>
|
||||
</React.Suspense>
|
||||
</Container>
|
||||
<Guide
|
||||
isOpen={this.keyboardShortcutsOpen}
|
||||
onRequestClose={this.handleCloseKeyboardShortcuts}
|
||||
title={t("Keyboard shortcuts")}
|
||||
>
|
||||
<KeyboardShortcuts />
|
||||
</Guide>
|
||||
<CommandBar />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -52,7 +52,9 @@ const Modal = ({
|
||||
}
|
||||
});
|
||||
|
||||
if (!isOpen) return null;
|
||||
if (!isOpen && !wasOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogBackdrop {...dialog}>
|
||||
|
@ -27,8 +27,6 @@ const Contents = styled.div`
|
||||
overflow-y: scroll;
|
||||
width: ${(props) => props.width}px;
|
||||
box-shadow: ${(props) => props.theme.menuShadow};
|
||||
border: ${(props) =>
|
||||
props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
|
||||
`;
|
||||
|
||||
export default Popover;
|
||||
|
@ -11,14 +11,17 @@ type Props = {|
|
||||
flex?: boolean,
|
||||
|};
|
||||
|
||||
function Scrollable({ shadow, topShadow, bottomShadow, flex, ...rest }: Props) {
|
||||
const ref = React.useRef<?HTMLDivElement>();
|
||||
function Scrollable(
|
||||
{ shadow, topShadow, bottomShadow, flex, ...rest }: Props,
|
||||
ref: any
|
||||
) {
|
||||
const fallbackRef = React.useRef<?HTMLDivElement>();
|
||||
const [topShadowVisible, setTopShadow] = React.useState(false);
|
||||
const [bottomShadowVisible, setBottomShadow] = React.useState(false);
|
||||
const { height } = useWindowSize();
|
||||
|
||||
const updateShadows = React.useCallback(() => {
|
||||
const c = ref.current;
|
||||
const c = (ref || fallbackRef).current;
|
||||
if (!c) return;
|
||||
|
||||
const scrollTop = c.scrollTop;
|
||||
@ -33,7 +36,14 @@ function Scrollable({ shadow, topShadow, bottomShadow, flex, ...rest }: Props) {
|
||||
if (bsv !== bottomShadowVisible) {
|
||||
setBottomShadow(bsv);
|
||||
}
|
||||
}, [shadow, topShadow, bottomShadow, topShadowVisible, bottomShadowVisible]);
|
||||
}, [
|
||||
shadow,
|
||||
topShadow,
|
||||
bottomShadow,
|
||||
ref,
|
||||
topShadowVisible,
|
||||
bottomShadowVisible,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
updateShadows();
|
||||
@ -41,7 +51,7 @@ function Scrollable({ shadow, topShadow, bottomShadow, flex, ...rest }: Props) {
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
ref={ref}
|
||||
ref={ref || fallbackRef}
|
||||
onScroll={updateShadows}
|
||||
$flex={flex}
|
||||
$topShadowVisible={topShadowVisible}
|
||||
@ -75,4 +85,4 @@ const Wrapper = styled.div`
|
||||
transition: all 100ms ease-in-out;
|
||||
`;
|
||||
|
||||
export default observer(Scrollable);
|
||||
export default observer(React.forwardRef(Scrollable));
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
SearchIcon,
|
||||
ShapesIcon,
|
||||
HomeIcon,
|
||||
PlusIcon,
|
||||
SettingsIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
@ -13,62 +12,42 @@ import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import CollectionNew from "scenes/CollectionNew";
|
||||
import Invite from "scenes/Invite";
|
||||
import Bubble from "components/Bubble";
|
||||
import Flex from "components/Flex";
|
||||
import Modal from "components/Modal";
|
||||
import Scrollable from "components/Scrollable";
|
||||
import Sidebar from "./Sidebar";
|
||||
import ArchiveLink from "./components/ArchiveLink";
|
||||
import Collections from "./components/Collections";
|
||||
import Section from "./components/Section";
|
||||
import SidebarAction from "./components/SidebarAction";
|
||||
import SidebarLink from "./components/SidebarLink";
|
||||
import Starred from "./components/Starred";
|
||||
import TeamButton from "./components/TeamButton";
|
||||
import TrashLink from "./components/TrashLink";
|
||||
import { inviteUser } from "actions/definitions/users";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useCurrentUser from "hooks/useCurrentUser";
|
||||
import useStores from "hooks/useStores";
|
||||
import AccountMenu from "menus/AccountMenu";
|
||||
import {
|
||||
homePath,
|
||||
searchUrl,
|
||||
draftsPath,
|
||||
templatesPath,
|
||||
settingsPath,
|
||||
} from "utils/routeHelpers";
|
||||
|
||||
function MainSidebar() {
|
||||
const { t } = useTranslation();
|
||||
const { policies, documents } = useStores();
|
||||
const team = useCurrentTeam();
|
||||
const user = useCurrentUser();
|
||||
const [inviteModalOpen, setInviteModalOpen] = React.useState(false);
|
||||
const [
|
||||
createCollectionModalOpen,
|
||||
setCreateCollectionModalOpen,
|
||||
] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
documents.fetchDrafts();
|
||||
documents.fetchTemplates();
|
||||
}, [documents]);
|
||||
|
||||
const handleCreateCollectionModalOpen = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
setCreateCollectionModalOpen(true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCreateCollectionModalClose = React.useCallback(() => {
|
||||
setCreateCollectionModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleInviteModalOpen = React.useCallback((ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
setInviteModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleInviteModalClose = React.useCallback(() => {
|
||||
setInviteModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const [dndArea, setDndArea] = React.useState();
|
||||
const handleSidebarRef = React.useCallback((node) => setDndArea(node), []);
|
||||
const html5Options = React.useMemo(() => ({ rootElement: dndArea }), [
|
||||
@ -95,14 +74,14 @@ function MainSidebar() {
|
||||
<Scrollable flex topShadow>
|
||||
<Section>
|
||||
<SidebarLink
|
||||
to="/home"
|
||||
to={homePath()}
|
||||
icon={<HomeIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label={t("Home")}
|
||||
/>
|
||||
<SidebarLink
|
||||
to={{
|
||||
pathname: "/search",
|
||||
pathname: searchUrl(),
|
||||
state: { fromMenu: true },
|
||||
}}
|
||||
icon={<SearchIcon color="currentColor" />}
|
||||
@ -111,7 +90,7 @@ function MainSidebar() {
|
||||
/>
|
||||
{can.createDocument && (
|
||||
<SidebarLink
|
||||
to="/drafts"
|
||||
to={draftsPath()}
|
||||
icon={<EditIcon color="currentColor" />}
|
||||
label={
|
||||
<Drafts align="center">
|
||||
@ -131,15 +110,13 @@ function MainSidebar() {
|
||||
</Section>
|
||||
<Starred />
|
||||
<Section auto>
|
||||
<Collections
|
||||
onCreateCollection={handleCreateCollectionModalOpen}
|
||||
/>
|
||||
<Collections />
|
||||
</Section>
|
||||
<Section>
|
||||
{can.createDocument && (
|
||||
<>
|
||||
<SidebarLink
|
||||
to="/templates"
|
||||
to={templatesPath()}
|
||||
icon={<ShapesIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label={t("Templates")}
|
||||
@ -156,37 +133,14 @@ function MainSidebar() {
|
||||
</>
|
||||
)}
|
||||
<SidebarLink
|
||||
to="/settings"
|
||||
to={settingsPath()}
|
||||
icon={<SettingsIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label={t("Settings")}
|
||||
/>
|
||||
{can.inviteUser && (
|
||||
<SidebarLink
|
||||
to="/settings/members"
|
||||
onClick={handleInviteModalOpen}
|
||||
icon={<PlusIcon color="currentColor" />}
|
||||
label={`${t("Invite people")}…`}
|
||||
/>
|
||||
)}
|
||||
<SidebarAction action={inviteUser} />
|
||||
</Section>
|
||||
</Scrollable>
|
||||
{can.inviteUser && (
|
||||
<Modal
|
||||
title={t("Invite people")}
|
||||
onRequestClose={handleInviteModalClose}
|
||||
isOpen={inviteModalOpen}
|
||||
>
|
||||
<Invite onSubmit={handleInviteModalClose} />
|
||||
</Modal>
|
||||
)}
|
||||
<Modal
|
||||
title={t("Create a collection")}
|
||||
onRequestClose={handleCreateCollectionModalClose}
|
||||
isOpen={createCollectionModalOpen}
|
||||
>
|
||||
<CollectionNew onSubmit={handleCreateCollectionModalClose} />
|
||||
</Modal>
|
||||
</DndProvider>
|
||||
)}
|
||||
</Sidebar>
|
||||
|
@ -4,9 +4,10 @@ import { ArchiveIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useStores from "../../../hooks/useStores";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import { archivePath } from "utils/routeHelpers";
|
||||
|
||||
function ArchiveLink({ documents }) {
|
||||
const { policies } = useStores();
|
||||
@ -29,7 +30,7 @@ function ArchiveLink({ documents }) {
|
||||
return (
|
||||
<div ref={dropToArchiveDocument}>
|
||||
<SidebarLink
|
||||
to="/archive"
|
||||
to={archivePath()}
|
||||
icon={<ArchiveIcon color="currentColor" open={isDocumentDropping} />}
|
||||
exact={false}
|
||||
label={t("Archive")}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon, CollapsedIcon } from "outline-icons";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -12,15 +12,12 @@ import useStores from "../../../hooks/useStores";
|
||||
import CollectionLink from "./CollectionLink";
|
||||
import DropCursor from "./DropCursor";
|
||||
import PlaceholderCollections from "./PlaceholderCollections";
|
||||
import SidebarAction from "./SidebarAction";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import { createCollection } from "actions/definitions/collections";
|
||||
import useToasts from "hooks/useToasts";
|
||||
|
||||
type Props = {
|
||||
onCreateCollection: () => void,
|
||||
};
|
||||
|
||||
function Collections({ onCreateCollection }: Props) {
|
||||
function Collections() {
|
||||
const [isFetching, setFetching] = React.useState(false);
|
||||
const [fetchError, setFetchError] = React.useState();
|
||||
const { ui, policies, documents, collections } = useStores();
|
||||
@ -28,9 +25,7 @@ function Collections({ onCreateCollection }: Props) {
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
const isPreloaded: boolean = !!collections.orderedData.length;
|
||||
const { t } = useTranslation();
|
||||
const team = useCurrentTeam();
|
||||
const orderedCollections = collections.orderedData;
|
||||
const can = policies.abilities(team.id);
|
||||
const [isDraggingAnyCollection, setIsDraggingAnyCollection] = React.useState(
|
||||
false
|
||||
);
|
||||
@ -93,16 +88,7 @@ function Collections({ onCreateCollection }: Props) {
|
||||
belowCollection={orderedCollections[index + 1]}
|
||||
/>
|
||||
))}
|
||||
{can.createCollection && (
|
||||
<SidebarLink
|
||||
to="/collections"
|
||||
onClick={onCreateCollection}
|
||||
icon={<PlusIcon color="currentColor" />}
|
||||
label={`${t("New collection")}…`}
|
||||
exact
|
||||
depth={0.5}
|
||||
/>
|
||||
)}
|
||||
<SidebarAction action={createCollection} depth={0.5} />
|
||||
</>
|
||||
);
|
||||
|
||||
|
44
app/components/Sidebar/components/SidebarAction.js
Normal file
44
app/components/Sidebar/components/SidebarAction.js
Normal file
@ -0,0 +1,44 @@
|
||||
// @flow
|
||||
import invariant from "invariant";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import { actionToMenuItem } from "actions";
|
||||
import useStores from "hooks/useStores";
|
||||
import type { Action } from "types";
|
||||
|
||||
type Props = {|
|
||||
action: Action,
|
||||
|};
|
||||
|
||||
function SidebarAction({ action, ...rest }: Props) {
|
||||
const stores = useStores();
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const context = {
|
||||
isContextMenu: false,
|
||||
isCommandBar: false,
|
||||
activeCollectionId: undefined,
|
||||
activeDocumentId: undefined,
|
||||
location,
|
||||
stores,
|
||||
t,
|
||||
};
|
||||
|
||||
const menuItem = actionToMenuItem(action, context);
|
||||
invariant(menuItem.onClick, "passed action must have perform");
|
||||
|
||||
return (
|
||||
<SidebarLink
|
||||
onClick={menuItem.onClick}
|
||||
icon={menuItem.icon}
|
||||
label={menuItem.title}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(SidebarAction);
|
@ -7,8 +7,9 @@ import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DocumentDelete from "scenes/DocumentDelete";
|
||||
import Modal from "components/Modal";
|
||||
import useStores from "../../../hooks/useStores";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import useStores from "hooks/useStores";
|
||||
import { trashPath } from "utils/routeHelpers";
|
||||
|
||||
function TrashLink({ documents }) {
|
||||
const { policies } = useStores();
|
||||
@ -33,7 +34,7 @@ function TrashLink({ documents }) {
|
||||
<>
|
||||
<div ref={dropToTrashDocument}>
|
||||
<SidebarLink
|
||||
to="/trash"
|
||||
to={trashPath()}
|
||||
icon={<TrashIcon color="currentColor" open={isDocumentDropping} />}
|
||||
exact={false}
|
||||
label={t("Trash")}
|
||||
|
30
app/hooks/useCommandBarActions.js
Normal file
30
app/hooks/useCommandBarActions.js
Normal file
@ -0,0 +1,30 @@
|
||||
// @flow
|
||||
import { useRegisterActions } from "kbar";
|
||||
import { flattenDeep } from "lodash";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { actionToKBar } from "actions";
|
||||
import useStores from "hooks/useStores";
|
||||
import type { Action } from "types";
|
||||
|
||||
export default function useCommandBarActions(actions: Action[]) {
|
||||
const stores = useStores();
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const context = {
|
||||
t,
|
||||
isCommandBar: true,
|
||||
isContextMenu: false,
|
||||
activeCollectionId: stores.ui.activeCollectionId,
|
||||
activeDocumentId: stores.ui.activeDocumentId,
|
||||
location,
|
||||
stores,
|
||||
};
|
||||
|
||||
const registerable = flattenDeep(
|
||||
actions.map((action) => actionToKBar(action, context))
|
||||
);
|
||||
|
||||
useRegisterActions(registerable, [registerable.length, location.pathname]);
|
||||
}
|
31
app/index.js
31
app/index.js
@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import "focus-visible";
|
||||
import { LazyMotion } from "framer-motion";
|
||||
import { createBrowserHistory } from "history";
|
||||
import { KBarProvider } from "kbar";
|
||||
import { Provider } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { render } from "react-dom";
|
||||
@ -9,19 +9,21 @@ import { Router } from "react-router-dom";
|
||||
import { initI18n } from "shared/i18n";
|
||||
import stores from "stores";
|
||||
import Analytics from "components/Analytics";
|
||||
import { CommandBarOptions } from "components/CommandBar";
|
||||
import Dialogs from "components/Dialogs";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import PageTheme from "components/PageTheme";
|
||||
import ScrollToTop from "components/ScrollToTop";
|
||||
import Theme from "components/Theme";
|
||||
import Toasts from "components/Toasts";
|
||||
import Routes from "./routes";
|
||||
import history from "./utils/history";
|
||||
import { initSentry } from "./utils/sentry";
|
||||
import env from "env";
|
||||
|
||||
initI18n();
|
||||
|
||||
const element = window.document.getElementById("root");
|
||||
const history = createBrowserHistory();
|
||||
|
||||
if (env.SENTRY_DSN) {
|
||||
initSentry(history);
|
||||
@ -61,17 +63,20 @@ if (element) {
|
||||
<Analytics>
|
||||
<Theme>
|
||||
<ErrorBoundary>
|
||||
<LazyMotion features={loadFeatures}>
|
||||
<Router history={history}>
|
||||
<>
|
||||
<PageTheme />
|
||||
<ScrollToTop>
|
||||
<Routes />
|
||||
</ScrollToTop>
|
||||
<Toasts />
|
||||
</>
|
||||
</Router>
|
||||
</LazyMotion>
|
||||
<KBarProvider actions={[]} options={CommandBarOptions}>
|
||||
<LazyMotion features={loadFeatures}>
|
||||
<Router history={history}>
|
||||
<>
|
||||
<PageTheme />
|
||||
<ScrollToTop>
|
||||
<Routes />
|
||||
</ScrollToTop>
|
||||
<Toasts />
|
||||
<Dialogs />
|
||||
</>
|
||||
</Router>
|
||||
</LazyMotion>
|
||||
</KBarProvider>
|
||||
</ErrorBoundary>
|
||||
</Theme>
|
||||
</Analytics>
|
||||
|
@ -1,29 +1,27 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { MoonIcon, SunIcon, TrashIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
changelog,
|
||||
developers,
|
||||
githubIssuesUrl,
|
||||
mailToUrl,
|
||||
settings,
|
||||
} from "shared/utils/routeHelpers";
|
||||
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Guide from "components/Guide";
|
||||
import env from "env";
|
||||
import useBoolean from "hooks/useBoolean";
|
||||
import { development } from "actions/definitions/debug";
|
||||
import {
|
||||
navigateToSettings,
|
||||
openKeyboardShortcuts,
|
||||
openChangelog,
|
||||
openAPIDocumentation,
|
||||
openBugReportUrl,
|
||||
openFeedbackUrl,
|
||||
logout,
|
||||
} from "actions/definitions/navigation";
|
||||
import { changeTheme } from "actions/definitions/settings";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import usePrevious from "hooks/usePrevious";
|
||||
import useSessions from "hooks/useSessions";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import { deleteAllDatabases } from "utils/developer";
|
||||
import separator from "menus/separator";
|
||||
|
||||
type Props = {|
|
||||
children: (props: any) => React.Node,
|
||||
@ -36,18 +34,11 @@ function AccountMenu(props: Props) {
|
||||
placement: "bottom-start",
|
||||
modal: true,
|
||||
});
|
||||
const { showToast } = useToasts();
|
||||
const { auth, ui } = useStores();
|
||||
const { theme, resolvedTheme } = ui;
|
||||
const { ui } = useStores();
|
||||
const { theme } = ui;
|
||||
const team = useCurrentTeam();
|
||||
const previousTheme = usePrevious(theme);
|
||||
const { t } = useTranslation();
|
||||
const [includeAlt, setIncludeAlt] = React.useState(false);
|
||||
const [
|
||||
keyboardShortcutsOpen,
|
||||
handleKeyboardShortcutsOpen,
|
||||
handleKeyboardShortcutsClose,
|
||||
] = useBoolean();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (theme !== previousTheme) {
|
||||
@ -55,132 +46,43 @@ function AccountMenu(props: Props) {
|
||||
}
|
||||
}, [menu, theme, previousTheme]);
|
||||
|
||||
const handleDeleteAllDatabases = React.useCallback(async () => {
|
||||
await deleteAllDatabases();
|
||||
showToast("IndexedDB cache deleted");
|
||||
menu.hide();
|
||||
}, [showToast, menu]);
|
||||
|
||||
const handleOpenMenu = React.useCallback((event) => {
|
||||
setIncludeAlt(event.altKey);
|
||||
}, []);
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
const actions = React.useMemo(() => {
|
||||
const otherSessions = sessions.filter(
|
||||
(session) => session.teamId !== team.id && session.url !== team.url
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
title: t("Settings"),
|
||||
to: settings(),
|
||||
},
|
||||
{
|
||||
title: t("Keyboard shortcuts"),
|
||||
onClick: handleKeyboardShortcutsOpen,
|
||||
},
|
||||
{
|
||||
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(),
|
||||
},
|
||||
...(includeAlt || env.ENVIRONMENT === "development"
|
||||
? [
|
||||
{
|
||||
title: t("Development"),
|
||||
items: [
|
||||
{
|
||||
title: "Delete IndexedDB cache",
|
||||
icon: <TrashIcon />,
|
||||
onClick: handleDeleteAllDatabases,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("Appearance"),
|
||||
icon: resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />,
|
||||
items: [
|
||||
{
|
||||
title: t("System"),
|
||||
onClick: () => ui.setTheme("system"),
|
||||
selected: theme === "system",
|
||||
},
|
||||
{
|
||||
title: t("Light"),
|
||||
onClick: () => ui.setTheme("light"),
|
||||
selected: theme === "light",
|
||||
},
|
||||
{
|
||||
title: t("Dark"),
|
||||
onClick: () => ui.setTheme("dark"),
|
||||
selected: theme === "dark",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
navigateToSettings,
|
||||
openKeyboardShortcuts,
|
||||
openAPIDocumentation,
|
||||
separator(),
|
||||
openChangelog,
|
||||
openFeedbackUrl,
|
||||
openBugReportUrl,
|
||||
development,
|
||||
changeTheme,
|
||||
separator(),
|
||||
...(otherSessions.length
|
||||
? [
|
||||
{
|
||||
title: t("Switch team"),
|
||||
items: otherSessions.map((session) => ({
|
||||
title: session.name,
|
||||
name: t("Switch team"),
|
||||
children: otherSessions.map((session) => ({
|
||||
name: session.name,
|
||||
icon: <Logo alt={session.name} src={session.logoUrl} />,
|
||||
href: session.url,
|
||||
perform: () => (window.location.href = session.url),
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("Log out"),
|
||||
onClick: auth.logout,
|
||||
},
|
||||
logout,
|
||||
];
|
||||
}, [
|
||||
auth.logout,
|
||||
team.id,
|
||||
team.url,
|
||||
sessions,
|
||||
handleKeyboardShortcutsOpen,
|
||||
handleDeleteAllDatabases,
|
||||
resolvedTheme,
|
||||
includeAlt,
|
||||
theme,
|
||||
t,
|
||||
ui,
|
||||
]);
|
||||
}, [team.id, team.url, sessions, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Guide
|
||||
isOpen={keyboardShortcutsOpen}
|
||||
onRequestClose={handleKeyboardShortcutsClose}
|
||||
title={t("Keyboard shortcuts")}
|
||||
>
|
||||
<KeyboardShortcuts />
|
||||
</Guide>
|
||||
<MenuButton {...menu} onClick={handleOpenMenu}>
|
||||
{props.children}
|
||||
</MenuButton>
|
||||
<MenuButton {...menu}>{props.children}</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("Account")}>
|
||||
<Template {...menu} items={items} />
|
||||
<Template {...menu} actions={actions} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
|
@ -26,7 +26,7 @@ import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import getDataTransferFiles from "utils/getDataTransferFiles";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
import { newDocumentPath } from "utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
collection: Collection,
|
||||
@ -72,7 +72,7 @@ function CollectionMenu({
|
||||
const handleNewDocument = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
history.push(newDocumentUrl(collection.id));
|
||||
history.push(newDocumentPath(collection.id));
|
||||
},
|
||||
[history, collection.id]
|
||||
);
|
||||
@ -225,7 +225,7 @@ function CollectionMenu({
|
||||
>
|
||||
<CollectionEdit
|
||||
onSubmit={() => setShowCollectionEdit(false)}
|
||||
collection={collection}
|
||||
collectionId={collection.id}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
|
@ -45,7 +45,7 @@ import {
|
||||
documentHistoryUrl,
|
||||
documentUrl,
|
||||
editDocumentUrl,
|
||||
newDocumentUrl,
|
||||
newDocumentPath,
|
||||
} from "utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
@ -330,7 +330,7 @@ function DocumentMenu({
|
||||
},
|
||||
{
|
||||
title: t("New nested document"),
|
||||
to: newDocumentUrl(document.collectionId, {
|
||||
to: newDocumentPath(document.collectionId, {
|
||||
parentDocumentId: document.id,
|
||||
}),
|
||||
visible: !!can.createChildDocument,
|
||||
|
@ -7,7 +7,7 @@ import Document from "models/Document";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import useStores from "hooks/useStores";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
import { newDocumentPath } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
label?: (any) => React.Node,
|
||||
@ -38,11 +38,11 @@ function NewChildDocumentMenu({ document, label }: Props) {
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
to: newDocumentUrl(document.collectionId),
|
||||
to: newDocumentPath(document.collectionId),
|
||||
},
|
||||
{
|
||||
title: t("New nested document"),
|
||||
to: newDocumentUrl(document.collectionId, {
|
||||
to: newDocumentPath(document.collectionId, {
|
||||
parentDocumentId: document.id,
|
||||
}),
|
||||
},
|
||||
|
@ -13,7 +13,7 @@ import Header from "components/ContextMenu/Header";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
import { newDocumentPath } from "utils/routeHelpers";
|
||||
|
||||
function NewDocumentMenu() {
|
||||
const menu = useMenuState({ modal: true });
|
||||
@ -29,7 +29,7 @@ function NewDocumentMenu() {
|
||||
|
||||
if (can.update) {
|
||||
filtered.push({
|
||||
to: newDocumentUrl(collection.id),
|
||||
to: newDocumentPath(collection.id),
|
||||
title: <CollectionName>{collection.name}</CollectionName>,
|
||||
icon: <CollectionIcon collection={collection} />,
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ import Header from "components/ContextMenu/Header";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
import { newDocumentPath } from "utils/routeHelpers";
|
||||
|
||||
function NewTemplateMenu() {
|
||||
const menu = useMenuState({ modal: true });
|
||||
@ -27,7 +27,7 @@ function NewTemplateMenu() {
|
||||
const can = policies.abilities(collection.id);
|
||||
if (can.update) {
|
||||
filtered.push({
|
||||
to: newDocumentUrl(collection.id, { template: true }),
|
||||
to: newDocumentPath(collection.id, { template: true }),
|
||||
title: <CollectionName>{collection.name}</CollectionName>,
|
||||
icon: <CollectionIcon collection={collection} />,
|
||||
});
|
||||
|
7
app/menus/separator.js
Normal file
7
app/menus/separator.js
Normal file
@ -0,0 +1,7 @@
|
||||
// @flow
|
||||
|
||||
export default function separator() {
|
||||
return {
|
||||
type: "separator",
|
||||
};
|
||||
}
|
@ -39,13 +39,15 @@ import Tabs from "components/Tabs";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import Collection from "../models/Collection";
|
||||
import { updateCollectionUrl } from "../utils/routeHelpers";
|
||||
import { editCollection } from "actions/definitions/collections";
|
||||
import useBoolean from "hooks/useBoolean";
|
||||
import useCommandBarActions from "hooks/useCommandBarActions";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useImportDocument from "hooks/useImportDocument";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import CollectionMenu from "menus/CollectionMenu";
|
||||
import { newDocumentUrl, collectionUrl } from "utils/routeHelpers";
|
||||
import { newDocumentPath, collectionUrl } from "utils/routeHelpers";
|
||||
|
||||
function CollectionScene() {
|
||||
const params = useParams();
|
||||
@ -109,6 +111,8 @@ function CollectionScene() {
|
||||
load();
|
||||
}, [collections, isFetching, collection, error, id, can]);
|
||||
|
||||
useCommandBarActions([editCollection]);
|
||||
|
||||
const handleRejection = React.useCallback(() => {
|
||||
showToast(
|
||||
t("Document not supported – try Markdown, Plain text, HTML, or Word"),
|
||||
@ -158,7 +162,7 @@ function CollectionScene() {
|
||||
>
|
||||
<Button
|
||||
as={Link}
|
||||
to={collection ? newDocumentUrl(collection.id) : ""}
|
||||
to={collection ? newDocumentPath(collection.id) : ""}
|
||||
disabled={!collection}
|
||||
icon={<PlusIcon />}
|
||||
>
|
||||
@ -227,7 +231,7 @@ function CollectionScene() {
|
||||
</HelpText>
|
||||
<Empty>
|
||||
{canUser.createDocument && (
|
||||
<Link to={newDocumentUrl(collection.id)}>
|
||||
<Link to={newDocumentPath(collection.id)}>
|
||||
<Button icon={<NewDocumentIcon color="currentColor" />}>
|
||||
{t("Create a document")}
|
||||
</Button>
|
||||
@ -388,6 +392,7 @@ const DropMessage = styled(HelpText)`
|
||||
`;
|
||||
|
||||
const DropzoneContainer = styled.div`
|
||||
outline-color: transparent !important;
|
||||
min-height: calc(100% - 56px);
|
||||
position: relative;
|
||||
|
||||
|
@ -8,7 +8,7 @@ import Button from "components/Button";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import { homeUrl } from "utils/routeHelpers";
|
||||
import { homePath } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
@ -28,7 +28,7 @@ function CollectionDelete({ collection, onSubmit }: Props) {
|
||||
|
||||
try {
|
||||
await collection.delete();
|
||||
history.push(homeUrl());
|
||||
history.push(homePath());
|
||||
onSubmit();
|
||||
} catch (err) {
|
||||
showToast(err.message, { type: "error" });
|
||||
|
@ -1,23 +1,28 @@
|
||||
// @flow
|
||||
import invariant from "invariant";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import Collection from "models/Collection";
|
||||
import Button from "components/Button";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import IconPicker from "components/IconPicker";
|
||||
import Input from "components/Input";
|
||||
import InputSelect from "components/InputSelect";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
collectionId: string,
|
||||
onSubmit: () => void,
|
||||
};
|
||||
|
||||
const CollectionEdit = ({ collection, onSubmit }: Props) => {
|
||||
const CollectionEdit = ({ collectionId, onSubmit }: Props) => {
|
||||
const { collections } = useStores();
|
||||
const collection = collections.get(collectionId);
|
||||
invariant(collection, "Collection not found");
|
||||
|
||||
const [name, setName] = useState(collection.name);
|
||||
const [icon, setIcon] = useState(collection.icon);
|
||||
const [color, setColor] = useState(collection.color || "#4E5C6E");
|
||||
|
@ -31,7 +31,7 @@ import TableOfContentsMenu from "menus/TableOfContentsMenu";
|
||||
import TemplatesMenu from "menus/TemplatesMenu";
|
||||
import { type NavigationNode } from "types";
|
||||
import { metaDisplay } from "utils/keyboard";
|
||||
import { newDocumentUrl, editDocumentUrl } from "utils/routeHelpers";
|
||||
import { newDocumentPath, editDocumentUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
@ -255,7 +255,7 @@ function DocumentHeader({
|
||||
<Button
|
||||
icon={<PlusIcon />}
|
||||
as={Link}
|
||||
to={newDocumentUrl(document.collectionId, {
|
||||
to={newDocumentPath(document.collectionId, {
|
||||
templateId: document.id,
|
||||
})}
|
||||
primary
|
||||
|
@ -14,7 +14,7 @@ import usePageVisibility from "hooks/usePageVisibility";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import MultiplayerExtension from "multiplayer/MultiplayerExtension";
|
||||
import { homeUrl } from "utils/routeHelpers";
|
||||
import { homePath } from "utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
...EditorProps,
|
||||
@ -61,7 +61,7 @@ function MultiplayerEditor({ ...props }: Props, ref: any) {
|
||||
)
|
||||
);
|
||||
|
||||
history.replace(homeUrl());
|
||||
history.replace(homePath());
|
||||
});
|
||||
|
||||
provider.on("awarenessChange", ({ states }) => {
|
||||
|
@ -3,12 +3,12 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { groupSettings } from "shared/utils/routeHelpers";
|
||||
import Group from "models/Group";
|
||||
import Button from "components/Button";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import { groupSettingsPath } from "utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
group: Group,
|
||||
@ -27,7 +27,7 @@ function GroupDelete({ group, onSubmit }: Props) {
|
||||
|
||||
try {
|
||||
await group.delete();
|
||||
history.push(groupSettings());
|
||||
history.push(groupSettingsPath());
|
||||
onSubmit();
|
||||
} catch (err) {
|
||||
showToast(err.message, { type: "error" });
|
||||
|
@ -36,8 +36,7 @@ import StatusFilter from "./components/StatusFilter";
|
||||
import UserFilter from "./components/UserFilter";
|
||||
import NewDocumentMenu from "menus/NewDocumentMenu";
|
||||
import { type LocationWithState } from "types";
|
||||
import { metaDisplay } from "utils/keyboard";
|
||||
import { newDocumentUrl, searchUrl } from "utils/routeHelpers";
|
||||
import { newDocumentPath, searchUrl } from "utils/routeHelpers";
|
||||
import { decodeURIComponentSafe } from "utils/urls";
|
||||
|
||||
type Props = {
|
||||
@ -153,7 +152,7 @@ class Search extends React.Component<Props> {
|
||||
|
||||
handleNewDoc = () => {
|
||||
if (this.collectionId) {
|
||||
this.props.history.push(newDocumentUrl(this.collectionId));
|
||||
this.props.history.push(newDocumentPath(this.collectionId));
|
||||
}
|
||||
};
|
||||
|
||||
@ -289,8 +288,8 @@ class Search extends React.Component<Props> {
|
||||
<Fade>
|
||||
<HelpText small>
|
||||
<Trans
|
||||
defaults="Use the <em>{{ meta }}+K</em> shortcut to search from anywhere in your knowledge base"
|
||||
values={{ meta: metaDisplay }}
|
||||
defaults="Use the <em>{{ shortcut }}</em> shortcut to search from anywhere in your knowledge base"
|
||||
values={{ shortcut: "/" }}
|
||||
components={{ em: <strong /> }}
|
||||
/>
|
||||
</HelpText>
|
||||
|
@ -49,7 +49,7 @@ function Features() {
|
||||
<HelpText>
|
||||
<Trans>
|
||||
Manage optional and beta features. Changing these settings will affect
|
||||
all team members.
|
||||
the experience for all team members.
|
||||
</Trans>
|
||||
</HelpText>
|
||||
<Checkbox
|
||||
@ -57,7 +57,13 @@ function Features() {
|
||||
name="collaborativeEditing"
|
||||
checked={data.collaborativeEditing}
|
||||
onChange={handleChange}
|
||||
note="When enabled multiple people can edit documents at the same time (Beta)"
|
||||
note={
|
||||
<Trans>
|
||||
When enabled multiple people can edit documents at the same time.
|
||||
Please note that this feature is in beta and currently disables
|
||||
updating the document via the API.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</Scene>
|
||||
);
|
||||
|
@ -6,7 +6,6 @@ import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { settings } from "shared/utils/routeHelpers";
|
||||
import User from "models/User";
|
||||
import Avatar from "components/Avatar";
|
||||
import Badge from "components/Badge";
|
||||
@ -18,6 +17,7 @@ import PaginatedDocumentList from "components/PaginatedDocumentList";
|
||||
import Subheading from "components/Subheading";
|
||||
import useCurrentUser from "hooks/useCurrentUser";
|
||||
import useStores from "hooks/useStores";
|
||||
import { settingsPath } from "utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
user: User,
|
||||
@ -61,7 +61,7 @@ function UserProfile(props: Props) {
|
||||
{isCurrentUser && (
|
||||
<Edit>
|
||||
<Button
|
||||
onClick={() => history.push(settings())}
|
||||
onClick={() => history.push(settingsPath())}
|
||||
icon={<EditIcon />}
|
||||
neutral
|
||||
>
|
||||
|
73
app/stores/DialogsStore.js
Normal file
73
app/stores/DialogsStore.js
Normal file
@ -0,0 +1,73 @@
|
||||
// @flow
|
||||
import { observable, action } from "mobx";
|
||||
import * as React from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export default class DialogsStore {
|
||||
@observable guide: {
|
||||
title: string,
|
||||
content: React.Node,
|
||||
isOpen: boolean,
|
||||
};
|
||||
@observable modalStack = new Map<
|
||||
string,
|
||||
{
|
||||
title: string,
|
||||
content: React.Node,
|
||||
isOpen: boolean,
|
||||
}
|
||||
>();
|
||||
|
||||
openGuide = ({ title, content }: { title: string, content: React.Node }) => {
|
||||
setTimeout(
|
||||
action(() => {
|
||||
this.guide = { title, content, isOpen: true };
|
||||
}),
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
@action
|
||||
closeGuide = () => {
|
||||
if (this.guide) {
|
||||
this.guide.isOpen = false;
|
||||
}
|
||||
};
|
||||
|
||||
openModal = ({
|
||||
title,
|
||||
content,
|
||||
replace,
|
||||
}: {
|
||||
title: string,
|
||||
content: React.Node,
|
||||
replace?: boolean,
|
||||
}) => {
|
||||
setTimeout(
|
||||
action(() => {
|
||||
const id = uuidv4();
|
||||
|
||||
if (replace) {
|
||||
this.modalStack.clear();
|
||||
}
|
||||
|
||||
this.modalStack.set(id, {
|
||||
title,
|
||||
content,
|
||||
isOpen: true,
|
||||
});
|
||||
}),
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
@action
|
||||
closeModal = (id: string) => {
|
||||
this.modalStack.delete(id);
|
||||
};
|
||||
|
||||
@action
|
||||
closeAllModals = () => {
|
||||
this.modalStack.clear();
|
||||
};
|
||||
}
|
@ -3,6 +3,7 @@ import ApiKeysStore from "./ApiKeysStore";
|
||||
import AuthStore from "./AuthStore";
|
||||
import CollectionGroupMembershipsStore from "./CollectionGroupMembershipsStore";
|
||||
import CollectionsStore from "./CollectionsStore";
|
||||
import DialogsStore from "./DialogsStore";
|
||||
import DocumentPresenceStore from "./DocumentPresenceStore";
|
||||
import DocumentsStore from "./DocumentsStore";
|
||||
import EventsStore from "./EventsStore";
|
||||
@ -25,6 +26,7 @@ export default class RootStore {
|
||||
auth: AuthStore;
|
||||
collections: CollectionsStore;
|
||||
collectionGroupMemberships: CollectionGroupMembershipsStore;
|
||||
dialogs: DialogsStore;
|
||||
documents: DocumentsStore;
|
||||
events: EventsStore;
|
||||
groups: GroupsStore;
|
||||
@ -49,6 +51,7 @@ export default class RootStore {
|
||||
this.auth = new AuthStore(this);
|
||||
this.collections = new CollectionsStore(this);
|
||||
this.collectionGroupMemberships = new CollectionGroupMembershipsStore(this);
|
||||
this.dialogs = new DialogsStore();
|
||||
this.documents = new DocumentsStore(this);
|
||||
this.events = new EventsStore(this);
|
||||
this.groups = new GroupsStore(this);
|
||||
|
@ -1,10 +1,81 @@
|
||||
// @flow
|
||||
import { type TFunction } from "react-i18next";
|
||||
import { type Location } from "react-router-dom";
|
||||
import theme from "shared/theme";
|
||||
import RootStore from "stores/RootStore";
|
||||
import Document from "models/Document";
|
||||
|
||||
export type Theme = typeof theme;
|
||||
|
||||
export type MenuItemClickable = {|
|
||||
title: React.Node,
|
||||
onClick: (event: SyntheticEvent<>) => void | Promise<void>,
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
icon?: React.Node,
|
||||
|};
|
||||
|
||||
export type MenuItemWithChildren = {|
|
||||
title: React.Node,
|
||||
visible?: boolean,
|
||||
disabled?: boolean,
|
||||
style?: Object,
|
||||
hover?: boolean,
|
||||
items: MenuItem[],
|
||||
icon?: React.Node,
|
||||
|};
|
||||
|
||||
export type MenuSeparator = {|
|
||||
type: "separator",
|
||||
visible?: boolean,
|
||||
|};
|
||||
|
||||
export type MenuHeading = {|
|
||||
type: "heading",
|
||||
visible?: boolean,
|
||||
title: React.Node,
|
||||
|};
|
||||
|
||||
export type ActionContext = {
|
||||
isContextMenu: boolean,
|
||||
isCommandBar: boolean,
|
||||
activeCollectionId: ?string,
|
||||
activeDocumentId: ?string,
|
||||
location: Location,
|
||||
stores: RootStore,
|
||||
event?: Event,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
export type Action = {|
|
||||
id: string,
|
||||
name: ((ActionContext) => string) | string,
|
||||
section: ((ActionContext) => string) | string,
|
||||
shortcut?: string[],
|
||||
keywords?: string,
|
||||
iconInContextMenu?: boolean,
|
||||
icon?: React.Element,
|
||||
placeholder?: ((ActionContext) => string) | string,
|
||||
selected?: (ActionContext) => boolean,
|
||||
visible?: (ActionContext) => boolean,
|
||||
perform?: (ActionContext) => any,
|
||||
children?: ((ActionContext) => Action[]) | Action[],
|
||||
|};
|
||||
|
||||
export type CommandBarAction = {|
|
||||
id: string,
|
||||
name: string,
|
||||
section: string,
|
||||
shortcut?: string[],
|
||||
keywords?: string,
|
||||
placeholder?: string,
|
||||
icon?: React.Element,
|
||||
perform?: () => any,
|
||||
children?: string[],
|
||||
parent?: string,
|
||||
|};
|
||||
|
||||
export type LocationWithState = Location & {
|
||||
state: {
|
||||
[key: string]: string,
|
||||
@ -68,14 +139,7 @@ export type MenuItem =
|
||||
disabled?: boolean,
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
onClick: (event: SyntheticEvent<>) => void | Promise<void>,
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| MenuItemClickable
|
||||
| {|
|
||||
title: React.Node,
|
||||
href: string,
|
||||
@ -85,24 +149,9 @@ export type MenuItem =
|
||||
level?: number,
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
visible?: boolean,
|
||||
disabled?: boolean,
|
||||
style?: Object,
|
||||
hover?: boolean,
|
||||
items: MenuItem[],
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| {|
|
||||
type: "separator",
|
||||
visible?: boolean,
|
||||
|}
|
||||
| {|
|
||||
type: "heading",
|
||||
visible?: boolean,
|
||||
title: React.Node,
|
||||
|};
|
||||
| MenuItemWithChildren
|
||||
| MenuSeparator
|
||||
| MenuHeading;
|
||||
|
||||
export type ToastOptions = {|
|
||||
type: "warning" | "error" | "info" | "success",
|
||||
|
6
app/utils/history.js
Normal file
6
app/utils/history.js
Normal file
@ -0,0 +1,6 @@
|
||||
// @flow
|
||||
import { createBrowserHistory } from "history";
|
||||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
export default history;
|
@ -3,12 +3,32 @@ import queryString from "query-string";
|
||||
import Collection from "models/Collection";
|
||||
import Document from "models/Document";
|
||||
|
||||
export function homeUrl(): string {
|
||||
export function homePath(): string {
|
||||
return "/home";
|
||||
}
|
||||
|
||||
export function newCollectionUrl(): string {
|
||||
return "/collection/new";
|
||||
export function draftsPath(): string {
|
||||
return "/drafts";
|
||||
}
|
||||
|
||||
export function templatesPath(): string {
|
||||
return "/templates";
|
||||
}
|
||||
|
||||
export function settingsPath(): string {
|
||||
return "/settings";
|
||||
}
|
||||
|
||||
export function archivePath(): string {
|
||||
return "/archive";
|
||||
}
|
||||
|
||||
export function trashPath(): string {
|
||||
return "/trash";
|
||||
}
|
||||
|
||||
export function groupSettingsPath(): string {
|
||||
return "/settings/groups";
|
||||
}
|
||||
|
||||
export function collectionUrl(url: string, section: ?string): string {
|
||||
@ -54,7 +74,7 @@ export function updateDocumentUrl(oldUrl: string, document: Document): string {
|
||||
return oldUrl.replace(new RegExp("/doc/[0-9a-zA-Z-_~]*"), document.url);
|
||||
}
|
||||
|
||||
export function newDocumentUrl(
|
||||
export function newDocumentPath(
|
||||
collectionId: string,
|
||||
params?: {
|
||||
parentDocumentId?: string,
|
||||
|
@ -13,6 +13,7 @@ app
|
||||
├── components - React components reusable across scenes
|
||||
├── embeds - Embed definitions that represent rich interactive embeds in the editor
|
||||
├── hooks - Reusable React hooks
|
||||
├── actions - Reusable actions such as navigating, opening, creating entities
|
||||
├── menus - Context menus, often appear in multiple places in the UI
|
||||
├── models - State models using MobX observables
|
||||
├── routes - Route definitions, note that chunks are async loaded with suspense
|
||||
|
@ -97,6 +97,7 @@
|
||||
"json-loader": "0.5.4",
|
||||
"jsonwebtoken": "^8.5.0",
|
||||
"jszip": "^3.5.0",
|
||||
"kbar": "^0.1.0-beta.15",
|
||||
"koa": "^2.10.0",
|
||||
"koa-body": "^4.2.0",
|
||||
"koa-compress": "2.0.0",
|
||||
@ -144,6 +145,7 @@
|
||||
"react-portal": "^4.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-table": "^7.7.0",
|
||||
"react-virtual": "^2.8.2",
|
||||
"react-virtualized-auto-sizer": "^1.0.5",
|
||||
"react-waypoint": "^10.1.0",
|
||||
"react-window": "^1.8.6",
|
||||
@ -225,4 +227,4 @@
|
||||
"js-yaml": "^3.13.1"
|
||||
},
|
||||
"version": "0.59.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,38 @@
|
||||
{
|
||||
"Open collection": "Open collection",
|
||||
"New collection": "New collection",
|
||||
"Create a collection": "Create a collection",
|
||||
"Edit collection": "Edit collection",
|
||||
"Delete IndexedDB cache": "Delete IndexedDB cache",
|
||||
"IndexedDB cache deleted": "IndexedDB cache deleted",
|
||||
"Development": "Development",
|
||||
"Open document": "Open document",
|
||||
"New document": "New document",
|
||||
"Import document": "Import document",
|
||||
"Home": "Home",
|
||||
"Search": "Search",
|
||||
"Drafts": "Drafts",
|
||||
"Templates": "Templates",
|
||||
"Archive": "Archive",
|
||||
"Trash": "Trash",
|
||||
"Settings": "Settings",
|
||||
"API documentation": "API documentation",
|
||||
"Send us feedback": "Send us feedback",
|
||||
"Report a bug": "Report a bug",
|
||||
"Changelog": "Changelog",
|
||||
"Keyboard shortcuts": "Keyboard shortcuts",
|
||||
"Log out": "Log out",
|
||||
"Dark": "Dark",
|
||||
"Light": "Light",
|
||||
"System": "System",
|
||||
"Change theme": "Change theme",
|
||||
"Change theme to": "Change theme to",
|
||||
"Invite people": "Invite people",
|
||||
"Collection": "Collection",
|
||||
"Debug": "Debug",
|
||||
"Document": "Document",
|
||||
"Navigation": "Navigation",
|
||||
"People": "People",
|
||||
"currently editing": "currently editing",
|
||||
"currently viewing": "currently viewing",
|
||||
"previously edited": "previously edited",
|
||||
@ -8,13 +42,10 @@
|
||||
"Add a description": "Add a description",
|
||||
"Collapse": "Collapse",
|
||||
"Expand": "Expand",
|
||||
"Type a command or search": "Type a command or search",
|
||||
"Server connection lost": "Server connection lost",
|
||||
"Edits you make will sync once you’re online": "Edits you make will sync once you’re online",
|
||||
"Submenu": "Submenu",
|
||||
"Trash": "Trash",
|
||||
"Archive": "Archive",
|
||||
"Drafts": "Drafts",
|
||||
"Templates": "Templates",
|
||||
"Deleted Collection": "Deleted Collection",
|
||||
"History": "History",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
@ -127,7 +158,6 @@
|
||||
"Choose icon": "Choose icon",
|
||||
"Loading": "Loading",
|
||||
"Loading editor": "Loading editor",
|
||||
"Search": "Search",
|
||||
"Default access": "Default access",
|
||||
"View and edit": "View and edit",
|
||||
"View only": "View only",
|
||||
@ -139,12 +169,10 @@
|
||||
"Outline is available in your language {{optionLabel}}, would you like to change?": "Outline is available in your language {{optionLabel}}, would you like to change?",
|
||||
"Change Language": "Change Language",
|
||||
"Dismiss": "Dismiss",
|
||||
"Keyboard shortcuts": "Keyboard shortcuts",
|
||||
"Back": "Back",
|
||||
"Document archived": "Document archived",
|
||||
"Move document": "Move document",
|
||||
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
|
||||
"New collection": "New collection",
|
||||
"Collections": "Collections",
|
||||
"Untitled": "Untitled",
|
||||
"Document not supported – try Markdown, Plain text, HTML, or Word": "Document not supported – try Markdown, Plain text, HTML, or Word",
|
||||
@ -154,10 +182,6 @@
|
||||
"Show less": "Show less",
|
||||
"Toggle sidebar": "Toggle sidebar",
|
||||
"Delete {{ documentName }}": "Delete {{ documentName }}",
|
||||
"Home": "Home",
|
||||
"Settings": "Settings",
|
||||
"Invite people": "Invite people",
|
||||
"Create a collection": "Create a collection",
|
||||
"Return to App": "Back to App",
|
||||
"Account": "Account",
|
||||
"Profile": "Profile",
|
||||
@ -179,29 +203,15 @@
|
||||
"Previous page": "Previous page",
|
||||
"Next page": "Next page",
|
||||
"Could not import file": "Could not import file",
|
||||
"API documentation": "API documentation",
|
||||
"Changelog": "Changelog",
|
||||
"Send us feedback": "Send us feedback",
|
||||
"Report a bug": "Report a bug",
|
||||
"Development": "Development",
|
||||
"Appearance": "Appearance",
|
||||
"System": "System",
|
||||
"Light": "Light",
|
||||
"Dark": "Dark",
|
||||
"Switch team": "Switch team",
|
||||
"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",
|
||||
"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",
|
||||
"Export collection": "Export collection",
|
||||
"Show sort menu": "Show sort menu",
|
||||
@ -433,7 +443,6 @@
|
||||
"Add another": "Add another",
|
||||
"Inviting": "Inviting",
|
||||
"Send Invites": "Send Invites",
|
||||
"Navigation": "Navigation",
|
||||
"Edit current document": "Edit current document",
|
||||
"Move current document": "Move current document",
|
||||
"Jump to search": "Jump to search",
|
||||
@ -489,7 +498,7 @@
|
||||
"Author": "Author",
|
||||
"Not Found": "Not Found",
|
||||
"We were unable to find the page you’re looking for.": "We were unable to find the page you’re looking for.",
|
||||
"Use the <em>{{ meta }}+K</em> shortcut to search from anywhere in your knowledge base": "Use the <em>{{ meta }}+K</em> shortcut to search from anywhere in your knowledge base",
|
||||
"Use the <em>{{ shortcut }}</em> shortcut to search from anywhere in your knowledge base": "Use the <em>{{ shortcut }}</em> shortcut to search from anywhere in your knowledge base",
|
||||
"No documents found for your search filters. <1></1>": "No documents found for your search filters. <1></1>",
|
||||
"Create a new document?": "Create a new document?",
|
||||
"Clear filters": "Clear filters",
|
||||
@ -515,8 +524,9 @@
|
||||
"Upload": "Upload",
|
||||
"Subdomain": "Subdomain",
|
||||
"Your knowledge base will be accessible at": "Your knowledge base will be accessible at",
|
||||
"Manage optional and beta features. Changing these settings will affect all team members.": "Manage optional and beta features. Changing these settings will affect all team members.",
|
||||
"Manage optional and beta features. Changing these settings will affect the experience for all team members.": "Manage optional and beta features. Changing these settings will affect the experience for all team members.",
|
||||
"Collaborative editing": "Collaborative editing",
|
||||
"When enabled multiple people can edit documents at the same time. Please note that this feature is in beta and currently disables updating the document via the API.": "When enabled multiple people can edit documents at the same time. Please note that this feature is in beta and currently disables updating the document via the API.",
|
||||
"New group": "New group",
|
||||
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
|
||||
"All groups": "All groups",
|
||||
|
@ -126,6 +126,7 @@ export const base = {
|
||||
popover: 9000,
|
||||
titleBarDivider: 10000,
|
||||
loadingIndicatorBar: 20000,
|
||||
commandBar: 30000,
|
||||
},
|
||||
};
|
||||
|
||||
@ -147,9 +148,10 @@ export const light = {
|
||||
backdrop: "rgba(0, 0, 0, 0.2)",
|
||||
shadow: "rgba(0, 0, 0, 0.2)",
|
||||
|
||||
menuItemSelected: colors.warmGrey,
|
||||
menuBackground: colors.white,
|
||||
menuShadow:
|
||||
"0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 8px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.08), 0 30px 40px rgb(0 0 0 / 8%)",
|
||||
"0 0 0 1px rgb(0 0 0 / 2%), 0 4px 8px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 0%), 0 30px 40px rgb(0 0 0 / 8%)",
|
||||
divider: colors.slateLight,
|
||||
titleBarDivider: colors.slateLight,
|
||||
inputBorder: colors.slateLight,
|
||||
@ -206,10 +208,10 @@ export const dark = {
|
||||
backdrop: "rgba(255, 255, 255, 0.3)",
|
||||
shadow: "rgba(0, 0, 0, 0.6)",
|
||||
|
||||
menuBorder: lighten(0.1, colors.almostBlack),
|
||||
menuBackground: lighten(0.015, colors.almostBlack),
|
||||
menuItemSelected: lighten(0.1, "#1f2128"),
|
||||
menuBackground: "#1f2128",
|
||||
menuShadow:
|
||||
"0 0 0 1px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.08), inset 0 0 1px rgba(255,255,255,.4)",
|
||||
"0 0 0 1px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.08)",
|
||||
divider: lighten(0.1, colors.almostBlack),
|
||||
titleBarDivider: darken(0.4, colors.slate),
|
||||
inputBorder: colors.slateDark,
|
||||
|
@ -42,11 +42,11 @@ export function mailToUrl(): string {
|
||||
return "mailto:hello@getoutline.com";
|
||||
}
|
||||
|
||||
export function developers(): string {
|
||||
export function developersUrl(): string {
|
||||
return `https://www.getoutline.com/developers`;
|
||||
}
|
||||
|
||||
export function changelog(): string {
|
||||
export function changelogUrl(): string {
|
||||
return `https://www.getoutline.com/changelog`;
|
||||
}
|
||||
|
||||
@ -54,12 +54,4 @@ export function signin(service: string = "slack"): string {
|
||||
return `${process.env.URL}/auth/${service}`;
|
||||
}
|
||||
|
||||
export function settings(): string {
|
||||
return `/settings`;
|
||||
}
|
||||
|
||||
export function groupSettings(): string {
|
||||
return `/settings/groups`;
|
||||
}
|
||||
|
||||
export const SLUG_URL_REGEX = /^[0-9a-zA-Z-_~]*-([a-zA-Z0-9]{10,15})$/;
|
||||
|
64
yarn.lock
64
yarn.lock
@ -2257,6 +2257,27 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@reach/observe-rect@^1.1.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
|
||||
integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==
|
||||
|
||||
"@reach/portal@^0.16.0":
|
||||
version "0.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.16.0.tgz#1544531d978b770770b718b2872b35652a11e7e3"
|
||||
integrity sha512-vXJ0O9T+72HiSEWHPs2cx7YbSO7pQsTMhgqPc5aaddIYpo2clJx1PnYuS0lSNlVaDO0IxQhwYq43evXaXnmviw==
|
||||
dependencies:
|
||||
"@reach/utils" "0.16.0"
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@reach/utils@0.16.0":
|
||||
version "0.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.16.0.tgz#5b0777cf16a7cab1ddd4728d5d02762df0ba84ce"
|
||||
integrity sha512-PCggBet3qaQmwFNcmQ/GqHSefadAFyNCUekq9RrWoaU9hh/S4iaFgf2MBMdM47eQj5i/Bk0Mm07cP/XPFlkN+Q==
|
||||
dependencies:
|
||||
tiny-warning "^1.0.3"
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@react-aria/i18n@^3.3.2":
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/i18n/-/i18n-3.3.2.tgz#891902938333c6ab5491b7acb7581f8567045dbc"
|
||||
@ -6599,6 +6620,11 @@ fast-diff@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
|
||||
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
|
||||
|
||||
fast-equals@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.3.tgz#7039b0a039909f345a2ce53f6202a14e5f392efc"
|
||||
integrity sha512-0EMw4TTUxsMDpDkCg0rXor2gsg+npVrMIHbEhvD0HZyIhUX6AktC/yasm+qKwfyswd06Qy95ZKk8p2crTo0iPA==
|
||||
|
||||
fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||
@ -9179,6 +9205,16 @@ jws@^3.2.2:
|
||||
jwa "^1.4.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
kbar@^0.1.0-beta.15:
|
||||
version "0.1.0-beta.15"
|
||||
resolved "https://registry.yarnpkg.com/kbar/-/kbar-0.1.0-beta.15.tgz#10122a278b3575bdb2db3c0bed62b0fc38b1175a"
|
||||
integrity sha512-haAKaGZLenbONpK4FsF4FVaXudCMcNK2lzjdMM3itvMXiaaVZd5sVW/iHd7ZQ8S4T+zgXkXJE3GxCRMmvpuP7g==
|
||||
dependencies:
|
||||
"@reach/portal" "^0.16.0"
|
||||
fast-equals "^2.0.3"
|
||||
match-sorter "^6.3.0"
|
||||
react-virtual "^2.8.2"
|
||||
|
||||
keygrip@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
|
||||
@ -10071,6 +10107,14 @@ markdown-it@^12.2.0:
|
||||
mdurl "^1.0.1"
|
||||
uc.micro "^1.0.5"
|
||||
|
||||
match-sorter@^6.3.0:
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda"
|
||||
integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
remove-accents "0.4.2"
|
||||
|
||||
matcher-collection@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-2.0.1.tgz#90be1a4cf58d6f2949864f65bb3b0f3e41303b29"
|
||||
@ -12201,6 +12245,13 @@ react-test-renderer@^16.0.0-0:
|
||||
react-is "^16.8.6"
|
||||
scheduler "^0.19.1"
|
||||
|
||||
react-virtual@^2.8.2:
|
||||
version "2.8.2"
|
||||
resolved "https://registry.yarnpkg.com/react-virtual/-/react-virtual-2.8.2.tgz#e204b30c57c426bd260ed1ac49f8b1099e92b7cb"
|
||||
integrity sha512-CwnvF/3Jev4M14S9S7fgzGc0UFQ/bG/VXbrUCq+AB0zH8WGnVDTG0lQT7O3jPY76YLPzTHBu+AMl64Stp8+exg==
|
||||
dependencies:
|
||||
"@reach/observe-rect" "^1.1.0"
|
||||
|
||||
react-virtualized-auto-sizer@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.5.tgz#9eeeb8302022de56fbd7a860b08513120ce36509"
|
||||
@ -12515,6 +12566,11 @@ relateurl@0.2.x:
|
||||
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
||||
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
|
||||
|
||||
remove-accents@0.4.2:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
|
||||
integrity sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=
|
||||
|
||||
remove-bom-buffer@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53"
|
||||
@ -14350,10 +14406,10 @@ tslib@^1.0.0, tslib@^1.9.0, tslib@^1.9.3:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
||||
tsscmp@1.0.6:
|
||||
version "1.0.6"
|
||||
|
Reference in New Issue
Block a user