chore: Menu templates (#1644)
* chore: Menu template system * NewTemplateMenu * UserMenu * MenuItemsTemplate -> DropdownMenuItems * support nested menus * DocumentMenu * BreadcrumbMenu * isInvited
This commit is contained in:
@ -1,25 +1,22 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
||||
import { DropdownMenu } from "components/DropdownMenu";
|
||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
||||
|
||||
type Props = {
|
||||
label: React.Node,
|
||||
path: Array<any>,
|
||||
};
|
||||
|
||||
export default class BreadcrumbMenu extends React.Component<Props> {
|
||||
render() {
|
||||
const { path } = this.props;
|
||||
|
||||
return (
|
||||
<DropdownMenu label={this.props.label} position="center">
|
||||
{path.map((item) => (
|
||||
<DropdownMenuItem as={Link} to={item.url} key={item.id}>
|
||||
{item.title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
export default function BreadcrumbMenu({ label, path }: Props) {
|
||||
return (
|
||||
<DropdownMenu label={label} position="center">
|
||||
<DropdownMenuItems
|
||||
items={path.map((item) => ({
|
||||
title: item.title,
|
||||
to: item.url,
|
||||
}))}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ type Children =
|
||||
| React.Node
|
||||
| ((options: { closePortal: () => void }) => React.Node);
|
||||
|
||||
type Props = {
|
||||
type Props = {|
|
||||
label?: React.Node,
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
@ -27,7 +27,7 @@ type Props = {
|
||||
hover?: boolean,
|
||||
style?: Object,
|
||||
position?: "left" | "right" | "center",
|
||||
};
|
||||
|};
|
||||
|
||||
@observer
|
||||
class DropdownMenu extends React.Component<Props> {
|
||||
|
128
app/components/DropdownMenu/DropdownMenuItems.js
Normal file
128
app/components/DropdownMenu/DropdownMenuItems.js
Normal file
@ -0,0 +1,128 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import DropdownMenu from "./DropdownMenu";
|
||||
import DropdownMenuItem from "./DropdownMenuItem";
|
||||
|
||||
type MenuItem =
|
||||
| {|
|
||||
title: React.Node,
|
||||
to: string,
|
||||
visible?: boolean,
|
||||
disabled?: boolean,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
onClick: (event: SyntheticEvent<>) => void | Promise<void>,
|
||||
visible?: boolean,
|
||||
disabled?: boolean,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
href: string,
|
||||
visible?: boolean,
|
||||
disabled?: boolean,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
visible?: boolean,
|
||||
disabled?: boolean,
|
||||
style?: Object,
|
||||
hover?: boolean,
|
||||
items: MenuItem[],
|
||||
|}
|
||||
| {|
|
||||
type: "separator",
|
||||
visible?: boolean,
|
||||
|}
|
||||
| {|
|
||||
type: "heading",
|
||||
visible?: boolean,
|
||||
title: React.Node,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
items: MenuItem[],
|
||||
|};
|
||||
|
||||
export default function DropdownMenuItems({ items }: Props): React.Node {
|
||||
let filtered = items.filter((item) => item.visible !== false);
|
||||
|
||||
// this block literally just trims unneccessary separators
|
||||
filtered = filtered.reduce((acc, item, index) => {
|
||||
// trim separators from start / end
|
||||
if (item.type === "separator" && index === 0) return acc;
|
||||
if (item.type === "separator" && index === filtered.length - 1) return acc;
|
||||
|
||||
// trim double separators looking ahead / behind
|
||||
const prev = filtered[index - 1];
|
||||
if (prev && prev.type === "separator" && item.type === "separator")
|
||||
return acc;
|
||||
|
||||
// otherwise, continue
|
||||
return [...acc, item];
|
||||
}, []);
|
||||
|
||||
return filtered.map((item, index) => {
|
||||
if (item.to) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
as={Link}
|
||||
to={item.to}
|
||||
key={index}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.title}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.href) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
href={item.href}
|
||||
key={index}
|
||||
disabled={item.disabled}
|
||||
target="_blank"
|
||||
>
|
||||
{item.title}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.onClick) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={item.onClick}
|
||||
disabled={item.disabled}
|
||||
key={index}
|
||||
>
|
||||
{item.title}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.items) {
|
||||
return (
|
||||
<DropdownMenu
|
||||
style={item.style}
|
||||
label={
|
||||
<DropdownMenuItem disabled={item.disabled}>
|
||||
{item.title}
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
hover={item.hover}
|
||||
key={index}
|
||||
>
|
||||
<DropdownMenuItems items={item.items} />
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === "separator") {
|
||||
return <hr key={index} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
@ -11,7 +11,8 @@ import CollectionDelete from "scenes/CollectionDelete";
|
||||
import CollectionEdit from "scenes/CollectionEdit";
|
||||
import CollectionExport from "scenes/CollectionExport";
|
||||
import CollectionMembers from "scenes/CollectionMembers";
|
||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
||||
import { DropdownMenu } from "components/DropdownMenu";
|
||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
||||
import Modal from "components/Modal";
|
||||
import VisuallyHidden from "components/VisuallyHidden";
|
||||
import getDataTransferFiles from "utils/getDataTransferFiles";
|
||||
@ -139,41 +140,43 @@ class CollectionMenu extends React.Component<Props> {
|
||||
/>
|
||||
</Modal>
|
||||
<DropdownMenu onOpen={onOpen} onClose={onClose} position={position}>
|
||||
{collection && (
|
||||
<>
|
||||
{can.update && (
|
||||
<DropdownMenuItem onClick={this.onNewDocument}>
|
||||
New document
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{can.update && (
|
||||
<DropdownMenuItem onClick={this.onImportDocument}>
|
||||
Import document
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{can.update && <hr />}
|
||||
{can.update && (
|
||||
<DropdownMenuItem onClick={this.handleEditCollectionOpen}>
|
||||
Edit…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{can.update && (
|
||||
<DropdownMenuItem onClick={this.handleMembersModalOpen}>
|
||||
Permissions…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{can.export && (
|
||||
<DropdownMenuItem onClick={this.handleExportCollectionOpen}>
|
||||
Export…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{can.delete && (
|
||||
<DropdownMenuItem onClick={this.handleDeleteCollectionOpen}>
|
||||
Delete…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItems
|
||||
items={[
|
||||
{
|
||||
title: "New document",
|
||||
visible: !!(collection && can.update),
|
||||
onClick: this.onNewDocument,
|
||||
},
|
||||
{
|
||||
title: "Import document",
|
||||
visible: !!(collection && can.update),
|
||||
onClick: this.onImportDocument,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: "Edit…",
|
||||
visible: !!(collection && can.update),
|
||||
onClick: this.handleEditCollectionOpen,
|
||||
},
|
||||
{
|
||||
title: "Permissions…",
|
||||
visible: !!(collection && can.update),
|
||||
onClick: this.handleMembersModalOpen,
|
||||
},
|
||||
{
|
||||
title: "Export…",
|
||||
visible: !!(collection && can.export),
|
||||
onClick: this.handleExportCollectionOpen,
|
||||
},
|
||||
{
|
||||
title: "Delete…",
|
||||
visible: !!(collection && can.delete),
|
||||
onClick: this.handleDeleteCollectionOpen,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
<Modal
|
||||
title="Edit collection"
|
||||
|
@ -12,11 +12,8 @@ import DocumentDelete from "scenes/DocumentDelete";
|
||||
import DocumentShare from "scenes/DocumentShare";
|
||||
import DocumentTemplatize from "scenes/DocumentTemplatize";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
Header,
|
||||
} from "components/DropdownMenu";
|
||||
import { DropdownMenu } from "components/DropdownMenu";
|
||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
||||
import Modal from "components/Modal";
|
||||
import {
|
||||
documentHistoryUrl,
|
||||
@ -170,7 +167,7 @@ class DocumentMenu extends React.Component<Props> {
|
||||
} = this.props;
|
||||
|
||||
const can = policies.abilities(document.id);
|
||||
const canShareDocuments = can.share && auth.team && auth.team.sharing;
|
||||
const canShareDocuments = !!(can.share && auth.team && auth.team.sharing);
|
||||
const canViewHistory = can.read && !can.restore;
|
||||
const collection = collections.get(document.collectionId);
|
||||
|
||||
@ -183,146 +180,147 @@ class DocumentMenu extends React.Component<Props> {
|
||||
onClose={onClose}
|
||||
label={label}
|
||||
>
|
||||
{can.unarchive && (
|
||||
<DropdownMenuItem onClick={this.handleRestore}>
|
||||
Restore
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{can.restore &&
|
||||
(collection ? (
|
||||
<DropdownMenuItem onClick={this.handleRestore}>
|
||||
Restore
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenu
|
||||
label={<DropdownMenuItem>Restore…</DropdownMenuItem>}
|
||||
style={{
|
||||
<DropdownMenuItems
|
||||
items={[
|
||||
{
|
||||
title: "Restore",
|
||||
visible: !!can.unarchive,
|
||||
onClick: this.handleRestore,
|
||||
},
|
||||
{
|
||||
title: "Restore",
|
||||
visible: !!(collection && can.restore),
|
||||
onClick: this.handleRestore,
|
||||
},
|
||||
{
|
||||
title: "Restore…",
|
||||
visible: !collection && !!can.restore,
|
||||
style: {
|
||||
left: -170,
|
||||
position: "relative",
|
||||
top: -40,
|
||||
}}
|
||||
hover
|
||||
>
|
||||
<Header>Choose a collection</Header>
|
||||
{collections.orderedData.map((collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
},
|
||||
hover: true,
|
||||
items: [
|
||||
{
|
||||
type: "heading",
|
||||
title: "Choose a collection",
|
||||
},
|
||||
...collections.orderedData.map((collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={collection.id}
|
||||
onClick={(ev) =>
|
||||
this.handleRestore(ev, { collectionId: collection.id })
|
||||
}
|
||||
disabled={!can.update}
|
||||
>
|
||||
<CollectionIcon collection={collection} />
|
||||
{collection.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenu>
|
||||
))}
|
||||
{showPin &&
|
||||
(document.pinned
|
||||
? can.unpin && (
|
||||
<DropdownMenuItem onClick={this.handleUnpin}>
|
||||
Unpin
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
: can.pin && (
|
||||
<DropdownMenuItem onClick={this.handlePin}>
|
||||
Pin to collection
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{document.isStarred
|
||||
? can.unstar && (
|
||||
<DropdownMenuItem onClick={this.handleUnstar}>
|
||||
Unstar
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
: can.star && (
|
||||
<DropdownMenuItem onClick={this.handleStar}>
|
||||
Star
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canShareDocuments && (
|
||||
<DropdownMenuItem
|
||||
onClick={this.handleShareLink}
|
||||
title="Create a public share link"
|
||||
>
|
||||
Share link…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{showToggleEmbeds && (
|
||||
<>
|
||||
{document.embedsDisabled ? (
|
||||
<DropdownMenuItem onClick={document.enableEmbeds}>
|
||||
Enable embeds
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem onClick={document.disableEmbeds}>
|
||||
Disable embeds
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!can.restore && <hr />}
|
||||
|
||||
{can.createChildDocument && (
|
||||
<DropdownMenuItem
|
||||
onClick={this.handleNewChild}
|
||||
title="Create a nested document inside the current document"
|
||||
>
|
||||
New nested document
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{can.update && !document.isTemplate && (
|
||||
<DropdownMenuItem onClick={this.handleOpenTemplateModal}>
|
||||
Create template…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{can.unpublish && (
|
||||
<DropdownMenuItem onClick={this.handleUnpublish}>
|
||||
Unpublish
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{can.update && (
|
||||
<DropdownMenuItem onClick={this.handleEdit}>Edit</DropdownMenuItem>
|
||||
)}
|
||||
{can.update && (
|
||||
<DropdownMenuItem onClick={this.handleDuplicate}>
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{can.archive && (
|
||||
<DropdownMenuItem onClick={this.handleArchive}>
|
||||
Archive
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{can.delete && (
|
||||
<DropdownMenuItem onClick={this.handleDelete}>
|
||||
Delete…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{can.move && (
|
||||
<DropdownMenuItem onClick={this.handleMove}>Move…</DropdownMenuItem>
|
||||
)}
|
||||
<hr />
|
||||
{canViewHistory && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={this.handleDocumentHistory}>
|
||||
History
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{can.download && (
|
||||
<DropdownMenuItem onClick={this.handleExport}>
|
||||
Download
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{showPrint && (
|
||||
<DropdownMenuItem onClick={window.print}>Print</DropdownMenuItem>
|
||||
)}
|
||||
return {
|
||||
title: (
|
||||
<>
|
||||
<CollectionIcon collection={collection} />
|
||||
{collection.name}
|
||||
</>
|
||||
),
|
||||
onClick: (ev) =>
|
||||
this.handleRestore(ev, { collectionId: collection.id }),
|
||||
disabled: !can.update,
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Unpin",
|
||||
onClick: this.handleUnpin,
|
||||
visible: !!(showPin && document.pinned && can.unpin),
|
||||
},
|
||||
{
|
||||
title: "Pin to collection",
|
||||
onClick: this.handlePin,
|
||||
visible: !!(showPin && !document.pinned && can.pin),
|
||||
},
|
||||
{
|
||||
title: "Unstar",
|
||||
onClick: this.handleUnstar,
|
||||
visible: document.isStarred && !!can.unstar,
|
||||
},
|
||||
{
|
||||
title: "Star",
|
||||
onClick: this.handleStar,
|
||||
visible: !document.isStarred && !!can.star,
|
||||
},
|
||||
{
|
||||
title: "Share link…",
|
||||
onClick: this.handleShareLink,
|
||||
visible: canShareDocuments,
|
||||
},
|
||||
{
|
||||
title: "Enable embeds",
|
||||
onClick: document.enableEmbeds,
|
||||
visible: !!showToggleEmbeds && document.embedsDisabled,
|
||||
},
|
||||
{
|
||||
title: "Disable embeds",
|
||||
onClick: document.disableEmbeds,
|
||||
visible: !!showToggleEmbeds && !document.embedsDisabled,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: "New nested document",
|
||||
onClick: this.handleNewChild,
|
||||
visible: !!can.createChildDocument,
|
||||
},
|
||||
{
|
||||
title: "Create template…",
|
||||
onClick: this.handleOpenTemplateModal,
|
||||
visible: !!can.update && !document.isTemplate,
|
||||
},
|
||||
{
|
||||
title: "Edit",
|
||||
onClick: this.handleEdit,
|
||||
visible: !!can.update,
|
||||
},
|
||||
{
|
||||
title: "Duplicate",
|
||||
onClick: this.handleDuplicate,
|
||||
visible: !!can.update,
|
||||
},
|
||||
{
|
||||
title: "Unpublish",
|
||||
onClick: this.handleUnpublish,
|
||||
visible: !!can.unpublish,
|
||||
},
|
||||
{
|
||||
title: "Archive",
|
||||
onClick: this.handleArchive,
|
||||
visible: !!can.archive,
|
||||
},
|
||||
{
|
||||
title: "Delete…",
|
||||
onClick: this.handleDelete,
|
||||
visible: !!can.delete,
|
||||
},
|
||||
{
|
||||
title: "Move…",
|
||||
onClick: this.handleMove,
|
||||
visible: !!can.move,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: "History",
|
||||
onClick: this.handleDocumentHistory,
|
||||
visible: canViewHistory,
|
||||
},
|
||||
{
|
||||
title: "Download",
|
||||
onClick: this.handleExport,
|
||||
visible: !!can.download,
|
||||
},
|
||||
{
|
||||
title: "Print",
|
||||
onClick: window.print,
|
||||
visible: !!showPrint,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
<Modal
|
||||
title={`Delete ${this.props.document.noun}`}
|
||||
|
@ -8,8 +8,8 @@ import UiStore from "stores/UiStore";
|
||||
import Group from "models/Group";
|
||||
import GroupDelete from "scenes/GroupDelete";
|
||||
import GroupEdit from "scenes/GroupEdit";
|
||||
|
||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
||||
import { DropdownMenu } from "components/DropdownMenu";
|
||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
||||
import Modal from "components/Modal";
|
||||
|
||||
type Props = {
|
||||
@ -72,27 +72,29 @@ class GroupMenu extends React.Component<Props> {
|
||||
onSubmit={this.handleDeleteModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<DropdownMenu onOpen={onOpen} onClose={onClose}>
|
||||
{group && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={this.props.onMembers}>
|
||||
Members…
|
||||
</DropdownMenuItem>
|
||||
|
||||
{(can.update || can.delete) && <hr />}
|
||||
|
||||
{can.update && (
|
||||
<DropdownMenuItem onClick={this.onEdit}>Edit…</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{can.delete && (
|
||||
<DropdownMenuItem onClick={this.onDelete}>
|
||||
Delete…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItems
|
||||
items={[
|
||||
{
|
||||
title: "Members…",
|
||||
onClick: this.props.onMembers,
|
||||
visible: !!(group && can.read),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: "Edit…",
|
||||
onClick: this.onEdit,
|
||||
visible: !!(group && can.update),
|
||||
},
|
||||
{
|
||||
title: "Delete…",
|
||||
onClick: this.onDelete,
|
||||
visible: !!(group && can.delete),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
);
|
||||
|
@ -1,13 +1,13 @@
|
||||
// @flow
|
||||
import { observable } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { MoreIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
|
||||
import CollectionsStore from "stores/CollectionsStore";
|
||||
import Document from "models/Document";
|
||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
||||
import { DropdownMenu } from "components/DropdownMenu";
|
||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
@ -39,20 +39,28 @@ class NewChildDocumentMenu extends React.Component<Props> {
|
||||
render() {
|
||||
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
|
||||
|
||||
const { label, document, collections, ...rest } = this.props;
|
||||
const { label, document, collections } = this.props;
|
||||
const collection = collections.get(document.collectionId);
|
||||
|
||||
return (
|
||||
<DropdownMenu label={label || <MoreIcon />} {...rest}>
|
||||
<DropdownMenuItem onClick={this.handleNewDocument}>
|
||||
<span>
|
||||
New document in{" "}
|
||||
<strong>{collection ? collection.name : "collection"}</strong>
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={this.handleNewChild}>
|
||||
New nested document
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenu label={label}>
|
||||
<DropdownMenuItems
|
||||
items={[
|
||||
{
|
||||
title: (
|
||||
<span>
|
||||
New document in{" "}
|
||||
<strong>{collection ? collection.name : "collection"}</strong>
|
||||
</span>
|
||||
),
|
||||
onClick: this.handleNewDocument,
|
||||
},
|
||||
{
|
||||
title: "New nested document",
|
||||
onClick: this.handleNewChild,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
@ -10,11 +10,8 @@ import DocumentsStore from "stores/DocumentsStore";
|
||||
import PoliciesStore from "stores/PoliciesStore";
|
||||
import Button from "components/Button";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
Header,
|
||||
} from "components/DropdownMenu";
|
||||
import { DropdownMenu, Header } from "components/DropdownMenu";
|
||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
@ -63,20 +60,18 @@ class NewDocumentMenu extends React.Component<Props> {
|
||||
{...rest}
|
||||
>
|
||||
<Header>Choose a collection</Header>
|
||||
{collections.orderedData.map((collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={collection.id}
|
||||
onClick={() => this.handleNewDocument(collection.id)}
|
||||
disabled={!can.update}
|
||||
>
|
||||
<CollectionIcon collection={collection} />
|
||||
{collection.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuItems
|
||||
items={collections.orderedData.map((collection) => ({
|
||||
onClick: () => this.handleNewDocument(collection.id),
|
||||
disabled: !policies.abilities(collection.id).update,
|
||||
title: (
|
||||
<>
|
||||
<CollectionIcon collection={collection} />
|
||||
{collection.name}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
@ -9,11 +9,8 @@ import CollectionsStore from "stores/CollectionsStore";
|
||||
import PoliciesStore from "stores/PoliciesStore";
|
||||
import Button from "components/Button";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
Header,
|
||||
} from "components/DropdownMenu";
|
||||
import { DropdownMenu, Header } from "components/DropdownMenu";
|
||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
@ -53,20 +50,18 @@ class NewTemplateMenu extends React.Component<Props> {
|
||||
{...rest}
|
||||
>
|
||||
<Header>Choose a collection</Header>
|
||||
{collections.orderedData.map((collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={collection.id}
|
||||
onClick={() => this.handleNewDocument(collection.id)}
|
||||
disabled={!can.update}
|
||||
>
|
||||
<CollectionIcon collection={collection} />
|
||||
{collection.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuItems
|
||||
items={collections.orderedData.map((collection) => ({
|
||||
onClick: () => this.handleNewDocument(collection.id),
|
||||
disabled: !policies.abilities(collection.id).update,
|
||||
title: (
|
||||
<>
|
||||
<CollectionIcon collection={collection} />
|
||||
{collection.name}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
@ -4,7 +4,8 @@ import * as React from "react";
|
||||
|
||||
import UsersStore from "stores/UsersStore";
|
||||
import User from "models/User";
|
||||
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
||||
import { DropdownMenu } from "components/DropdownMenu";
|
||||
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
|
||||
|
||||
type Props = {
|
||||
user: User,
|
||||
@ -65,31 +66,38 @@ class UserMenu extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
{user.isAdmin && (
|
||||
<DropdownMenuItem onClick={this.handleDemote}>
|
||||
Make {user.name} a member…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!user.isAdmin && !user.isSuspended && (
|
||||
<DropdownMenuItem onClick={this.handlePromote}>
|
||||
Make {user.name} an admin…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!user.lastActiveAt && (
|
||||
<DropdownMenuItem onClick={this.handleRevoke}>
|
||||
Revoke invite…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{user.lastActiveAt &&
|
||||
(user.isSuspended ? (
|
||||
<DropdownMenuItem onClick={this.handleActivate}>
|
||||
Activate account
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem onClick={this.handleSuspend}>
|
||||
Suspend account…
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuItems
|
||||
items={[
|
||||
{
|
||||
title: `Make ${user.name} a member…`,
|
||||
onClick: this.handleDemote,
|
||||
visible: user.isAdmin,
|
||||
},
|
||||
{
|
||||
title: `Make ${user.name} an admin…`,
|
||||
onClick: this.handlePromote,
|
||||
visible: !user.isAdmin && !user.isSuspended,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: "Revoke invite…",
|
||||
onClick: this.handleRevoke,
|
||||
visible: user.isInvited,
|
||||
},
|
||||
{
|
||||
title: "Reactivate account",
|
||||
onClick: this.handleActivate,
|
||||
visible: !user.isInvited && user.isSuspended,
|
||||
},
|
||||
{
|
||||
title: "Suspend account",
|
||||
onClick: this.handleSuspend,
|
||||
visible: !user.isInvited && !user.isSuspended,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import { computed } from "mobx";
|
||||
import BaseModel from "./BaseModel";
|
||||
|
||||
class User extends BaseModel {
|
||||
@ -10,6 +11,11 @@ class User extends BaseModel {
|
||||
lastActiveAt: string;
|
||||
isSuspended: boolean;
|
||||
createdAt: string;
|
||||
|
||||
@computed
|
||||
get isInvited(): boolean {
|
||||
return !this.lastActiveAt;
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
||||
|
@ -45,7 +45,7 @@ const MemberListItem = ({
|
||||
) : (
|
||||
"Never signed in"
|
||||
)}
|
||||
{!user.lastActiveAt && <Badge>Invited</Badge>}
|
||||
{user.isInvited && <Badge>Invited</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
|
||||
</>
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
|
||||
) : (
|
||||
"Never signed in"
|
||||
)}
|
||||
{!user.lastActiveAt && <Badge>Invited</Badge>}
|
||||
{user.isInvited && <Badge>Invited</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
|
||||
</>
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ const GroupMemberListItem = ({
|
||||
) : (
|
||||
"Never signed in"
|
||||
)}
|
||||
{!user.lastActiveAt && <Badge>Invited</Badge>}
|
||||
{user.isInvited && <Badge>Invited</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
|
||||
</>
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
|
||||
) : (
|
||||
"Never signed in"
|
||||
)}
|
||||
{!user.lastActiveAt && <Badge>Invited</Badge>}
|
||||
{user.isInvited && <Badge>Invited</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
|
||||
</>
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export default class UsersStore extends BaseStore<User> {
|
||||
|
||||
@computed
|
||||
get invited(): User[] {
|
||||
return filter(this.orderedData, (user) => !user.lastActiveAt);
|
||||
return filter(this.orderedData, (user) => user.isInvited);
|
||||
}
|
||||
|
||||
@computed
|
||||
|
Reference in New Issue
Block a user