@@ -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
11
app/components/Divider.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
22
app/components/InputSelectPermission.js
Normal file
22
app/components/InputSelectPermission.js
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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",
|
||||
]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
{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"
|
||||
>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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);
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import CollectionMembers from "./CollectionMembers";
|
||||
export default CollectionMembers;
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
/>
|
||||
)}
|
||||
|
||||
)}{" "}
|
||||
{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;
|
||||
@@ -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) => {
|
||||
283
app/scenes/CollectionPermissions/index.js
Normal file
283
app/scenes/CollectionPermissions/index.js
Normal 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);
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user