chore: Move to Typescript (#2783)
This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously. closes #1282
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import { development } from "actions/definitions/debug";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import { createAction } from "~/actions";
|
||||
import { development } from "~/actions/definitions/debug";
|
||||
import {
|
||||
navigateToSettings,
|
||||
openKeyboardShortcuts,
|
||||
@@ -15,17 +15,17 @@ import {
|
||||
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 separator from "menus/separator";
|
||||
} 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 separator from "~/menus/separator";
|
||||
|
||||
type Props = {|
|
||||
children: (props: any) => React.Node,
|
||||
|};
|
||||
type Props = {
|
||||
children: (props: any) => React.ReactNode;
|
||||
};
|
||||
|
||||
function AccountMenu(props: Props) {
|
||||
const [sessions] = useSessions();
|
||||
@@ -47,7 +47,9 @@ function AccountMenu(props: Props) {
|
||||
}, [menu, theme, previousTheme]);
|
||||
|
||||
const actions = React.useMemo(() => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'filter' does not exist on type 'Session[... Remove this comment to see the full error message
|
||||
const otherSessions = sessions.filter(
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'session' implicitly has an 'any' type.
|
||||
(session) => session.teamId !== team.id && session.url !== team.url
|
||||
);
|
||||
|
||||
@@ -64,14 +66,16 @@ function AccountMenu(props: Props) {
|
||||
separator(),
|
||||
...(otherSessions.length
|
||||
? [
|
||||
{
|
||||
createAction({
|
||||
name: t("Switch team"),
|
||||
section: "account",
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'session' implicitly has an 'any' type.
|
||||
children: otherSessions.map((session) => ({
|
||||
name: session.name,
|
||||
icon: <Logo alt={session.name} src={session.logoUrl} />,
|
||||
perform: () => (window.location.href = session.url),
|
||||
})),
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
logout,
|
||||
@@ -82,7 +86,7 @@ function AccountMenu(props: Props) {
|
||||
<>
|
||||
<MenuButton {...menu}>{props.children}</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("Account")}>
|
||||
<Template {...menu} actions={actions} />
|
||||
<Template {...menu} items={undefined} actions={actions} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
@@ -1,19 +1,13 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMenuState } from "reakit/Menu";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
|
||||
type MenuItem = {|
|
||||
icon?: React.Node,
|
||||
title: React.Node,
|
||||
to?: string,
|
||||
|};
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import { MenuInternalLink } from "~/types";
|
||||
|
||||
type Props = {
|
||||
items: MenuItem[],
|
||||
items: MenuInternalLink[];
|
||||
};
|
||||
|
||||
export default function BreadcrumbMenu({ items }: Props) {
|
||||
@@ -1,21 +1,21 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMenuState } from "reakit/Menu";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
|
||||
type Props = {|
|
||||
onMembers: () => void,
|
||||
onRemove: () => void,
|
||||
|};
|
||||
type Props = {
|
||||
onMembers: () => void;
|
||||
onRemove: () => void;
|
||||
};
|
||||
|
||||
function CollectionGroupMemberMenu({ onMembers, onRemove }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const menu = useMenuState({ modal: true });
|
||||
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
|
||||
@@ -24,6 +24,7 @@ function CollectionGroupMemberMenu({ onMembers, onRemove }: Props) {
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
type: "button",
|
||||
title: t("Members"),
|
||||
onClick: onMembers,
|
||||
},
|
||||
@@ -31,6 +32,7 @@ function CollectionGroupMemberMenu({ onMembers, onRemove }: Props) {
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Remove"),
|
||||
onClick: onRemove,
|
||||
},
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
NewDocumentIcon,
|
||||
@@ -13,29 +12,30 @@ import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import Collection from "models/Collection";
|
||||
import CollectionDelete from "scenes/CollectionDelete";
|
||||
import CollectionEdit from "scenes/CollectionEdit";
|
||||
import CollectionExport from "scenes/CollectionExport";
|
||||
import CollectionPermissions from "scenes/CollectionPermissions";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Modal from "components/Modal";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import getDataTransferFiles from "utils/getDataTransferFiles";
|
||||
import { newDocumentPath } from "utils/routeHelpers";
|
||||
import Collection from "~/models/Collection";
|
||||
import CollectionDelete from "~/scenes/CollectionDelete";
|
||||
import CollectionEdit from "~/scenes/CollectionEdit";
|
||||
import CollectionExport from "~/scenes/CollectionExport";
|
||||
import CollectionPermissions from "~/scenes/CollectionPermissions";
|
||||
import ContextMenu, { Placement } from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import Modal from "~/components/Modal";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import { MenuItem } from "~/types";
|
||||
import getDataTransferFiles from "~/utils/getDataTransferFiles";
|
||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
collection: Collection,
|
||||
placement?: string,
|
||||
modal?: boolean,
|
||||
label?: (any) => React.Node,
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
|};
|
||||
type Props = {
|
||||
collection: Collection;
|
||||
placement?: Placement;
|
||||
modal?: boolean;
|
||||
label?: (arg0: any) => React.ReactNode;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
function CollectionMenu({
|
||||
collection,
|
||||
@@ -45,15 +45,17 @@ function CollectionMenu({
|
||||
onOpen,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const menu = useMenuState({ modal, placement });
|
||||
const menu = useMenuState({
|
||||
modal,
|
||||
placement,
|
||||
});
|
||||
const [renderModals, setRenderModals] = React.useState(false);
|
||||
const team = useCurrentTeam();
|
||||
const { documents, policies } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
const file = React.useRef<?HTMLInputElement>();
|
||||
const file = React.useRef<HTMLInputElement>(null);
|
||||
const [
|
||||
showCollectionPermissions,
|
||||
setShowCollectionPermissions,
|
||||
@@ -64,25 +66,26 @@ function CollectionMenu({
|
||||
|
||||
const handleOpen = React.useCallback(() => {
|
||||
setRenderModals(true);
|
||||
|
||||
if (onOpen) {
|
||||
onOpen();
|
||||
}
|
||||
}, [onOpen]);
|
||||
|
||||
const handleNewDocument = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
history.push(newDocumentPath(collection.id));
|
||||
},
|
||||
[history, collection.id]
|
||||
);
|
||||
|
||||
const stopPropagation = React.useCallback((ev: SyntheticEvent<>) => {
|
||||
const stopPropagation = React.useCallback((ev: React.SyntheticEvent) => {
|
||||
ev.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const handleImportDocument = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
@@ -95,7 +98,7 @@ function CollectionMenu({
|
||||
);
|
||||
|
||||
const handleFilePicked = React.useCallback(
|
||||
async (ev: SyntheticEvent<>) => {
|
||||
async (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const files = getDataTransferFiles(ev);
|
||||
|
||||
// Because this is the onChange handler it's possible for the change to be
|
||||
@@ -114,7 +117,6 @@ function CollectionMenu({
|
||||
showToast(err.message, {
|
||||
type: "error",
|
||||
});
|
||||
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
@@ -123,16 +125,17 @@ function CollectionMenu({
|
||||
|
||||
const can = policies.abilities(collection.id);
|
||||
const canUserInTeam = policies.abilities(team.id);
|
||||
|
||||
const items = React.useMemo(
|
||||
const items: MenuItem[] = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
type: "button",
|
||||
title: t("New document"),
|
||||
visible: can.update,
|
||||
onClick: handleNewDocument,
|
||||
icon: <NewDocumentIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Import document"),
|
||||
visible: can.update,
|
||||
onClick: handleImportDocument,
|
||||
@@ -142,18 +145,21 @@ function CollectionMenu({
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Edit")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionEdit(true),
|
||||
icon: <EditIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Permissions")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionPermissions(true),
|
||||
icon: <PadlockIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Export")}…`,
|
||||
visible: !!(collection && canUserInTeam.export),
|
||||
onClick: () => setShowCollectionExport(true),
|
||||
@@ -163,6 +169,7 @@ function CollectionMenu({
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Delete")}…`,
|
||||
visible: !!(collection && can.delete),
|
||||
onClick: () => setShowCollectionDelete(true),
|
||||
@@ -193,7 +200,7 @@ function CollectionMenu({
|
||||
onChange={handleFilePicked}
|
||||
onClick={stopPropagation}
|
||||
accept={documents.importFileTypes.join(", ")}
|
||||
tabIndex="-1"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</VisuallyHidden>
|
||||
{label ? (
|
||||
@@ -1,24 +1,24 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { AlphabeticalSortIcon, ManualSortIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||
import Collection from "models/Collection";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import NudeButton from "components/NudeButton";
|
||||
import Collection from "~/models/Collection";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
|
||||
type Props = {|
|
||||
collection: Collection,
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
|};
|
||||
type Props = {
|
||||
collection: Collection;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
function CollectionSortMenu({ collection, onOpen, onClose, ...rest }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const menu = useMenuState({ modal: true });
|
||||
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
const handleChangeSort = React.useCallback(
|
||||
(field: string) => {
|
||||
menu.hide();
|
||||
@@ -31,7 +31,6 @@ function CollectionSortMenu({ collection, onOpen, onClose, ...rest }: Props) {
|
||||
},
|
||||
[collection, menu]
|
||||
);
|
||||
|
||||
const alphabeticalSort = collection.sort.field === "title";
|
||||
|
||||
return (
|
||||
@@ -53,11 +52,13 @@ function CollectionSortMenu({ collection, onOpen, onClose, ...rest }: Props) {
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
type: "button",
|
||||
title: t("Alphabetical sort"),
|
||||
onClick: () => handleChangeSort("title"),
|
||||
selected: alphabeticalSort,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Manual sort"),
|
||||
onClick: () => handleChangeSort("index"),
|
||||
selected: !alphabeticalSort,
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
EditIcon,
|
||||
@@ -26,40 +25,41 @@ import { useHistory } from "react-router-dom";
|
||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import styled from "styled-components";
|
||||
import Document from "models/Document";
|
||||
import DocumentDelete from "scenes/DocumentDelete";
|
||||
import DocumentMove from "scenes/DocumentMove";
|
||||
import DocumentPermanentDelete from "scenes/DocumentPermanentDelete";
|
||||
import DocumentTemplatize from "scenes/DocumentTemplatize";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Flex from "components/Flex";
|
||||
import Modal from "components/Modal";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import getDataTransferFiles from "utils/getDataTransferFiles";
|
||||
import Document from "~/models/Document";
|
||||
import DocumentDelete from "~/scenes/DocumentDelete";
|
||||
import DocumentMove from "~/scenes/DocumentMove";
|
||||
import DocumentPermanentDelete from "~/scenes/DocumentPermanentDelete";
|
||||
import DocumentTemplatize from "~/scenes/DocumentTemplatize";
|
||||
import CollectionIcon from "~/components/CollectionIcon";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import Flex from "~/components/Flex";
|
||||
import Modal from "~/components/Modal";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import { MenuItem } from "~/types";
|
||||
import getDataTransferFiles from "~/utils/getDataTransferFiles";
|
||||
import {
|
||||
documentHistoryUrl,
|
||||
documentUrl,
|
||||
editDocumentUrl,
|
||||
newDocumentPath,
|
||||
} from "utils/routeHelpers";
|
||||
} from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
className: string,
|
||||
isRevision?: boolean,
|
||||
showPrint?: boolean,
|
||||
modal?: boolean,
|
||||
showToggleEmbeds?: boolean,
|
||||
showPin?: boolean,
|
||||
label?: (any) => React.Node,
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
|};
|
||||
type Props = {
|
||||
document: Document;
|
||||
className?: string;
|
||||
isRevision?: boolean;
|
||||
showPrint?: boolean;
|
||||
modal?: boolean;
|
||||
showToggleEmbeds?: boolean;
|
||||
showPin?: boolean;
|
||||
label?: (arg0: any) => React.ReactNode;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
function DocumentMenu({
|
||||
document,
|
||||
@@ -92,60 +92,61 @@ function DocumentMenu({
|
||||
] = React.useState(false);
|
||||
const [showMoveModal, setShowMoveModal] = React.useState(false);
|
||||
const [showTemplateModal, setShowTemplateModal] = React.useState(false);
|
||||
const file = React.useRef<?HTMLInputElement>();
|
||||
const file = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleOpen = React.useCallback(() => {
|
||||
setRenderModals(true);
|
||||
|
||||
if (onOpen) {
|
||||
onOpen();
|
||||
}
|
||||
}, [onOpen]);
|
||||
|
||||
const handleDuplicate = React.useCallback(
|
||||
async (ev: SyntheticEvent<>) => {
|
||||
const duped = await document.duplicate();
|
||||
const handleDuplicate = React.useCallback(async () => {
|
||||
const duped = await document.duplicate();
|
||||
// when duplicating, go straight to the duplicated document content
|
||||
history.push(duped.url);
|
||||
showToast(t("Document duplicated"), {
|
||||
type: "success",
|
||||
});
|
||||
}, [t, history, showToast, document]);
|
||||
|
||||
// when duplicating, go straight to the duplicated document content
|
||||
history.push(duped.url);
|
||||
showToast(t("Document duplicated"), { type: "success" });
|
||||
},
|
||||
[t, history, showToast, document]
|
||||
);
|
||||
|
||||
const handleArchive = React.useCallback(
|
||||
async (ev: SyntheticEvent<>) => {
|
||||
await document.archive();
|
||||
showToast(t("Document archived"), { type: "success" });
|
||||
},
|
||||
[showToast, t, document]
|
||||
);
|
||||
const handleArchive = React.useCallback(async () => {
|
||||
await document.archive();
|
||||
showToast(t("Document archived"), {
|
||||
type: "success",
|
||||
});
|
||||
}, [showToast, t, document]);
|
||||
|
||||
const handleRestore = React.useCallback(
|
||||
async (ev: SyntheticEvent<>, options?: { collectionId: string }) => {
|
||||
async (
|
||||
ev: React.SyntheticEvent,
|
||||
options?: {
|
||||
collectionId: string;
|
||||
}
|
||||
) => {
|
||||
await document.restore(options);
|
||||
showToast(t("Document restored"), { type: "success" });
|
||||
showToast(t("Document restored"), {
|
||||
type: "success",
|
||||
});
|
||||
},
|
||||
[showToast, t, document]
|
||||
);
|
||||
|
||||
const handleUnpublish = React.useCallback(
|
||||
async (ev: SyntheticEvent<>) => {
|
||||
await document.unpublish();
|
||||
showToast(t("Document unpublished"), { type: "success" });
|
||||
},
|
||||
[showToast, t, document]
|
||||
);
|
||||
const handleUnpublish = React.useCallback(async () => {
|
||||
await document.unpublish();
|
||||
showToast(t("Document unpublished"), {
|
||||
type: "success",
|
||||
});
|
||||
}, [showToast, t, document]);
|
||||
|
||||
const handlePrint = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
menu.hide();
|
||||
window.print();
|
||||
},
|
||||
[menu]
|
||||
);
|
||||
const handlePrint = React.useCallback(() => {
|
||||
menu.hide();
|
||||
window.print();
|
||||
}, [menu]);
|
||||
|
||||
const handleStar = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
document.star();
|
||||
@@ -154,7 +155,7 @@ function DocumentMenu({
|
||||
);
|
||||
|
||||
const handleUnstar = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
document.unstar();
|
||||
@@ -167,12 +168,16 @@ function DocumentMenu({
|
||||
const canViewHistory = can.read && !can.restore;
|
||||
const restoreItems = React.useMemo(
|
||||
() => [
|
||||
...collections.orderedData.reduce((filtered, collection) => {
|
||||
...collections.orderedData.reduce<MenuItem[]>((filtered, collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
if (can.update) {
|
||||
filtered.push({
|
||||
onClick: (ev) => handleRestore(ev, { collectionId: collection.id }),
|
||||
type: "button",
|
||||
onClick: (ev) =>
|
||||
handleRestore(ev, {
|
||||
collectionId: collection.id,
|
||||
}),
|
||||
title: (
|
||||
<Flex align="center">
|
||||
<CollectionIcon collection={collection} />
|
||||
@@ -181,18 +186,18 @@ function DocumentMenu({
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, []),
|
||||
],
|
||||
[collections.orderedData, handleRestore, policies]
|
||||
);
|
||||
|
||||
const stopPropagation = React.useCallback((ev: SyntheticEvent<>) => {
|
||||
const stopPropagation = React.useCallback((ev: React.SyntheticEvent) => {
|
||||
ev.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const handleImportDocument = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
@@ -205,7 +210,7 @@ function DocumentMenu({
|
||||
);
|
||||
|
||||
const handleFilePicked = React.useCallback(
|
||||
async (ev: SyntheticEvent<>) => {
|
||||
async (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const files = getDataTransferFiles(ev);
|
||||
|
||||
// Because this is the onChange handler it's possible for the change to be
|
||||
@@ -233,7 +238,6 @@ function DocumentMenu({
|
||||
showToast(err.message, {
|
||||
type: "error",
|
||||
});
|
||||
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
@@ -249,7 +253,7 @@ function DocumentMenu({
|
||||
onChange={handleFilePicked}
|
||||
onClick={stopPropagation}
|
||||
accept={documents.importFileTypes.join(", ")}
|
||||
tabIndex="-1"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</VisuallyHidden>
|
||||
{label ? (
|
||||
@@ -271,12 +275,14 @@ function DocumentMenu({
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
type: "button",
|
||||
title: t("Restore"),
|
||||
visible: (!!collection && can.restore) || can.unarchive,
|
||||
onClick: handleRestore,
|
||||
onClick: (ev) => handleRestore(ev),
|
||||
icon: <RestoreIcon />,
|
||||
},
|
||||
{
|
||||
type: "submenu",
|
||||
title: t("Restore"),
|
||||
visible:
|
||||
!collection && !!can.restore && restoreItems.length !== 0,
|
||||
@@ -296,24 +302,28 @@ function DocumentMenu({
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Unpin"),
|
||||
onClick: document.unpin,
|
||||
visible: !!(showPin && document.pinned && can.unpin),
|
||||
icon: <PinIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Pin to collection"),
|
||||
onClick: document.pin,
|
||||
visible: !!(showPin && !document.pinned && can.pin),
|
||||
icon: <PinIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Unstar"),
|
||||
onClick: handleUnstar,
|
||||
visible: document.isStarred && !!can.unstar,
|
||||
icon: <UnstarredIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Star"),
|
||||
onClick: handleStar,
|
||||
visible: !document.isStarred && !!can.star,
|
||||
@@ -323,12 +333,14 @@ function DocumentMenu({
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
title: t("Edit"),
|
||||
to: editDocumentUrl(document),
|
||||
visible: !!can.update && !team.collaborativeEditing,
|
||||
icon: <EditIcon />,
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
title: t("New nested document"),
|
||||
to: newDocumentPath(document.collectionId, {
|
||||
parentDocumentId: document.id,
|
||||
@@ -337,54 +349,63 @@ function DocumentMenu({
|
||||
icon: <NewDocumentIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Import document"),
|
||||
visible: can.createChildDocument,
|
||||
onClick: handleImportDocument,
|
||||
icon: <ImportIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Create template")}…`,
|
||||
onClick: () => setShowTemplateModal(true),
|
||||
visible: !!can.update && !document.isTemplate,
|
||||
icon: <ShapesIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Duplicate"),
|
||||
onClick: handleDuplicate,
|
||||
visible: !!can.update,
|
||||
icon: <DuplicateIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Unpublish"),
|
||||
onClick: handleUnpublish,
|
||||
visible: !!can.unpublish,
|
||||
icon: <UnpublishIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Archive"),
|
||||
onClick: handleArchive,
|
||||
visible: !!can.archive,
|
||||
icon: <ArchiveIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Delete")}…`,
|
||||
onClick: () => setShowDeleteModal(true),
|
||||
visible: !!can.delete,
|
||||
icon: <TrashIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Permanently delete")}…`,
|
||||
onClick: () => setShowPermanentDeleteModal(true),
|
||||
visible: can.permanentDelete,
|
||||
icon: <CrossIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Move")}…`,
|
||||
onClick: () => setShowMoveModal(true),
|
||||
visible: !!can.move,
|
||||
icon: <MoveIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Enable embeds"),
|
||||
onClick: document.enableEmbeds,
|
||||
visible:
|
||||
@@ -394,6 +415,7 @@ function DocumentMenu({
|
||||
icon: <BuildingBlocksIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Disable embeds"),
|
||||
onClick: document.disableEmbeds,
|
||||
visible:
|
||||
@@ -406,6 +428,7 @@ function DocumentMenu({
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
title: t("History"),
|
||||
to: isRevision
|
||||
? documentUrl(document)
|
||||
@@ -414,12 +437,14 @@ function DocumentMenu({
|
||||
icon: <HistoryIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Download"),
|
||||
onClick: document.download,
|
||||
visible: !!can.download,
|
||||
icon: <DownloadIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Print"),
|
||||
onClick: handlePrint,
|
||||
visible: !!showPrint,
|
||||
@@ -1,19 +1,20 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMenuState } from "reakit/Menu";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
|
||||
type Props = {|
|
||||
id: string,
|
||||
onDelete: (ev: SyntheticEvent<>) => Promise<void>,
|
||||
|};
|
||||
type Props = {
|
||||
id: string;
|
||||
onDelete: (ev: React.SyntheticEvent) => Promise<void>;
|
||||
};
|
||||
|
||||
function FileOperationMenu({ id, onDelete }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const menu = useMenuState({ modal: true });
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -23,6 +24,7 @@ function FileOperationMenu({ id, onDelete }: Props) {
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
type: "link",
|
||||
title: t("Download"),
|
||||
href: "/api/fileOperations.redirect?id=" + id,
|
||||
},
|
||||
@@ -30,6 +32,7 @@ function FileOperationMenu({ id, onDelete }: Props) {
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Delete"),
|
||||
onClick: onDelete,
|
||||
},
|
||||
@@ -1,20 +1,20 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMenuState } from "reakit/Menu";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
|
||||
type Props = {|
|
||||
onRemove: () => void,
|
||||
|};
|
||||
type Props = {
|
||||
onRemove: () => void;
|
||||
};
|
||||
|
||||
function GroupMemberMenu({ onRemove }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const menu = useMenuState({ modal: true });
|
||||
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
|
||||
@@ -23,6 +23,7 @@ function GroupMemberMenu({ onRemove }: Props) {
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
type: "button",
|
||||
title: t("Remove"),
|
||||
onClick: onRemove,
|
||||
},
|
||||
@@ -1,26 +1,27 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMenuState } from "reakit/Menu";
|
||||
import Group from "models/Group";
|
||||
import GroupDelete from "scenes/GroupDelete";
|
||||
import GroupEdit from "scenes/GroupEdit";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Modal from "components/Modal";
|
||||
import useStores from "hooks/useStores";
|
||||
import Group from "~/models/Group";
|
||||
import GroupDelete from "~/scenes/GroupDelete";
|
||||
import GroupEdit from "~/scenes/GroupEdit";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import Modal from "~/components/Modal";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {|
|
||||
group: Group,
|
||||
onMembers: () => void,
|
||||
|};
|
||||
type Props = {
|
||||
group: Group;
|
||||
onMembers: () => void;
|
||||
};
|
||||
|
||||
function GroupMenu({ group, onMembers }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { policies } = useStores();
|
||||
const menu = useMenuState({ modal: true });
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
const [editModalOpen, setEditModalOpen] = React.useState(false);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = React.useState(false);
|
||||
const can = policies.abilities(group.id);
|
||||
@@ -47,6 +48,7 @@ function GroupMenu({ group, onMembers }: Props) {
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Members")}…`,
|
||||
onClick: onMembers,
|
||||
visible: !!(group && can.read),
|
||||
@@ -55,11 +57,13 @@ function GroupMenu({ group, onMembers }: Props) {
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Edit")}…`,
|
||||
onClick: () => setEditModalOpen(true),
|
||||
visible: !!(group && can.update),
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Delete")}…`,
|
||||
onClick: () => setDeleteModalOpen(true),
|
||||
visible: !!(group && can.delete),
|
||||
@@ -1,19 +1,19 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMenuState } from "reakit/Menu";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
|
||||
type Props = {|
|
||||
onRemove: () => void,
|
||||
|};
|
||||
type Props = {
|
||||
onRemove: () => void;
|
||||
};
|
||||
|
||||
function MemberMenu({ onRemove }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const menu = useMenuState({ modal: true });
|
||||
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
|
||||
@@ -22,6 +22,7 @@ function MemberMenu({ onRemove }: Props) {
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
type: "button",
|
||||
title: t("Remove"),
|
||||
onClick: onRemove,
|
||||
},
|
||||
@@ -1,21 +1,22 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||
import Document from "models/Document";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import useStores from "hooks/useStores";
|
||||
import { newDocumentPath } from "utils/routeHelpers";
|
||||
import Document from "~/models/Document";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
label?: (any) => React.Node,
|
||||
document: Document,
|
||||
label?: (arg0: any) => React.ReactNode;
|
||||
document: Document;
|
||||
};
|
||||
|
||||
function NewChildDocumentMenu({ document, label }: Props) {
|
||||
const menu = useMenuState({ modal: true });
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
const { collections } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const collection = collections.get(document.collectionId);
|
||||
@@ -29,18 +30,24 @@ function NewChildDocumentMenu({ document, label }: Props) {
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
type: "route",
|
||||
title: (
|
||||
<span>
|
||||
<Trans
|
||||
defaults="New document in <em>{{ collectionName }}</em>"
|
||||
values={{ collectionName }}
|
||||
components={{ em: <strong /> }}
|
||||
values={{
|
||||
collectionName,
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
to: newDocumentPath(document.collectionId),
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
title: t("New nested document"),
|
||||
to: newDocumentPath(document.collectionId, {
|
||||
parentDocumentId: document.id,
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
@@ -6,34 +5,38 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import Button from "components/Button";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Header from "components/ContextMenu/Header";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import { newDocumentPath } from "utils/routeHelpers";
|
||||
import Button from "~/components/Button";
|
||||
import CollectionIcon from "~/components/CollectionIcon";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import Header from "~/components/ContextMenu/Header";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { MenuItem } from "~/types";
|
||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
|
||||
function NewDocumentMenu() {
|
||||
const menu = useMenuState({ modal: true });
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const team = useCurrentTeam();
|
||||
const { collections, policies } = useStores();
|
||||
const can = policies.abilities(team.id);
|
||||
|
||||
const items = React.useMemo(
|
||||
() =>
|
||||
collections.orderedData.reduce((filtered, collection) => {
|
||||
collections.orderedData.reduce<MenuItem[]>((filtered, collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
if (can.update) {
|
||||
filtered.push({
|
||||
type: "route",
|
||||
to: newDocumentPath(collection.id),
|
||||
title: <CollectionName>{collection.name}</CollectionName>,
|
||||
icon: <CollectionIcon collection={collection} />,
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, []),
|
||||
[collections.orderedData, policies]
|
||||
@@ -45,7 +48,11 @@ function NewDocumentMenu() {
|
||||
|
||||
if (items.length === 1) {
|
||||
return (
|
||||
<Button as={Link} to={items[0].to} icon={<PlusIcon />}>
|
||||
<Button
|
||||
as={Link}
|
||||
to={items[0].type === "route" ? items[0].to : undefined}
|
||||
icon={<PlusIcon />}
|
||||
>
|
||||
{t("New doc")}
|
||||
</Button>
|
||||
);
|
||||
@@ -55,7 +62,7 @@ function NewDocumentMenu() {
|
||||
<>
|
||||
<MenuButton {...menu}>
|
||||
{(props) => (
|
||||
<Button icon={<PlusIcon />} {...props} small>
|
||||
<Button icon={<PlusIcon />} {...props}>
|
||||
{`${t("New doc")}…`}
|
||||
</Button>
|
||||
)}
|
||||
@@ -1,21 +1,23 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } 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 Button from "components/Button";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Header from "components/ContextMenu/Header";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import { newDocumentPath } from "utils/routeHelpers";
|
||||
import Button from "~/components/Button";
|
||||
import CollectionIcon from "~/components/CollectionIcon";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import Header from "~/components/ContextMenu/Header";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { MenuItem } from "~/types";
|
||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
|
||||
function NewTemplateMenu() {
|
||||
const menu = useMenuState({ modal: true });
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const team = useCurrentTeam();
|
||||
const { collections, policies } = useStores();
|
||||
@@ -23,15 +25,20 @@ function NewTemplateMenu() {
|
||||
|
||||
const items = React.useMemo(
|
||||
() =>
|
||||
collections.orderedData.reduce((filtered, collection) => {
|
||||
collections.orderedData.reduce<MenuItem[]>((filtered, collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
if (can.update) {
|
||||
filtered.push({
|
||||
to: newDocumentPath(collection.id, { template: true }),
|
||||
type: "route",
|
||||
to: newDocumentPath(collection.id, {
|
||||
template: true,
|
||||
}),
|
||||
title: <CollectionName>{collection.name}</CollectionName>,
|
||||
icon: <CollectionIcon collection={collection} />,
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, []),
|
||||
[collections.orderedData, policies]
|
||||
@@ -45,7 +52,7 @@ function NewTemplateMenu() {
|
||||
<>
|
||||
<MenuButton {...menu}>
|
||||
{(props) => (
|
||||
<Button icon={<PlusIcon />} {...props} small>
|
||||
<Button icon={<PlusIcon />} {...props}>
|
||||
{t("New template")}…
|
||||
</Button>
|
||||
)}
|
||||
@@ -1,36 +1,37 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { RestoreIcon, LinkIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useMenuState } from "reakit/Menu";
|
||||
import Document from "models/Document";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import MenuItem from "components/ContextMenu/MenuItem";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Separator from "components/ContextMenu/Separator";
|
||||
import CopyToClipboard from "components/CopyToClipboard";
|
||||
import MenuIconWrapper from "components/MenuIconWrapper";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||
import Document from "~/models/Document";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import MenuItem from "~/components/ContextMenu/MenuItem";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Separator from "~/components/ContextMenu/Separator";
|
||||
import CopyToClipboard from "~/components/CopyToClipboard";
|
||||
import MenuIconWrapper from "~/components/MenuIconWrapper";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import { documentHistoryUrl } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
revisionId: string,
|
||||
className?: string,
|
||||
|};
|
||||
type Props = {
|
||||
document: Document;
|
||||
revisionId: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function RevisionMenu({ document, revisionId, className }: Props) {
|
||||
const { showToast } = useToasts();
|
||||
const team = useCurrentTeam();
|
||||
const menu = useMenuState({ modal: true });
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
const handleRestore = React.useCallback(
|
||||
async (ev: SyntheticEvent<>) => {
|
||||
async (ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (team.collaborativeEditing) {
|
||||
@@ -39,8 +40,12 @@ function RevisionMenu({ document, revisionId, className }: Props) {
|
||||
revisionId,
|
||||
});
|
||||
} else {
|
||||
await document.restore({ revisionId });
|
||||
showToast(t("Document restored"), { type: "success" });
|
||||
await document.restore({
|
||||
revisionId,
|
||||
});
|
||||
showToast(t("Document restored"), {
|
||||
type: "success",
|
||||
});
|
||||
history.push(document.url);
|
||||
}
|
||||
},
|
||||
@@ -48,7 +53,9 @@ function RevisionMenu({ document, revisionId, className }: Props) {
|
||||
);
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
showToast(t("Link copied"), { type: "info" });
|
||||
showToast(t("Link copied"), {
|
||||
type: "info",
|
||||
});
|
||||
}, [showToast, t]);
|
||||
|
||||
const url = `${window.location.origin}${documentHistoryUrl(
|
||||
@@ -1,23 +1,24 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useMenuState } from "reakit/Menu";
|
||||
import Share from "models/Share";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import MenuItem from "components/ContextMenu/MenuItem";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import CopyToClipboard from "components/CopyToClipboard";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import Share from "~/models/Share";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import MenuItem from "~/components/ContextMenu/MenuItem";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import CopyToClipboard from "~/components/CopyToClipboard";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
|
||||
type Props = {
|
||||
share: Share,
|
||||
share: Share;
|
||||
};
|
||||
|
||||
function ShareMenu({ share }: Props) {
|
||||
const menu = useMenuState({ modal: true });
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
const { shares, policies } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const { t } = useTranslation();
|
||||
@@ -25,7 +26,7 @@ function ShareMenu({ share }: Props) {
|
||||
const can = policies.abilities(share.id);
|
||||
|
||||
const handleGoToDocument = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
history.push(share.documentUrl);
|
||||
},
|
||||
@@ -33,21 +34,27 @@ function ShareMenu({ share }: Props) {
|
||||
);
|
||||
|
||||
const handleRevoke = React.useCallback(
|
||||
async (ev: SyntheticEvent<>) => {
|
||||
async (ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
try {
|
||||
await shares.revoke(share);
|
||||
showToast(t("Share link revoked"), { type: "info" });
|
||||
showToast(t("Share link revoked"), {
|
||||
type: "info",
|
||||
});
|
||||
} catch (err) {
|
||||
showToast(err.message, { type: "error" });
|
||||
showToast(err.message, {
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
[t, shares, share, showToast]
|
||||
);
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
showToast(t("Share link copied"), { type: "info" });
|
||||
showToast(t("Share link copied"), {
|
||||
type: "info",
|
||||
});
|
||||
}, [t, showToast]);
|
||||
|
||||
return (
|
||||
@@ -1,17 +1,20 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { TableOfContentsIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import Button from "components/Button";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import { type MenuItem } from "types";
|
||||
import Button from "~/components/Button";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import { MenuItem } from "~/types";
|
||||
|
||||
type Props = {|
|
||||
headings: { title: string, level: number, id: string }[],
|
||||
|};
|
||||
type Props = {
|
||||
headings: {
|
||||
title: string;
|
||||
level: number;
|
||||
id: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
function TableOfContentsMenu({ headings }: Props) {
|
||||
const menu = useMenuState({
|
||||
@@ -20,22 +23,22 @@ function TableOfContentsMenu({ headings }: Props) {
|
||||
unstable_fixed: true,
|
||||
unstable_flip: true,
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const minHeading = headings.reduce(
|
||||
(memo, heading) => (heading.level < memo ? heading.level : memo),
|
||||
Infinity
|
||||
);
|
||||
|
||||
// @ts-expect-error check
|
||||
const items: MenuItem[] = React.useMemo(() => {
|
||||
let i = [
|
||||
const i = [
|
||||
{
|
||||
type: "heading",
|
||||
visible: true,
|
||||
title: t("Contents"),
|
||||
},
|
||||
...headings.map((heading) => ({
|
||||
type: "link",
|
||||
href: `#${heading.id}`,
|
||||
title: t(heading.title),
|
||||
level: heading.level - minHeading,
|
||||
@@ -44,8 +47,10 @@ function TableOfContentsMenu({ headings }: Props) {
|
||||
|
||||
if (i.length === 1) {
|
||||
i.push({
|
||||
type: "link",
|
||||
href: "#",
|
||||
title: t("Headings you add to the document will appear here"),
|
||||
// @ts-expect-error check
|
||||
disabled: true,
|
||||
});
|
||||
}
|
||||
@@ -1,24 +1,25 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { DocumentIcon } 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 Document from "models/Document";
|
||||
import Button from "components/Button";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import MenuItem from "components/ContextMenu/MenuItem";
|
||||
import Separator from "components/ContextMenu/Separator";
|
||||
import useStores from "hooks/useStores";
|
||||
import Document from "~/models/Document";
|
||||
import Button from "~/components/Button";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import MenuItem from "~/components/ContextMenu/MenuItem";
|
||||
import Separator from "~/components/ContextMenu/Separator";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
onSelectTemplate: (template: Document) => void,
|
||||
|};
|
||||
type Props = {
|
||||
document: Document;
|
||||
onSelectTemplate: (template: Document) => void;
|
||||
};
|
||||
|
||||
function TemplatesMenu({ onSelectTemplate, document }: Props) {
|
||||
const menu = useMenuState({ modal: true });
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
const { documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const templates = documents.templates;
|
||||
@@ -34,7 +35,7 @@ function TemplatesMenu({ onSelectTemplate, document }: Props) {
|
||||
(t) => t.collectionId !== document.collectionId
|
||||
);
|
||||
|
||||
const renderTemplate = (template) => (
|
||||
const renderTemplate = (template: Document) => (
|
||||
<MenuItem
|
||||
key={template.id}
|
||||
onClick={() => onSelectTemplate(template)}
|
||||
@@ -45,7 +46,9 @@ function TemplatesMenu({ onSelectTemplate, document }: Props) {
|
||||
<strong>{template.titleWithDefault}</strong>
|
||||
<br />
|
||||
<Author>
|
||||
{t("By {{ author }}", { author: template.createdBy.name })}
|
||||
{t("By {{ author }}", {
|
||||
author: template.createdBy.name,
|
||||
})}
|
||||
</Author>
|
||||
</TemplateItem>
|
||||
</MenuItem>
|
||||
@@ -1,45 +1,51 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMenuState } from "reakit/Menu";
|
||||
import User from "models/User";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import useStores from "hooks/useStores";
|
||||
import User from "~/models/User";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {|
|
||||
user: User,
|
||||
|};
|
||||
type Props = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
function UserMenu({ user }: Props) {
|
||||
const { users, policies } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const menu = useMenuState({ modal: true });
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
});
|
||||
const can = policies.abilities(user.id);
|
||||
|
||||
const handlePromote = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (
|
||||
!window.confirm(
|
||||
t(
|
||||
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.",
|
||||
{ userName: user.name }
|
||||
{
|
||||
userName: user.name,
|
||||
}
|
||||
)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
users.promote(user);
|
||||
},
|
||||
[users, user, t]
|
||||
);
|
||||
|
||||
const handleMember = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (
|
||||
!window.confirm(
|
||||
t("Are you sure you want to make {{ userName }} a member?", {
|
||||
@@ -49,14 +55,16 @@ function UserMenu({ user }: Props) {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
users.demote(user, "member");
|
||||
},
|
||||
[users, user, t]
|
||||
);
|
||||
|
||||
const handleViewer = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (
|
||||
!window.confirm(
|
||||
t(
|
||||
@@ -69,14 +77,16 @@ function UserMenu({ user }: Props) {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
users.demote(user, "viewer");
|
||||
},
|
||||
[users, user, t]
|
||||
);
|
||||
|
||||
const handleSuspend = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (
|
||||
!window.confirm(
|
||||
t(
|
||||
@@ -86,21 +96,24 @@ function UserMenu({ user }: Props) {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
users.suspend(user);
|
||||
},
|
||||
[users, user, t]
|
||||
);
|
||||
|
||||
const handleRevoke = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
users.delete(user, { confirmation: true });
|
||||
users.delete(user, {
|
||||
confirmation: true,
|
||||
});
|
||||
},
|
||||
[users, user]
|
||||
);
|
||||
|
||||
const handleActivate = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
(ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
users.activate(user);
|
||||
},
|
||||
@@ -115,6 +128,7 @@ function UserMenu({ user }: Props) {
|
||||
{...menu}
|
||||
items={[
|
||||
{
|
||||
type: "button",
|
||||
title: t("Make {{ userName }} a member", {
|
||||
userName: user.name,
|
||||
}),
|
||||
@@ -122,6 +136,7 @@ function UserMenu({ user }: Props) {
|
||||
visible: can.demote && user.role !== "member",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Make {{ userName }} a viewer", {
|
||||
userName: user.name,
|
||||
}),
|
||||
@@ -129,6 +144,7 @@ function UserMenu({ user }: Props) {
|
||||
visible: can.demote && user.role !== "viewer",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Make {{ userName }} an admin…", {
|
||||
userName: user.name,
|
||||
}),
|
||||
@@ -139,16 +155,19 @@ function UserMenu({ user }: Props) {
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Revoke invite")}…`,
|
||||
onClick: handleRevoke,
|
||||
visible: user.isInvited,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: t("Activate account"),
|
||||
onClick: handleActivate,
|
||||
visible: !user.isInvited && user.isSuspended,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Suspend account")}…`,
|
||||
onClick: handleSuspend,
|
||||
visible: !user.isInvited && !user.isSuspended,
|
||||
@@ -1,7 +0,0 @@
|
||||
// @flow
|
||||
|
||||
export default function separator() {
|
||||
return {
|
||||
type: "separator",
|
||||
};
|
||||
}
|
||||
7
app/menus/separator.ts
Normal file
7
app/menus/separator.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { MenuSeparator } from "~/types";
|
||||
|
||||
export default function separator(): MenuSeparator {
|
||||
return {
|
||||
type: "separator",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user