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:
Tom Moor
2021-11-29 06:40:55 -08:00
committed by GitHub
parent 25ccfb5d04
commit 15b1069bcc
1017 changed files with 17410 additions and 54942 deletions

View File

@@ -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>
</>
);

View File

@@ -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) {

View File

@@ -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,
},

View File

@@ -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 ? (

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
},

View File

@@ -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,
},

View File

@@ -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),

View File

@@ -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,
},

View File

@@ -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,

View File

@@ -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>
)}

View File

@@ -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>
)}

View File

@@ -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(

View File

@@ -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 (

View File

@@ -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,
});
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -1,7 +0,0 @@
// @flow
export default function separator() {
return {
type: "separator",
};
}

7
app/menus/separator.ts Normal file
View File

@@ -0,0 +1,7 @@
import { MenuSeparator } from "~/types";
export default function separator(): MenuSeparator {
return {
type: "separator",
};
}