feat: Collection admins (#5273
* Split permissions for reading documents from updating collection * fix: Admins should have collection read permission, tests * tsc * Add admin option to permission selector * Combine publish and create permissions, update -> createDocuments where appropriate * Plural -> singular * wip * Quick version of collection structure loading, will revisit * Remove documentIds method * stash * fixing tests to account for admin creation * Add self-hosted migration * fix: Allow groups to have admin permission * Prefetch collection documents * fix: Document explorer (move/publish) not working with async documents * fix: Cannot re-parent document to collection by drag and drop * fix: Cannot drag to import into collection item without admin permission * Remove unused isEditor getter
This commit is contained in:
@@ -148,7 +148,7 @@ function CollectionScene() {
|
||||
>
|
||||
<DropToImport
|
||||
accept={documents.importFileTypes.join(", ")}
|
||||
disabled={!can.update}
|
||||
disabled={!can.createDocument}
|
||||
collectionId={collection.id}
|
||||
>
|
||||
<CenteredContent withStickyHeader>
|
||||
@@ -159,7 +159,7 @@ function CollectionScene() {
|
||||
<HeadingWithIcon $isStarred={collection.isStarred}>
|
||||
<HeadingIcon collection={collection} size={40} expanded />
|
||||
{collection.name}
|
||||
{!collection.permission && (
|
||||
{collection.isPrivate && (
|
||||
<Tooltip
|
||||
tooltip={t(
|
||||
"This collection is only visible to those given access"
|
||||
|
||||
@@ -37,9 +37,11 @@ function EmptyCollection({ collection }: Props) {
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
{can.update && <Trans>Get started by creating a new one!</Trans>}
|
||||
{can.createDocument && (
|
||||
<Trans>Get started by creating a new one!</Trans>
|
||||
)}
|
||||
</Text>
|
||||
{can.update && (
|
||||
{can.createDocument && (
|
||||
<Empty>
|
||||
<Link to={newDocumentPath(collection.id)}>
|
||||
<Button icon={<NewDocumentIcon />} neutral>
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import CollectionGroupMembership from "~/models/CollectionGroupMembership";
|
||||
import Group from "~/models/Group";
|
||||
import GroupListItem from "~/components/GroupListItem";
|
||||
import InputSelect, { Props as SelectProps } from "~/components/InputSelect";
|
||||
import CollectionGroupMemberMenu from "~/menus/CollectionGroupMemberMenu";
|
||||
import InputMemberPermissionSelect from "./InputMemberPermissionSelect";
|
||||
|
||||
type Props = {
|
||||
group: Group;
|
||||
@@ -21,57 +18,27 @@ const CollectionGroupMemberListItem = ({
|
||||
collectionGroupMembership,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<GroupListItem
|
||||
group={group}
|
||||
showAvatar
|
||||
renderActions={({ openMembersModal }) => (
|
||||
<>
|
||||
<Select
|
||||
label={t("Permissions")}
|
||||
options={[
|
||||
{
|
||||
label: t("View only"),
|
||||
value: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
label: t("View and edit"),
|
||||
value: CollectionPermission.ReadWrite,
|
||||
},
|
||||
]}
|
||||
value={
|
||||
collectionGroupMembership
|
||||
? collectionGroupMembership.permission
|
||||
: undefined
|
||||
}
|
||||
onChange={onUpdate}
|
||||
ariaLabel={t("Permissions")}
|
||||
labelHidden
|
||||
nude
|
||||
/>
|
||||
<CollectionGroupMemberMenu
|
||||
onMembers={openMembersModal}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Select = styled(InputSelect)`
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
color: ${s("textSecondary")};
|
||||
|
||||
select {
|
||||
margin: 0;
|
||||
}
|
||||
` as React.ComponentType<SelectProps>;
|
||||
}: Props) => (
|
||||
<GroupListItem
|
||||
group={group}
|
||||
showAvatar
|
||||
renderActions={({ openMembersModal }) => (
|
||||
<>
|
||||
<InputMemberPermissionSelect
|
||||
value={
|
||||
collectionGroupMembership
|
||||
? collectionGroupMembership.permission
|
||||
: undefined
|
||||
}
|
||||
onChange={onUpdate}
|
||||
/>
|
||||
<CollectionGroupMemberMenu
|
||||
onMembers={openMembersModal}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
export default CollectionGroupMemberListItem;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import InputSelect, { Props as SelectProps } from "~/components/InputSelect";
|
||||
|
||||
export default function InputMemberPermissionSelect(
|
||||
props: Partial<SelectProps>
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={t("Permissions")}
|
||||
options={[
|
||||
{
|
||||
label: t("View only"),
|
||||
value: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
label: t("View and edit"),
|
||||
value: CollectionPermission.ReadWrite,
|
||||
},
|
||||
{
|
||||
label: t("Admin"),
|
||||
value: CollectionPermission.Admin,
|
||||
},
|
||||
]}
|
||||
ariaLabel={t("Permissions")}
|
||||
labelHidden
|
||||
nude
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Select = styled(InputSelect)`
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
color: ${s("textSecondary")};
|
||||
|
||||
select {
|
||||
margin: 0;
|
||||
}
|
||||
` as React.ComponentType<SelectProps>;
|
||||
@@ -1,8 +1,6 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import Membership from "~/models/Membership";
|
||||
import User from "~/models/User";
|
||||
@@ -10,10 +8,10 @@ import Avatar from "~/components/Avatar";
|
||||
import Badge from "~/components/Badge";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
import InputSelect, { Props as SelectProps } from "~/components/InputSelect";
|
||||
import ListItem from "~/components/List/Item";
|
||||
import Time from "~/components/Time";
|
||||
import MemberMenu from "~/menus/MemberMenu";
|
||||
import InputMemberPermissionSelect from "./InputMemberPermissionSelect";
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
@@ -54,24 +52,10 @@ const MemberListItem = ({
|
||||
actions={
|
||||
<Flex align="center" gap={8}>
|
||||
{onUpdate && (
|
||||
<Select
|
||||
label={t("Permissions")}
|
||||
options={[
|
||||
{
|
||||
label: t("View only"),
|
||||
value: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
label: t("View and edit"),
|
||||
value: CollectionPermission.ReadWrite,
|
||||
},
|
||||
]}
|
||||
<InputMemberPermissionSelect
|
||||
value={membership ? membership.permission : undefined}
|
||||
onChange={onUpdate}
|
||||
disabled={!canEdit}
|
||||
ariaLabel={t("Permissions")}
|
||||
labelHidden
|
||||
nude
|
||||
/>
|
||||
)}
|
||||
{canEdit && (
|
||||
@@ -90,16 +74,4 @@ const MemberListItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Select = styled(InputSelect)`
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
color: ${s("textSecondary")};
|
||||
|
||||
select {
|
||||
margin: 0;
|
||||
}
|
||||
` as React.ComponentType<SelectProps>;
|
||||
|
||||
export default observer(MemberListItem);
|
||||
|
||||
@@ -211,7 +211,7 @@ function CollectionPermissions({ collectionId }: Props) {
|
||||
value={collection.permission || ""}
|
||||
/>
|
||||
<PermissionExplainer size="small">
|
||||
{!collection.permission && (
|
||||
{collection.isPrivate && (
|
||||
<Trans
|
||||
defaults="The <em>{{ collectionName }}</em> collection is private. Workspace members have no access to it by default."
|
||||
values={{
|
||||
|
||||
@@ -28,13 +28,15 @@ function DocumentMove({ document }: Props) {
|
||||
null
|
||||
);
|
||||
|
||||
const moveOptions = React.useMemo(() => {
|
||||
// filter out the document itself and also its parent doc if any
|
||||
const items = React.useMemo(() => {
|
||||
// Filter out the document itself and its existing parent doc, if any.
|
||||
const nodes = flatten(collectionTrees.map(flattenTree)).filter(
|
||||
(node) => node.id !== document.id && node.id !== document.parentDocumentId
|
||||
);
|
||||
|
||||
// If the document we're moving is a template, only show collections as
|
||||
// move targets.
|
||||
if (document.isTemplate) {
|
||||
// only show collections with children stripped off to prevent node expansion
|
||||
return nodes
|
||||
.filter((node) => node.type === "collection")
|
||||
.map((node) => ({ ...node, children: [] }));
|
||||
@@ -80,11 +82,7 @@ function DocumentMove({ document }: Props) {
|
||||
|
||||
return (
|
||||
<FlexContainer column>
|
||||
<DocumentExplorer
|
||||
items={moveOptions}
|
||||
onSubmit={move}
|
||||
onSelect={selectPath}
|
||||
/>
|
||||
<DocumentExplorer items={items} onSubmit={move} onSelect={selectPath} />
|
||||
<Footer justify="space-between" align="center" gap={8}>
|
||||
<StyledText type="secondary">
|
||||
{selectedPath ? (
|
||||
|
||||
Reference in New Issue
Block a user