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