diff --git a/app/components/DocumentListItem.js b/app/components/DocumentListItem.js index 1a52d4f6..bcc1bfff 100644 --- a/app/components/DocumentListItem.js +++ b/app/components/DocumentListItem.js @@ -15,7 +15,9 @@ import Flex from "components/Flex"; import Highlight from "components/Highlight"; import StarButton, { AnimatedStar } from "components/Star"; import Tooltip from "components/Tooltip"; +import useCurrentTeam from "hooks/useCurrentTeam"; import useCurrentUser from "hooks/useCurrentUser"; +import useStores from "hooks/useStores"; import DocumentMenu from "menus/DocumentMenu"; import { newDocumentUrl } from "utils/routeHelpers"; @@ -41,7 +43,9 @@ function replaceResultMarks(tag: string) { function DocumentListItem(props: Props) { const { t } = useTranslation(); + const { policies } = useStores(); const currentUser = useCurrentUser(); + const currentTeam = useCurrentTeam(); const [menuOpen, setMenuOpen] = React.useState(false); const { document, @@ -60,6 +64,7 @@ function DocumentListItem(props: Props) { !!document.title.toLowerCase().includes(highlight.toLowerCase()); const canStar = !document.isDraft && !document.isArchived && !document.isTemplate; + const can = policies.abilities(currentTeam.id); return ( - {document.isTemplate && !document.isArchived && !document.isDeleted && ( - <> - -   - - )} + {document.isTemplate && + !document.isArchived && + !document.isDeleted && + can.createDocument && ( + <> + +   + + )} - } - exact={false} - label={t("Templates")} - active={ - documents.active ? documents.active.template : undefined - } - /> - } - label={ - - {t("Drafts")} - - - } - active={ - documents.active - ? !documents.active.publishedAt && - !documents.active.isDeleted && - !documents.active.isTemplate - : undefined - } - /> + {can.createDocument && ( + } + exact={false} + label={t("Templates")} + active={ + documents.active ? documents.active.template : undefined + } + /> + )} + {can.createDocument && ( + } + label={ + + {t("Drafts")} + + + } + active={ + documents.active + ? !documents.active.publishedAt && + !documents.active.isDeleted && + !documents.active.isTemplate + : undefined + } + /> + )}
} label={t("Notifications")} /> - } - label={t("API Tokens")} - /> + {can.createApiKey && ( + } + label={t("API Tokens")} + /> + )}
{t("Team")}
diff --git a/app/components/Sidebar/components/Collections.js b/app/components/Sidebar/components/Collections.js index bd55a1e1..97141b08 100644 --- a/app/components/Sidebar/components/Collections.js +++ b/app/components/Sidebar/components/Collections.js @@ -13,6 +13,7 @@ import CollectionsLoading from "./CollectionsLoading"; import DropCursor from "./DropCursor"; import Header from "./Header"; import SidebarLink from "./SidebarLink"; +import useCurrentTeam from "hooks/useCurrentTeam"; type Props = { onCreateCollection: () => void, }; @@ -22,7 +23,9 @@ function Collections({ onCreateCollection }: Props) { const { ui, policies, documents, collections } = useStores(); const isPreloaded: boolean = !!collections.orderedData.length; const { t } = useTranslation(); + const team = useCurrentTeam(); const orderedCollections = collections.orderedData; + const can = policies.abilities(team.id); const [isDraggingAnyCollection, setIsDraggingAnyCollection] = React.useState( false ); @@ -77,13 +80,15 @@ function Collections({ onCreateCollection }: Props) { belowCollection={orderedCollections[index + 1]} /> ))} - } - label={`${t("New collection")}…`} - exact - /> + {can.createCollection && ( + } + label={`${t("New collection")}…`} + exact + /> + )} ); diff --git a/app/menus/NewDocumentMenu.js b/app/menus/NewDocumentMenu.js index 5bb65b0e..a4c37cc8 100644 --- a/app/menus/NewDocumentMenu.js +++ b/app/menus/NewDocumentMenu.js @@ -12,14 +12,21 @@ import ContextMenu from "components/ContextMenu"; import Header from "components/ContextMenu/Header"; import Template from "components/ContextMenu/Template"; import Flex from "components/Flex"; +import useCurrentTeam from "hooks/useCurrentTeam"; import useStores from "hooks/useStores"; import { newDocumentUrl } from "utils/routeHelpers"; function NewDocumentMenu() { const menu = useMenuState(); const { t } = useTranslation(); + const team = useCurrentTeam(); const { collections, policies } = useStores(); const singleCollection = collections.orderedData.length === 1; + const can = policies.abilities(team.id); + + if (!can.createDocument) { + return; + } if (singleCollection) { return ( diff --git a/app/menus/NewTemplateMenu.js b/app/menus/NewTemplateMenu.js index 610fce1f..ae298192 100644 --- a/app/menus/NewTemplateMenu.js +++ b/app/menus/NewTemplateMenu.js @@ -11,13 +11,20 @@ import ContextMenu from "components/ContextMenu"; import Header from "components/ContextMenu/Header"; import Template from "components/ContextMenu/Template"; import Flex from "components/Flex"; +import useCurrentTeam from "hooks/useCurrentTeam"; import useStores from "hooks/useStores"; import { newDocumentUrl } from "utils/routeHelpers"; function NewTemplateMenu() { const menu = useMenuState(); const { t } = useTranslation(); + const team = useCurrentTeam(); const { collections, policies } = useStores(); + const can = policies.abilities(team.id); + + if (!can.createDocument) { + return; + } return ( <> diff --git a/app/menus/ShareMenu.js b/app/menus/ShareMenu.js index e5cb11b8..d5773f49 100644 --- a/app/menus/ShareMenu.js +++ b/app/menus/ShareMenu.js @@ -17,9 +17,10 @@ type Props = { function ShareMenu({ share }: Props) { const menu = useMenuState({ modal: true }); - const { ui, shares } = useStores(); + const { ui, shares, policies } = useStores(); const { t } = useTranslation(); const history = useHistory(); + const can = policies.abilities(share.id); const handleGoToDocument = React.useCallback( (ev: SyntheticEvent<>) => { @@ -57,10 +58,14 @@ function ShareMenu({ share }: Props) { {t("Go to document")} -
- - {t("Revoke link")} - + {can.revoke && ( + <> +
+ + {t("Revoke link")} + + + )} ); diff --git a/app/menus/UserMenu.js b/app/menus/UserMenu.js index cd1d0746..c9a0786d 100644 --- a/app/menus/UserMenu.js +++ b/app/menus/UserMenu.js @@ -37,7 +37,7 @@ function UserMenu({ user }: Props) { [users, user, t] ); - const handleDemote = React.useCallback( + const handleMember = React.useCallback( (ev: SyntheticEvent<>) => { ev.preventDefault(); if ( @@ -49,7 +49,27 @@ function UserMenu({ user }: Props) { ) { return; } - users.demote(user); + users.demote(user, "Member"); + }, + [users, user, t] + ); + + const handleViewer = React.useCallback( + (ev: SyntheticEvent<>) => { + ev.preventDefault(); + if ( + !window.confirm( + t( + "Are you sure you want to make {{ userName }} a read-only viewer? They will not be able to edit any content", + { + userName: user.name, + } + ) + ) + ) { + return; + } + users.demote(user, "Viewer"); }, [users, user, t] ); @@ -95,18 +115,25 @@ function UserMenu({ user }: Props) { {...menu} items={[ { - title: t("Make {{ userName }} a member…", { + title: t("Make {{ userName }} a member", { userName: user.name, }), - onClick: handleDemote, - visible: can.demote, + onClick: handleMember, + visible: can.demote && user.rank !== "Member", + }, + { + title: t("Make {{ userName }} a viewer", { + userName: user.name, + }), + onClick: handleViewer, + visible: can.demote && user.rank !== "Viewer", }, { title: t("Make {{ userName }} an admin…", { userName: user.name, }), onClick: handlePromote, - visible: can.promote, + visible: can.promote && user.rank !== "Admin", }, { type: "separator", diff --git a/app/models/User.js b/app/models/User.js index d6af010b..d870969b 100644 --- a/app/models/User.js +++ b/app/models/User.js @@ -1,5 +1,6 @@ // @flow import { computed } from "mobx"; +import type { Rank } from "shared/types"; import BaseModel from "./BaseModel"; class User extends BaseModel { @@ -8,6 +9,7 @@ class User extends BaseModel { name: string; email: string; isAdmin: boolean; + isViewer: boolean; lastActiveAt: string; isSuspended: boolean; createdAt: string; @@ -17,6 +19,17 @@ class User extends BaseModel { get isInvited(): boolean { return !this.lastActiveAt; } + + @computed + get rank(): Rank { + if (this.isAdmin) { + return "Admin"; + } else if (this.isViewer) { + return "Viewer"; + } else { + return "Member"; + } + } } export default User; diff --git a/app/scenes/Collection.js b/app/scenes/Collection.js index e497f28d..97f7b3ad 100644 --- a/app/scenes/Collection.js +++ b/app/scenes/Collection.js @@ -29,6 +29,7 @@ import Subheading from "components/Subheading"; import Tab from "components/Tab"; import Tabs from "components/Tabs"; import Tooltip from "components/Tooltip"; +import useCurrentTeam from "hooks/useCurrentTeam"; import useImportDocument from "hooks/useImportDocument"; import useStores from "hooks/useStores"; import useUnmount from "hooks/useUnmount"; @@ -39,6 +40,7 @@ function CollectionScene() { const params = useParams(); const { t } = useTranslation(); const { documents, policies, collections, ui } = useStores(); + const team = useCurrentTeam(); const [isFetching, setFetching] = React.useState(); const [error, setError] = React.useState(); const [permissionsModalOpen, setPermissionsModalOpen] = React.useState(false); @@ -46,6 +48,7 @@ function CollectionScene() { const collectionId = params.id || ""; const collection = collections.get(collectionId); const can = policies.abilities(collectionId || ""); + const canUser = policies.abilities(team.id); const { handleFiles, isImporting } = useImportDocument(collectionId); React.useEffect(() => { @@ -116,37 +119,35 @@ function CollectionScene() { } actions={ <> + + + {can.update && ( - <> - - - - - + + - - - - + {t("New doc")} + + + )} + }} />
- Get started by creating a new one! + {canUser.createDocument && ( + Get started by creating a new one! + )} - - - + {canUser.createDocument && ( + + + + )}