feat: Add read-only collections (#1991)

closes #1017
This commit is contained in:
Tom Moor
2021-03-30 21:02:08 -07:00
committed by GitHub
parent d7acf616cf
commit 7e1b07ef98
50 changed files with 940 additions and 558 deletions

View File

@@ -4,7 +4,6 @@ import {
ArchiveIcon,
EditIcon,
GoToIcon,
PadlockIcon,
ShapesIcon,
TrashIcon,
} from "outline-icons";
@@ -103,11 +102,6 @@ const Breadcrumb = ({ document, children, onlyText }: Props) => {
if (onlyText === true) {
return (
<>
{collection.private && (
<>
<SmallPadlockIcon color="currentColor" size={16} />{" "}
</>
)}
{collection.name}
{path.map((n) => (
<React.Fragment key={n.id}>
@@ -154,11 +148,6 @@ export const Slash = styled(GoToIcon)`
fill: ${(props) => props.theme.divider};
`;
const SmallPadlockIcon = styled(PadlockIcon)`
display: inline-block;
vertical-align: sub;
`;
const SmallSlash = styled(GoToIcon)`
width: 12px;
height: 12px;

11
app/components/Divider.js Normal file
View File

@@ -0,0 +1,11 @@
// @flow
import styled from "styled-components";
const Divider = styled.hr`
border: 0;
border-bottom: 1px solid ${(props) => props.theme.divider};
margin: 0;
padding: 0;
`;
export default Divider;

View File

@@ -1,6 +1,7 @@
// @flow
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import { GroupIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import { MAX_AVATAR_DISPLAY } from "shared/constants";
@@ -17,7 +18,8 @@ type Props = {
group: Group,
groupMemberships: GroupMembershipsStore,
membership?: CollectionGroupMembership,
showFacepile: boolean,
showFacepile?: boolean,
showAvatar?: boolean,
renderActions: ({ openMembersModal: () => void }) => React.Node,
};
@@ -48,6 +50,11 @@ class GroupListItem extends React.Component<Props> {
return (
<>
<ListItem
image={
<Image>
<GroupIcon size={28} />
</Image>
}
title={
<Title onClick={this.handleMembersModalOpen}>{group.name}</Title>
}
@@ -84,6 +91,15 @@ class GroupListItem extends React.Component<Props> {
}
}
const Image = styled(Flex)`
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: ${(props) => props.theme.secondaryBackground};
border-radius: 20px;
`;
const Title = styled.span`
&:hover {
text-decoration: underline;

View File

@@ -27,7 +27,7 @@ const Wrapper = styled.label`
max-width: ${(props) => (props.short ? "350px" : "100%")};
`;
type Option = { label: string, value: string };
export type Option = { label: string, value: string };
export type Props = {
value?: string,

View File

@@ -0,0 +1,22 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import InputSelect, { type Props, type Option } from "./InputSelect";
export default function InputSelectPermission(
props: $Rest<Props, { options: Array<Option> }>
) {
const { t } = useTranslation();
return (
<InputSelect
label={t("Default access")}
options={[
{ label: t("View and edit"), value: "read_write" },
{ label: t("View only"), value: "read" },
{ label: t("No access"), value: "" },
]}
{...props}
/>
);
}

View File

@@ -17,12 +17,10 @@ const Labeled = ({ label, children, ...props }: Props) => (
);
export const Label = styled(Flex)`
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
color: ${(props) => props.theme.textTertiary};
letter-spacing: 0.04em;
padding-bottom: 4px;
display: inline-block;
color: ${(props) => props.theme.text};
`;
export default observer(Labeled);

View File

@@ -27,7 +27,7 @@ const ListItem = ({ image, title, subtitle, actions }: Props) => {
const Wrapper = styled.li`
display: flex;
padding: ${(props) => (props.compact ? "8px" : "12px")} 0;
padding: 8px 0;
margin: 0;
border-bottom: 1px solid ${(props) => props.theme.divider};

View File

@@ -9,7 +9,7 @@ import Collection from "models/Collection";
import CollectionDelete from "scenes/CollectionDelete";
import CollectionEdit from "scenes/CollectionEdit";
import CollectionExport from "scenes/CollectionExport";
import CollectionMembers from "scenes/CollectionMembers";
import CollectionPermissions from "scenes/CollectionPermissions";
import ContextMenu from "components/ContextMenu";
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
import Template from "components/ContextMenu/Template";
@@ -42,9 +42,10 @@ function CollectionMenu({
const history = useHistory();
const file = React.useRef<?HTMLInputElement>();
const [showCollectionMembers, setShowCollectionMembers] = React.useState(
false
);
const [
showCollectionPermissions,
setShowCollectionPermissions,
] = React.useState(false);
const [showCollectionEdit, setShowCollectionEdit] = React.useState(false);
const [showCollectionDelete, setShowCollectionDelete] = React.useState(false);
const [showCollectionExport, setShowCollectionExport] = React.useState(false);
@@ -155,9 +156,9 @@ function CollectionMenu({
onClick: () => setShowCollectionEdit(true),
},
{
title: `${t("Members")}`,
title: `${t("Permissions")}`,
visible: can.update,
onClick: () => setShowCollectionMembers(true),
onClick: () => setShowCollectionPermissions(true),
},
{
title: `${t("Export")}`,
@@ -178,15 +179,11 @@ function CollectionMenu({
{renderModals && (
<>
<Modal
title={t("Collection members")}
onRequestClose={() => setShowCollectionMembers(false)}
isOpen={showCollectionMembers}
title={t("Collection permissions")}
onRequestClose={() => setShowCollectionPermissions(false)}
isOpen={showCollectionPermissions}
>
<CollectionMembers
collection={collection}
onSubmit={() => setShowCollectionMembers(false)}
onEdit={() => setShowCollectionEdit(true)}
/>
<CollectionPermissions collection={collection} />
</Modal>
<Modal
title={t("Edit collection")}

View File

@@ -15,7 +15,7 @@ export default class Collection extends BaseModel {
description: string;
icon: string;
color: string;
private: boolean;
permission: "read" | "read_write" | void;
sharing: boolean;
index: string;
documents: NavigationNode[];
@@ -25,11 +25,6 @@ export default class Collection extends BaseModel {
sort: { field: string, direction: "asc" | "desc" };
url: string;
@computed
get isPrivate(): boolean {
return this.private;
}
@computed
get isEmpty(): boolean {
return this.documents.length === 0;
@@ -121,7 +116,7 @@ export default class Collection extends BaseModel {
"description",
"sharing",
"icon",
"private",
"permission",
"sort",
"index",
]);

View File

@@ -12,8 +12,7 @@ import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
import Collection from "models/Collection";
import CollectionEdit from "scenes/CollectionEdit";
import CollectionMembers from "scenes/CollectionMembers";
import CollectionPermissions from "scenes/CollectionPermissions";
import Search from "scenes/Search";
import { Action, Separator } from "components/Actions";
import Badge from "components/Badge";
@@ -53,7 +52,6 @@ class CollectionScene extends React.Component<Props> {
@observable collection: ?Collection;
@observable isFetching: boolean = true;
@observable permissionsModalOpen: boolean = false;
@observable editModalOpen: boolean = false;
componentDidMount() {
const { id } = this.props.match.params;
@@ -113,14 +111,6 @@ class CollectionScene extends React.Component<Props> {
this.permissionsModalOpen = false;
};
handleEditModalOpen = () => {
this.editModalOpen = true;
};
handleEditModalClose = () => {
this.editModalOpen = false;
};
renderActions() {
const { match, policies, t } = this.props;
const can = policies.abilities(match.params.id || "");
@@ -221,32 +211,16 @@ class CollectionScene extends React.Component<Props> {
</Button>
</Link>
&nbsp;&nbsp;
{collection.private && (
<Button onClick={this.onPermissions} neutral>
{t("Manage members")}
</Button>
)}
<Button onClick={this.onPermissions} neutral>
{t("Manage permissions")}
</Button>
</Empty>
<Modal
title={t("Collection members")}
title={t("Collection permissions")}
onRequestClose={this.handlePermissionsModalClose}
isOpen={this.permissionsModalOpen}
>
<CollectionMembers
collection={this.collection}
onSubmit={this.handlePermissionsModalClose}
onEdit={this.handleEditModalOpen}
/>
</Modal>
<Modal
title={t("Edit collection")}
onRequestClose={this.handleEditModalClose}
isOpen={this.editModalOpen}
>
<CollectionEdit
collection={this.collection}
onSubmit={this.handleEditModalClose}
/>
<CollectionPermissions collection={this.collection} />
</Modal>
</Centered>
) : (
@@ -254,10 +228,10 @@ class CollectionScene extends React.Component<Props> {
<Heading>
<CollectionIcon collection={collection} size={40} expanded />{" "}
{collection.name}{" "}
{collection.private && (
{!collection.permission && (
<Tooltip
tooltip={t(
"This collection is only visible to people given access"
"This collection is only visible to those given access"
)}
placement="bottom"
>

View File

@@ -28,7 +28,6 @@ class CollectionEdit extends React.Component<Props> {
@observable sharing: boolean = this.props.collection.sharing;
@observable icon: string = this.props.collection.icon;
@observable color: string = this.props.collection.color || "#4E5C6E";
@observable private: boolean = this.props.collection.private;
@observable sort: { field: string, direction: "asc" | "desc" } = this.props
.collection.sort;
@observable isSaving: boolean;
@@ -43,7 +42,6 @@ class CollectionEdit extends React.Component<Props> {
name: this.name,
icon: this.icon,
color: this.color,
private: this.private,
sharing: this.sharing,
sort: this.sort,
});
@@ -75,10 +73,6 @@ class CollectionEdit extends React.Component<Props> {
this.icon = icon;
};
handlePrivateChange = (ev: SyntheticInputEvent<*>) => {
this.private = ev.target.checked;
};
handleSharingChange = (ev: SyntheticInputEvent<*>) => {
this.sharing = ev.target.checked;
};
@@ -122,17 +116,6 @@ class CollectionEdit extends React.Component<Props> {
value={`${this.sort.field}.${this.sort.direction}`}
onChange={this.handleSortChange}
/>
<Switch
id="private"
label={t("Private collection")}
onChange={this.handlePrivateChange}
checked={this.private}
/>
<HelpText>
<Trans>
A private collection will only be visible to invited team members.
</Trans>
</HelpText>
<Switch
id="sharing"
label={t("Public document sharing")}

View File

@@ -1,268 +0,0 @@
// @flow
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import CollectionGroupMembershipsStore from "stores/CollectionGroupMembershipsStore";
import GroupsStore from "stores/GroupsStore";
import MembershipsStore from "stores/MembershipsStore";
import UiStore from "stores/UiStore";
import UsersStore from "stores/UsersStore";
import Collection from "models/Collection";
import Button from "components/Button";
import ButtonLink from "components/ButtonLink";
import Empty from "components/Empty";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import Modal from "components/Modal";
import PaginatedList from "components/PaginatedList";
import Subheading from "components/Subheading";
import AddGroupsToCollection from "./AddGroupsToCollection";
import AddPeopleToCollection from "./AddPeopleToCollection";
import CollectionGroupMemberListItem from "./components/CollectionGroupMemberListItem";
import MemberListItem from "./components/MemberListItem";
type Props = {
ui: UiStore,
auth: AuthStore,
collection: Collection,
users: UsersStore,
memberships: MembershipsStore,
collectionGroupMemberships: CollectionGroupMembershipsStore,
groups: GroupsStore,
onEdit: () => void,
};
@observer
class CollectionMembers extends React.Component<Props> {
@observable addGroupModalOpen: boolean = false;
@observable addMemberModalOpen: boolean = false;
handleAddGroupModalOpen = () => {
this.addGroupModalOpen = true;
};
handleAddGroupModalClose = () => {
this.addGroupModalOpen = false;
};
handleAddMemberModalOpen = () => {
this.addMemberModalOpen = true;
};
handleAddMemberModalClose = () => {
this.addMemberModalOpen = false;
};
handleRemoveUser = (user) => {
try {
this.props.memberships.delete({
collectionId: this.props.collection.id,
userId: user.id,
});
this.props.ui.showToast(`${user.name} was removed from the collection`, {
type: "success",
});
} catch (err) {
this.props.ui.showToast("Could not remove user", { type: "error" });
}
};
handleUpdateUser = (user, permission) => {
try {
this.props.memberships.create({
collectionId: this.props.collection.id,
userId: user.id,
permission,
});
this.props.ui.showToast(`${user.name} permissions were updated`, {
type: "success",
});
} catch (err) {
this.props.ui.showToast("Could not update user", { type: "error" });
}
};
handleRemoveGroup = (group) => {
try {
this.props.collectionGroupMemberships.delete({
collectionId: this.props.collection.id,
groupId: group.id,
});
this.props.ui.showToast(`${group.name} was removed from the collection`, {
type: "success",
});
} catch (err) {
this.props.ui.showToast("Could not remove group", { type: "error" });
}
};
handleUpdateGroup = (group, permission) => {
try {
this.props.collectionGroupMemberships.create({
collectionId: this.props.collection.id,
groupId: group.id,
permission,
});
this.props.ui.showToast(`${group.name} permissions were updated`, {
type: "success",
});
} catch (err) {
this.props.ui.showToast("Could not update user", { type: "error" });
}
};
render() {
const {
collection,
users,
groups,
memberships,
collectionGroupMemberships,
auth,
} = this.props;
const { user } = auth;
if (!user) return null;
const key = memberships.orderedData
.map((m) => m.permission)
.concat(collection.private)
.join("-");
return (
<Flex column>
{collection.private ? (
<>
<HelpText>
Choose which groups and team members have access to view and edit
documents in the private <strong>{collection.name}</strong>{" "}
collection. You can make this collection visible to the entire
team by{" "}
<ButtonLink onClick={this.props.onEdit}>
changing the visibility
</ButtonLink>
.
</HelpText>
<span>
<Button
type="button"
onClick={this.handleAddGroupModalOpen}
icon={<PlusIcon />}
neutral
>
Add groups
</Button>
</span>
</>
) : (
<HelpText>
The <strong>{collection.name}</strong> collection is accessible by
everyone on the team. If you want to limit who can view the
collection,{" "}
<ButtonLink onClick={this.props.onEdit}>make it private</ButtonLink>
.
</HelpText>
)}
{collection.private && (
<GroupsWrap>
<Subheading>Groups</Subheading>
<PaginatedList
key={key}
items={groups.inCollection(collection.id)}
fetch={collectionGroupMemberships.fetchPage}
options={collection.private ? { id: collection.id } : undefined}
empty={<Empty>This collection has no groups.</Empty>}
renderItem={(group) => (
<CollectionGroupMemberListItem
key={group.id}
group={group}
collectionGroupMembership={collectionGroupMemberships.get(
`${group.id}-${collection.id}`
)}
onRemove={() => this.handleRemoveGroup(group)}
onUpdate={(permission) =>
this.handleUpdateGroup(group, permission)
}
/>
)}
/>
<Modal
title={`Add groups to ${collection.name}`}
onRequestClose={this.handleAddGroupModalClose}
isOpen={this.addGroupModalOpen}
>
<AddGroupsToCollection
collection={collection}
onSubmit={this.handleAddGroupModalClose}
/>
</Modal>
</GroupsWrap>
)}
{collection.private ? (
<>
<span>
<Button
type="button"
onClick={this.handleAddMemberModalOpen}
icon={<PlusIcon />}
neutral
>
Add individual members
</Button>
</span>
<Subheading>Individual Members</Subheading>
</>
) : (
<Subheading>Members</Subheading>
)}
<PaginatedList
key={key}
items={
collection.private
? users.inCollection(collection.id)
: users.active
}
fetch={collection.private ? memberships.fetchPage : users.fetchPage}
options={collection.private ? { id: collection.id } : undefined}
renderItem={(item) => (
<MemberListItem
key={item.id}
user={item}
membership={memberships.get(`${item.id}-${collection.id}`)}
canEdit={collection.private && item.id !== user.id}
onRemove={() => this.handleRemoveUser(item)}
onUpdate={(permission) => this.handleUpdateUser(item, permission)}
/>
)}
/>
<Modal
title={`Add people to ${collection.name}`}
onRequestClose={this.handleAddMemberModalClose}
isOpen={this.addMemberModalOpen}
>
<AddPeopleToCollection
collection={collection}
onSubmit={this.handleAddMemberModalClose}
/>
</Modal>
</Flex>
);
}
}
const GroupsWrap = styled.div`
margin-bottom: 50px;
`;
export default inject(
"auth",
"users",
"memberships",
"collectionGroupMemberships",
"groups",
"ui"
)(CollectionMembers);

View File

@@ -1,3 +0,0 @@
// @flow
import CollectionMembers from "./CollectionMembers";
export default CollectionMembers;

View File

@@ -14,6 +14,7 @@ import Flex from "components/Flex";
import HelpText from "components/HelpText";
import IconPicker, { icons } from "components/IconPicker";
import Input from "components/Input";
import InputSelectPermission from "components/InputSelectPermission";
import Switch from "components/Switch";
type Props = {
@@ -31,7 +32,7 @@ class CollectionNew extends React.Component<Props> {
@observable icon: string = "";
@observable color: string = "#4E5C6E";
@observable sharing: boolean = true;
@observable private: boolean = false;
@observable permission: string = "read_write";
@observable isSaving: boolean;
hasOpenedIconPicker: boolean = false;
@@ -44,7 +45,7 @@ class CollectionNew extends React.Component<Props> {
sharing: this.sharing,
icon: this.icon,
color: this.color,
private: this.private,
permission: this.permission,
},
this.props.collections
);
@@ -87,8 +88,8 @@ class CollectionNew extends React.Component<Props> {
this.hasOpenedIconPicker = true;
};
handlePrivateChange = (ev: SyntheticInputEvent<HTMLInputElement>) => {
this.private = ev.target.checked;
handlePermissionChange = (ev: SyntheticInputEvent<HTMLInputElement>) => {
this.permission = ev.target.value;
};
handleSharingChange = (ev: SyntheticInputEvent<HTMLInputElement>) => {
@@ -131,15 +132,16 @@ class CollectionNew extends React.Component<Props> {
icon={this.icon}
/>
</Flex>
<Switch
id="private"
label={t("Private collection")}
onChange={this.handlePrivateChange}
checked={this.private}
<InputSelectPermission
value={this.permission}
onChange={this.handlePermissionChange}
short
/>
<HelpText>
<Trans>
A private collection will only be visible to invited team members.
This is the default level of access given to team members, you can
give specific users or groups more access once the collection is
created.
</Trans>
</HelpText>
{teamSharingEnabled && (

View File

@@ -8,14 +8,14 @@ import GroupListItem from "components/GroupListItem";
import InputSelect from "components/InputSelect";
import CollectionGroupMemberMenu from "menus/CollectionGroupMemberMenu";
type Props = {
type Props = {|
group: Group,
collectionGroupMembership: ?CollectionGroupMembership,
onUpdate: (permission: string) => void,
onRemove: () => void,
};
onUpdate: (permission: string) => any,
onRemove: () => any,
|};
const MemberListItem = ({
const CollectionGroupMemberListItem = ({
group,
collectionGroupMembership,
onUpdate,
@@ -25,8 +25,8 @@ const MemberListItem = ({
const PERMISSIONS = React.useMemo(
() => [
{ label: t("Read only"), value: "read" },
{ label: t("Read & Edit"), value: "read_write" },
{ label: t("View only"), value: "read" },
{ label: t("View and edit"), value: "read_write" },
],
[t]
);
@@ -36,6 +36,7 @@ const MemberListItem = ({
group={group}
onRemove={onRemove}
onUpdate={onUpdate}
showAvatar
renderActions={({ openMembersModal }) => (
<>
<Select
@@ -48,13 +49,11 @@ const MemberListItem = ({
}
onChange={(ev) => onUpdate(ev.target.value)}
labelHidden
/>{" "}
<CollectionGroupMemberMenu
onMembers={openMembersModal}
onRemove={onRemove}
/>
<ButtonWrap>
<CollectionGroupMemberMenu
onMembers={openMembersModal}
onRemove={onRemove}
/>
</ButtonWrap>
</>
)}
/>
@@ -64,10 +63,7 @@ const MemberListItem = ({
const Select = styled(InputSelect)`
margin: 0;
font-size: 14px;
border-color: transparent;
`;
const ButtonWrap = styled.div`
margin-left: 6px;
`;
export default MemberListItem;
export default CollectionGroupMemberListItem;

View File

@@ -17,9 +17,9 @@ type Props = {
user: User,
membership?: ?Membership,
canEdit: boolean,
onAdd?: () => void,
onRemove?: () => void,
onUpdate?: (permission: string) => void,
onAdd?: () => any,
onRemove?: () => any,
onUpdate?: (permission: string) => any,
};
const MemberListItem = ({
@@ -34,8 +34,8 @@ const MemberListItem = ({
const PERMISSIONS = React.useMemo(
() => [
{ label: t("Read only"), value: "read" },
{ label: t("Read & Edit"), value: "read_write" },
{ label: t("View only"), value: "read" },
{ label: t("View and edit"), value: "read_write" },
],
[t]
);
@@ -67,8 +67,7 @@ const MemberListItem = ({
onChange={(ev) => onUpdate(ev.target.value)}
labelHidden
/>
)}
&nbsp;&nbsp;
)}{" "}
{canEdit && onRemove && <MemberMenu onRemove={onRemove} />}
{canEdit && onAdd && (
<Button onClick={onAdd} neutral>
@@ -84,6 +83,7 @@ const MemberListItem = ({
const Select = styled(InputSelect)`
margin: 0;
font-size: 14px;
border-color: transparent;
`;
export default MemberListItem;

View File

@@ -12,7 +12,7 @@ import Time from "components/Time";
type Props = {
user: User,
canEdit: boolean,
onAdd: () => void,
onAdd: () => any,
};
const UserListItem = ({ user, onAdd, canEdit }: Props) => {

View File

@@ -0,0 +1,283 @@
// @flow
import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import styled from "styled-components";
import Collection from "models/Collection";
import Button from "components/Button";
import Divider from "components/Divider";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import InputSelectPermission from "components/InputSelectPermission";
import Labeled from "components/Labeled";
import Modal from "components/Modal";
import PaginatedList from "components/PaginatedList";
import AddGroupsToCollection from "./AddGroupsToCollection";
import AddPeopleToCollection from "./AddPeopleToCollection";
import CollectionGroupMemberListItem from "./components/CollectionGroupMemberListItem";
import MemberListItem from "./components/MemberListItem";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores";
type Props = {|
collection: Collection,
|};
function CollectionPermissions({ collection }: Props) {
const { t } = useTranslation();
const user = useCurrentUser();
const {
ui,
memberships,
collectionGroupMemberships,
users,
groups,
} = useStores();
const [addGroupModalOpen, setAddGroupModalOpen] = React.useState(false);
const [addMemberModalOpen, setAddMemberModalOpen] = React.useState(false);
const handleRemoveUser = React.useCallback(
async (user) => {
try {
await memberships.delete({
collectionId: collection.id,
userId: user.id,
});
ui.showToast(
t(`{{ userName }} was removed from the collection`, {
userName: user.name,
}),
{
type: "success",
}
);
} catch (err) {
ui.showToast(t("Could not remove user"), { type: "error" });
}
},
[memberships, ui, collection, t]
);
const handleUpdateUser = React.useCallback(
async (user, permission) => {
try {
await memberships.create({
collectionId: collection.id,
userId: user.id,
permission,
});
ui.showToast(
t(`{{ userName }} permissions were updated`, { userName: user.name }),
{
type: "success",
}
);
} catch (err) {
ui.showToast(t("Could not update user"), { type: "error" });
}
},
[memberships, ui, collection, t]
);
const handleRemoveGroup = React.useCallback(
async (group) => {
try {
await collectionGroupMemberships.delete({
collectionId: collection.id,
groupId: group.id,
});
ui.showToast(
t(`The {{ groupName }} group was removed from the collection`, {
groupName: group.name,
}),
{
type: "success",
}
);
} catch (err) {
ui.showToast(t("Could not remove group"), { type: "error" });
}
},
[collectionGroupMemberships, ui, collection, t]
);
const handleUpdateGroup = React.useCallback(
async (group, permission) => {
try {
await collectionGroupMemberships.create({
collectionId: collection.id,
groupId: group.id,
permission,
});
ui.showToast(
t(`{{ groupName }} permissions were updated`, {
groupName: group.name,
}),
{
type: "success",
}
);
} catch (err) {
ui.showToast(t("Could not update user"), { type: "error" });
}
},
[collectionGroupMemberships, ui, collection, t]
);
const handleChangePermission = React.useCallback(
async (ev) => {
try {
await collection.save({ permission: ev.target.value });
ui.showToast(t("Default access permissions were updated"), {
type: "success",
});
} catch (err) {
ui.showToast(t("Could not update permissions"), { type: "error" });
}
},
[collection, ui, t]
);
const fetchOptions = React.useMemo(() => ({ id: collection.id }), [
collection.id,
]);
const collectionName = collection.name;
const collectionGroups = groups.inCollection(collection.id);
const collectionUsers = users.inCollection(collection.id);
const isEmpty = !collectionGroups.length && !collectionUsers.length;
return (
<Flex column>
<InputSelectPermission
onChange={handleChangePermission}
value={collection.permission || ""}
short
/>
<PermissionExplainer>
{!collection.permission && (
<Trans
defaults="The <em>{{ collectionName }}</em> collection is private. Team members have no access to it by default."
values={{ collectionName }}
components={{ em: <strong /> }}
/>
)}
{collection.permission === "read" && (
<Trans
defaults="Team members can view documents in the <em>{{ collectionName }}</em> collection by default."
values={{ collectionName }}
components={{ em: <strong /> }}
/>
)}
{collection.permission === "read_write" && (
<Trans
defaults="Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by
default."
values={{ collectionName }}
components={{ em: <strong /> }}
/>
)}
</PermissionExplainer>
<Labeled label={t("Additional access")}>
<Actions>
<Button
type="button"
onClick={() => setAddGroupModalOpen(true)}
icon={<PlusIcon />}
neutral
>
{t("Add groups")}
</Button>{" "}
<Button
type="button"
onClick={() => setAddMemberModalOpen(true)}
icon={<PlusIcon />}
neutral
>
{t("Add people")}
</Button>
</Actions>
</Labeled>
<Divider />
{isEmpty && (
<Empty>
<Trans>
Add specific access for individual groups and team members
</Trans>
</Empty>
)}
<PaginatedList
items={collectionGroups}
fetch={collectionGroupMemberships.fetchPage}
options={fetchOptions}
renderItem={(group) => (
<CollectionGroupMemberListItem
key={group.id}
group={group}
collectionGroupMembership={collectionGroupMemberships.get(
`${group.id}-${collection.id}`
)}
onRemove={() => handleRemoveGroup(group)}
onUpdate={(permission) => handleUpdateGroup(group, permission)}
/>
)}
/>
{collectionGroups.length ? <Divider /> : null}
<PaginatedList
items={collectionUsers}
fetch={memberships.fetchPage}
options={fetchOptions}
renderItem={(item) => (
<MemberListItem
key={item.id}
user={item}
membership={memberships.get(`${item.id}-${collection.id}`)}
canEdit={item.id !== user.id}
onRemove={() => handleRemoveUser(item)}
onUpdate={(permission) => handleUpdateUser(item, permission)}
/>
)}
/>
<Modal
title={t(`Add groups to {{ collectionName }}`, {
collectionName: collection.name,
})}
onRequestClose={() => setAddGroupModalOpen(false)}
isOpen={addGroupModalOpen}
>
<AddGroupsToCollection
collection={collection}
onSubmit={() => setAddGroupModalOpen(false)}
/>
</Modal>
<Modal
title={t(`Add people to {{ collectionName }}`, {
collectionName: collection.name,
})}
onRequestClose={() => setAddMemberModalOpen(false)}
isOpen={addMemberModalOpen}
>
<AddPeopleToCollection
collection={collection}
onSubmit={() => setAddMemberModalOpen(false)}
/>
</Modal>
</Flex>
);
}
const Empty = styled(HelpText)`
margin-top: 8px;
`;
const PermissionExplainer = styled(HelpText)`
margin-top: -8px;
margin-bottom: 24px;
`;
const Actions = styled.div`
margin-bottom: 12px;
`;
export default observer(CollectionPermissions);

View File

@@ -74,7 +74,7 @@ class GroupMembers extends React.Component<Props> {
<HelpText>
Add and remove team members in the <strong>{group.name}</strong>{" "}
group. Adding people to the group will give them access to any
collections this group has been given access to.
collections this group has been added to.
</HelpText>
<span>
<Button

View File

@@ -47,16 +47,6 @@ export default class CollectionsStore extends BaseStore<Collection> {
});
}
@computed
get public(): Collection[] {
return this.orderedData.filter((collection) => !collection.private);
}
@computed
get private(): Collection[] {
return this.orderedData.filter((collection) => collection.private);
}
/**
* List of paths to each of the documents, where paths are composed of id and title/name pairs
*/