parent
d7acf616cf
commit
7e1b07ef98
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) => {
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"sequelize:migrate": "sequelize db:migrate",
|
||||
"db:create-migration": "sequelize migration:create",
|
||||
"db:migrate": "sequelize db:migrate",
|
||||
"db:rollback": "sequelize db:migrate:undo",
|
||||
"upgrade": "git fetch && git pull && yarn install && yarn heroku-postbuild",
|
||||
"test": "yarn test:app && yarn test:server",
|
||||
"test:app": "jest",
|
||||
|
|
|
@ -112,7 +112,7 @@ describe("#attachments.delete", () => {
|
|||
it("should not allow deleting an attachment belonging to a document user does not have access to", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
teamId: collection.teamId,
|
||||
|
@ -184,7 +184,7 @@ describe("#attachments.redirect", () => {
|
|||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
teamId: user.teamId,
|
||||
|
@ -208,7 +208,7 @@ describe("#attachments.redirect", () => {
|
|||
it("should not return a redirect for a private attachment belonging to a document user does not have access to", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
teamId: collection.teamId,
|
||||
|
|
|
@ -39,13 +39,13 @@ router.post("collections.create", auth(), async (ctx) => {
|
|||
name,
|
||||
color,
|
||||
description,
|
||||
permission,
|
||||
sharing,
|
||||
icon,
|
||||
sort = Collection.DEFAULT_SORT,
|
||||
} = ctx.body;
|
||||
|
||||
let { index } = ctx.body;
|
||||
const isPrivate = ctx.body.private;
|
||||
ctx.assertPresent(name, "name is required");
|
||||
|
||||
if (color) {
|
||||
|
@ -89,7 +89,7 @@ router.post("collections.create", auth(), async (ctx) => {
|
|||
color,
|
||||
teamId: user.teamId,
|
||||
createdById: user.id,
|
||||
private: isPrivate,
|
||||
permission: permission ? permission : null,
|
||||
sharing,
|
||||
sort,
|
||||
index,
|
||||
|
@ -105,11 +105,9 @@ router.post("collections.create", auth(), async (ctx) => {
|
|||
});
|
||||
|
||||
// we must reload the collection to get memberships for policy presenter
|
||||
if (isPrivate) {
|
||||
collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collection.id);
|
||||
}
|
||||
collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collection.id);
|
||||
|
||||
ctx.body = {
|
||||
data: presentCollection(collection),
|
||||
|
@ -514,8 +512,16 @@ router.post("collections.export_all", auth(), async (ctx) => {
|
|||
});
|
||||
|
||||
router.post("collections.update", auth(), async (ctx) => {
|
||||
let { id, name, description, icon, color, sort, sharing } = ctx.body;
|
||||
const isPrivate = ctx.body.private;
|
||||
let {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
permission,
|
||||
color,
|
||||
sort,
|
||||
sharing,
|
||||
} = ctx.body;
|
||||
|
||||
if (color) {
|
||||
ctx.assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
|
||||
|
@ -528,9 +534,9 @@ router.post("collections.update", auth(), async (ctx) => {
|
|||
|
||||
authorize(user, "update", collection);
|
||||
|
||||
// we're making this collection private right now, ensure that the current
|
||||
// we're making this collection have no default access, ensure that the current
|
||||
// user has a read-write membership so that at least they can edit it
|
||||
if (isPrivate && !collection.private) {
|
||||
if (permission !== "read_write" && collection.permission === "read_write") {
|
||||
await CollectionUser.findOrCreate({
|
||||
where: {
|
||||
collectionId: collection.id,
|
||||
|
@ -543,7 +549,7 @@ router.post("collections.update", auth(), async (ctx) => {
|
|||
});
|
||||
}
|
||||
|
||||
const isPrivacyChanged = isPrivate !== collection.private;
|
||||
const permissionChanged = permission !== collection.permission;
|
||||
|
||||
if (name !== undefined) {
|
||||
collection.name = name;
|
||||
|
@ -557,8 +563,8 @@ router.post("collections.update", auth(), async (ctx) => {
|
|||
if (color !== undefined) {
|
||||
collection.color = color;
|
||||
}
|
||||
if (isPrivate !== undefined) {
|
||||
collection.private = isPrivate;
|
||||
if (permission !== undefined) {
|
||||
collection.permission = permission ? permission : null;
|
||||
}
|
||||
if (sharing !== undefined) {
|
||||
collection.sharing = sharing;
|
||||
|
@ -580,7 +586,7 @@ router.post("collections.update", auth(), async (ctx) => {
|
|||
|
||||
// must reload to update collection membership for correct policy calculation
|
||||
// if the privacy level has changed. Otherwise skip this query for speed.
|
||||
if (isPrivacyChanged) {
|
||||
if (permissionChanged) {
|
||||
await collection.reload();
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ describe("#collections.list", () => {
|
|||
it("should not return private collections actor is not a member of", async () => {
|
||||
const { user, collection } = await seed();
|
||||
await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post("/api/collections.list", {
|
||||
|
@ -58,12 +58,12 @@ describe("#collections.list", () => {
|
|||
it("should return private collections actor is a member of", async () => {
|
||||
const user = await buildUser();
|
||||
await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
@ -82,13 +82,13 @@ describe("#collections.list", () => {
|
|||
it("should return private collections actor is a group-member of", async () => {
|
||||
const user = await buildUser();
|
||||
await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
|
@ -256,7 +256,7 @@ describe("#collections.export", () => {
|
|||
it("should now allow export of private collection not a member", async () => {
|
||||
const { user } = await seed();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post("/api/collections.export", {
|
||||
|
@ -268,7 +268,7 @@ describe("#collections.export", () => {
|
|||
|
||||
it("should allow export of private collection when the actor is a member", async () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -288,7 +288,7 @@ describe("#collections.export", () => {
|
|||
it("should allow export of private collection when the actor is a group member", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
|
@ -369,7 +369,7 @@ describe("#collections.add_user", () => {
|
|||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const anotherUser = await buildUser({ teamId: user.teamId });
|
||||
const res = await server.post("/api/collections.add_user", {
|
||||
|
@ -389,7 +389,7 @@ describe("#collections.add_user", () => {
|
|||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const anotherUser = await buildUser();
|
||||
const res = await server.post("/api/collections.add_user", {
|
||||
|
@ -433,7 +433,7 @@ describe("#collections.add_group", () => {
|
|||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
const res = await server.post("/api/collections.add_group", {
|
||||
|
@ -454,7 +454,7 @@ describe("#collections.add_group", () => {
|
|||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const group = await buildGroup();
|
||||
const res = await server.post("/api/collections.add_group", {
|
||||
|
@ -496,7 +496,7 @@ describe("#collections.remove_group", () => {
|
|||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
|
||||
|
@ -528,7 +528,7 @@ describe("#collections.remove_group", () => {
|
|||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const group = await buildGroup();
|
||||
const res = await server.post("/api/collections.remove_group", {
|
||||
|
@ -572,7 +572,7 @@ describe("#collections.remove_user", () => {
|
|||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const anotherUser = await buildUser({ teamId: user.teamId });
|
||||
|
||||
|
@ -601,7 +601,7 @@ describe("#collections.remove_user", () => {
|
|||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const anotherUser = await buildUser();
|
||||
const res = await server.post("/api/collections.remove_user", {
|
||||
|
@ -642,7 +642,7 @@ describe("#collections.remove_user", () => {
|
|||
describe("#collections.users", () => {
|
||||
it("should return users in private collection", async () => {
|
||||
const { collection, user } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -684,7 +684,7 @@ describe("#collections.group_memberships", () => {
|
|||
const user = await buildUser();
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
|
@ -721,7 +721,7 @@ describe("#collections.group_memberships", () => {
|
|||
const group = await buildGroup({ name: "will find", teamId: user.teamId });
|
||||
const group2 = await buildGroup({ name: "wont find", teamId: user.teamId });
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
|
@ -766,7 +766,7 @@ describe("#collections.group_memberships", () => {
|
|||
const group = await buildGroup({ teamId: user.teamId });
|
||||
const group2 = await buildGroup({ teamId: user.teamId });
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
|
@ -817,7 +817,7 @@ describe("#collections.group_memberships", () => {
|
|||
it("should require authorization", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
|
@ -831,7 +831,7 @@ describe("#collections.group_memberships", () => {
|
|||
describe("#collections.memberships", () => {
|
||||
it("should return members in private collection", async () => {
|
||||
const { collection, user } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -945,7 +945,7 @@ describe("#collections.info", () => {
|
|||
|
||||
it("should require user member of collection", async () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/collections.info", {
|
||||
|
@ -956,7 +956,7 @@ describe("#collections.info", () => {
|
|||
|
||||
it("should allow user member of collection", async () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -1034,12 +1034,12 @@ describe("#collections.create", () => {
|
|||
it("should return correct policies with private collection", async () => {
|
||||
const { user } = await seed();
|
||||
const res = await server.post("/api/collections.create", {
|
||||
body: { token: user.getJwtToken(), name: "Test", private: true },
|
||||
body: { token: user.getJwtToken(), name: "Test", permission: null },
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.private).toBeTruthy();
|
||||
expect(body.data.permission).toEqual(null);
|
||||
expect(body.policies.length).toBe(1);
|
||||
expect(body.policies[0].abilities.read).toBeTruthy();
|
||||
expect(body.policies[0].abilities.export).toBeTruthy();
|
||||
|
@ -1176,11 +1176,11 @@ describe("#collections.update", () => {
|
|||
it("allows editing individual fields", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: { token: user.getJwtToken(), id: collection.id, private: true },
|
||||
body: { token: user.getJwtToken(), id: collection.id, permission: null },
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.private).toBe(true);
|
||||
expect(body.data.permission).toBe(null);
|
||||
expect(body.data.name).toBe(collection.name);
|
||||
});
|
||||
|
||||
|
@ -1190,14 +1190,14 @@ describe("#collections.update", () => {
|
|||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
private: true,
|
||||
permission: null,
|
||||
name: "Test",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.name).toBe("Test");
|
||||
expect(body.data.private).toBe(true);
|
||||
expect(body.data.permission).toBe(null);
|
||||
|
||||
// ensure we return with a write level policy
|
||||
expect(body.policies.length).toBe(1);
|
||||
|
@ -1206,7 +1206,7 @@ describe("#collections.update", () => {
|
|||
|
||||
it("allows editing from private to non-private collection", async () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -1220,14 +1220,14 @@ describe("#collections.update", () => {
|
|||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
private: false,
|
||||
permission: "read_write",
|
||||
name: "Test",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.name).toBe("Test");
|
||||
expect(body.data.private).toBe(false);
|
||||
expect(body.data.permission).toBe("read_write");
|
||||
|
||||
// ensure we return with a write level policy
|
||||
expect(body.policies.length).toBe(1);
|
||||
|
@ -1236,7 +1236,7 @@ describe("#collections.update", () => {
|
|||
|
||||
it("allows editing by read-write collection user", async () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -1258,7 +1258,7 @@ describe("#collections.update", () => {
|
|||
it("allows editing by read-write collection group user", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
|
@ -1280,7 +1280,7 @@ describe("#collections.update", () => {
|
|||
|
||||
it("does not allow editing by read-only collection user", async () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -1393,7 +1393,7 @@ describe("#collections.delete", () => {
|
|||
it("allows deleting by read-write collection group user", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
await buildCollection({
|
||||
|
|
|
@ -50,7 +50,7 @@ describe("#documents.info", () => {
|
|||
it("should not return published document in collection not a member of", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const document = await buildDocument({ collectionId: collection.id });
|
||||
|
@ -209,7 +209,7 @@ describe("#documents.info", () => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/documents.info", {
|
||||
|
@ -282,7 +282,7 @@ describe("#documents.export", () => {
|
|||
it("should not return published document in collection not a member of", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const document = await buildDocument({ collectionId: collection.id });
|
||||
|
@ -400,7 +400,7 @@ describe("#documents.export", () => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/documents.export", {
|
||||
|
@ -501,7 +501,7 @@ describe("#documents.list", () => {
|
|||
|
||||
it("should not return documents in private collections not a member of", async () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/documents.list", {
|
||||
|
@ -573,7 +573,7 @@ describe("#documents.list", () => {
|
|||
|
||||
it("should allow filtering to private collection", async () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -647,7 +647,7 @@ describe("#documents.pinned", () => {
|
|||
|
||||
it("should return pinned documents in private collections member of", async () => {
|
||||
const { user, collection, document } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
document.pinnedById = user.id;
|
||||
|
@ -672,7 +672,7 @@ describe("#documents.pinned", () => {
|
|||
|
||||
it("should not return pinned documents in private collections not a member of", async () => {
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
|
||||
const user = await buildUser({ teamId: collection.teamId });
|
||||
|
@ -710,7 +710,7 @@ describe("#documents.drafts", () => {
|
|||
document.publishedAt = null;
|
||||
await document.save();
|
||||
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/documents.drafts", {
|
||||
|
@ -996,7 +996,7 @@ describe("#documents.search", () => {
|
|||
|
||||
it("should return documents for a specific private collection", async () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -1061,7 +1061,7 @@ describe("#documents.search", () => {
|
|||
|
||||
it("should not return documents in private collections not a member of", async () => {
|
||||
const { user } = await seed();
|
||||
const collection = await buildCollection({ private: true });
|
||||
const collection = await buildCollection({ permission: null });
|
||||
|
||||
await buildDocument({
|
||||
title: "search term",
|
||||
|
@ -1158,7 +1158,7 @@ describe("#documents.archived", () => {
|
|||
|
||||
it("should not return documents in private collections not a member of", async () => {
|
||||
const { user } = await seed();
|
||||
const collection = await buildCollection({ private: true });
|
||||
const collection = await buildCollection({ permission: null });
|
||||
|
||||
const document = await buildDocument({
|
||||
teamId: user.teamId,
|
||||
|
@ -1224,7 +1224,7 @@ describe("#documents.viewed", () => {
|
|||
it("should not return recently viewed documents in collection not a member of", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
await View.increment({ documentId: document.id, userId: user.id });
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/documents.viewed", {
|
||||
|
@ -1808,7 +1808,7 @@ describe("#documents.update", () => {
|
|||
document.publishedAt = null;
|
||||
await document.save();
|
||||
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -1903,7 +1903,7 @@ describe("#documents.update", () => {
|
|||
|
||||
it("allows editing by read-write collection user", async () => {
|
||||
const { admin, document, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -1931,7 +1931,7 @@ describe("#documents.update", () => {
|
|||
|
||||
it("does not allow editing by read-only collection user", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -1953,6 +1953,23 @@ describe("#documents.update", () => {
|
|||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it("does not allow editing in read-only collection", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.permission = "read";
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/documents.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: document.id,
|
||||
text: "Changed text",
|
||||
lastRevision: document.revision,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it("should append document with text", async () => {
|
||||
const { user, document } = await seed();
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ describe("#revisions.list", () => {
|
|||
const { user, document, collection } = await seed();
|
||||
await Revision.createFromDocument(document);
|
||||
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/revisions.list", {
|
||||
|
|
|
@ -115,7 +115,7 @@ describe("#shares.list", () => {
|
|||
userId: admin.id,
|
||||
});
|
||||
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/shares.list", {
|
||||
|
@ -151,7 +151,7 @@ describe("#shares.create", () => {
|
|||
|
||||
it("should not allow creating a share record with read-only permissions", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
|
||||
await collection.save();
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ describe("#views.list", () => {
|
|||
|
||||
it("should return views for a document in read-only collection", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
@ -84,7 +84,7 @@ describe("#views.create", () => {
|
|||
|
||||
it("should allow creating a view record for document in read-only collection", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.private = true;
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
|
|
|
@ -58,7 +58,7 @@ export default async function collectionImporter({
|
|||
},
|
||||
defaults: {
|
||||
createdById: user.id,
|
||||
private: false,
|
||||
permission: "read_write",
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -71,7 +71,7 @@ export default async function collectionImporter({
|
|||
teamId: user.teamId,
|
||||
createdById: user.id,
|
||||
name,
|
||||
private: false,
|
||||
permission: "read_write",
|
||||
});
|
||||
await Event.create({
|
||||
name: "collections.create",
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn("collections", "permission", {
|
||||
type: Sequelize.STRING,
|
||||
defaultValue: null,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
isIn: [["read", "read_write"]],
|
||||
},
|
||||
});
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE collections
|
||||
SET "permission" = 'read_write'
|
||||
WHERE "private" = false
|
||||
`);
|
||||
|
||||
await queryInterface.removeColumn("collections", "private");
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn("collections", "private", {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE collections
|
||||
SET "private" = true
|
||||
WHERE "permission" IS NULL
|
||||
`);
|
||||
|
||||
await queryInterface.removeColumn("collections", "permission");
|
||||
}
|
||||
};
|
|
@ -25,7 +25,14 @@ const Collection = sequelize.define(
|
|||
type: DataTypes.STRING,
|
||||
defaultValue: null,
|
||||
},
|
||||
private: DataTypes.BOOLEAN,
|
||||
permission: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: null,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
isIn: [["read", "read_write"]],
|
||||
},
|
||||
},
|
||||
maintainerApprovalRequired: DataTypes.BOOLEAN,
|
||||
documentStructure: DataTypes.JSONB,
|
||||
sharing: {
|
||||
|
@ -199,7 +206,7 @@ Collection.addHook("afterDestroy", async (model: Collection) => {
|
|||
});
|
||||
|
||||
Collection.addHook("afterCreate", (model: Collection, options) => {
|
||||
if (model.private) {
|
||||
if (model.permission !== "read_write") {
|
||||
return CollectionUser.findOrCreate({
|
||||
where: {
|
||||
collectionId: model.id,
|
||||
|
|
|
@ -254,7 +254,7 @@ describe("#membershipUserIds", () => {
|
|||
|
||||
const collection = await buildCollection({
|
||||
userId: users[0].id,
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId,
|
||||
});
|
||||
|
||||
|
|
|
@ -178,7 +178,7 @@ describe("#searchForTeam", () => {
|
|||
test("should not return search results from private collections", async () => {
|
||||
const team = await buildTeam();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId: team.id,
|
||||
});
|
||||
await buildDocument({
|
||||
|
|
|
@ -15,11 +15,11 @@ describe("afterDestroy hook", () => {
|
|||
const user2 = await buildUser({ teamId });
|
||||
|
||||
const collection1 = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId,
|
||||
});
|
||||
const collection2 = await buildCollection({
|
||||
private: true,
|
||||
permission: null,
|
||||
teamId,
|
||||
});
|
||||
|
||||
|
|
|
@ -225,10 +225,16 @@ Team.prototype.activateUser = async function (user: User, admin: User) {
|
|||
|
||||
Team.prototype.collectionIds = async function (paranoid: boolean = true) {
|
||||
let models = await Collection.findAll({
|
||||
attributes: ["id", "private"],
|
||||
where: { teamId: this.id, private: false },
|
||||
attributes: ["id"],
|
||||
where: {
|
||||
teamId: this.id,
|
||||
permission: {
|
||||
[Op.ne]: null,
|
||||
},
|
||||
},
|
||||
paranoid,
|
||||
});
|
||||
|
||||
return models.map((c) => c.id);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,38 +1,56 @@
|
|||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { buildTeam } from "../test/factories";
|
||||
// @flow
|
||||
import { buildTeam, buildCollection } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
it("should set subdomain if available", async () => {
|
||||
const team = await buildTeam();
|
||||
const subdomain = await team.provisionSubdomain("testy");
|
||||
expect(subdomain).toEqual("testy");
|
||||
expect(team.subdomain).toEqual("testy");
|
||||
describe("collectionIds", () => {
|
||||
it("should return non-private collection ids", async () => {
|
||||
const team = await buildTeam();
|
||||
const collection = await buildCollection({ teamId: team.id });
|
||||
|
||||
// build a collection in another team
|
||||
await buildCollection();
|
||||
|
||||
// build a private collection
|
||||
await buildCollection({ teamId: team.id, permission: null });
|
||||
const response = await team.collectionIds();
|
||||
expect(response.length).toEqual(1);
|
||||
expect(response[0]).toEqual(collection.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set subdomain append if unavailable", async () => {
|
||||
await buildTeam({ subdomain: "myteam" });
|
||||
describe("provisionSubdomain", () => {
|
||||
it("should set subdomain if available", async () => {
|
||||
const team = await buildTeam();
|
||||
const subdomain = await team.provisionSubdomain("testy");
|
||||
expect(subdomain).toEqual("testy");
|
||||
expect(team.subdomain).toEqual("testy");
|
||||
});
|
||||
|
||||
const team = await buildTeam();
|
||||
const subdomain = await team.provisionSubdomain("myteam");
|
||||
expect(subdomain).toEqual("myteam1");
|
||||
expect(team.subdomain).toEqual("myteam1");
|
||||
});
|
||||
|
||||
it("should increment subdomain append if unavailable", async () => {
|
||||
await buildTeam({ subdomain: "myteam" });
|
||||
await buildTeam({ subdomain: "myteam1" });
|
||||
|
||||
const team = await buildTeam();
|
||||
const subdomain = await team.provisionSubdomain("myteam");
|
||||
expect(subdomain).toEqual("myteam2");
|
||||
expect(team.subdomain).toEqual("myteam2");
|
||||
});
|
||||
|
||||
it("should do nothing if subdomain already set", async () => {
|
||||
const team = await buildTeam({ subdomain: "example" });
|
||||
const subdomain = await team.provisionSubdomain("myteam");
|
||||
expect(subdomain).toEqual("example");
|
||||
expect(team.subdomain).toEqual("example");
|
||||
it("should set subdomain append if unavailable", async () => {
|
||||
await buildTeam({ subdomain: "myteam" });
|
||||
|
||||
const team = await buildTeam();
|
||||
const subdomain = await team.provisionSubdomain("myteam");
|
||||
expect(subdomain).toEqual("myteam1");
|
||||
expect(team.subdomain).toEqual("myteam1");
|
||||
});
|
||||
|
||||
it("should increment subdomain append if unavailable", async () => {
|
||||
await buildTeam({ subdomain: "myteam" });
|
||||
await buildTeam({ subdomain: "myteam1" });
|
||||
|
||||
const team = await buildTeam();
|
||||
const subdomain = await team.provisionSubdomain("myteam");
|
||||
expect(subdomain).toEqual("myteam2");
|
||||
expect(team.subdomain).toEqual("myteam2");
|
||||
});
|
||||
|
||||
it("should do nothing if subdomain already set", async () => {
|
||||
const team = await buildTeam({ subdomain: "example" });
|
||||
const subdomain = await team.provisionSubdomain("myteam");
|
||||
expect(subdomain).toEqual("example");
|
||||
expect(team.subdomain).toEqual("example");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -91,7 +91,7 @@ User.prototype.collectionIds = async function (options = {}) {
|
|||
const collectionStubs = await Collection.scope({
|
||||
method: ["withMembership", this.id],
|
||||
}).findAll({
|
||||
attributes: ["id", "private"],
|
||||
attributes: ["id", "permission"],
|
||||
where: { teamId: this.teamId },
|
||||
paranoid: true,
|
||||
...options,
|
||||
|
@ -100,7 +100,8 @@ User.prototype.collectionIds = async function (options = {}) {
|
|||
return collectionStubs
|
||||
.filter(
|
||||
(c) =>
|
||||
!c.private ||
|
||||
c.permission === "read" ||
|
||||
c.permission === "read_write" ||
|
||||
c.memberships.length > 0 ||
|
||||
c.collectionGroupMemberships.length > 0
|
||||
)
|
||||
|
|
|
@ -1,10 +1,75 @@
|
|||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { buildUser } from "../test/factories";
|
||||
// @flow
|
||||
import { CollectionUser } from "../models";
|
||||
import { buildUser, buildTeam, buildCollection } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
it("should set JWT secret", async () => {
|
||||
const user = await buildUser();
|
||||
expect(user.getJwtToken()).toBeTruthy();
|
||||
describe("user model", () => {
|
||||
describe("getJwtToken", () => {
|
||||
it("should set JWT secret", async () => {
|
||||
const user = await buildUser();
|
||||
expect(user.getJwtToken()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectionIds", () => {
|
||||
it("should return read_write collections", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
|
||||
const response = await user.collectionIds();
|
||||
expect(response.length).toEqual(1);
|
||||
expect(response[0]).toEqual(collection.id);
|
||||
});
|
||||
|
||||
it("should return read collections", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read",
|
||||
});
|
||||
|
||||
const response = await user.collectionIds();
|
||||
expect(response.length).toEqual(1);
|
||||
expect(response[0]).toEqual(collection.id);
|
||||
});
|
||||
|
||||
it("should not return private collections", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: null,
|
||||
});
|
||||
|
||||
const response = await user.collectionIds();
|
||||
expect(response.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should not return private collection with membership", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: null,
|
||||
});
|
||||
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
});
|
||||
|
||||
const response = await user.collectionIds();
|
||||
expect(response.length).toEqual(1);
|
||||
expect(response[0]).toEqual(collection.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ allow(User, "move", Collection, (user, collection) => {
|
|||
allow(User, ["read", "export"], Collection, (user, collection) => {
|
||||
if (!collection || user.teamId !== collection.teamId) return false;
|
||||
|
||||
if (collection.private) {
|
||||
if (!collection.permission) {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"membership should be preloaded, did you forget withMembership scope?"
|
||||
|
@ -51,7 +51,7 @@ allow(User, "share", Collection, (user, collection) => {
|
|||
if (!collection || user.teamId !== collection.teamId) return false;
|
||||
if (!collection.sharing) return false;
|
||||
|
||||
if (collection.private) {
|
||||
if (collection.permission !== "read_write") {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"membership should be preloaded, did you forget withMembership scope?"
|
||||
|
@ -73,7 +73,7 @@ allow(User, "share", Collection, (user, collection) => {
|
|||
allow(User, ["publish", "update"], Collection, (user, collection) => {
|
||||
if (!collection || user.teamId !== collection.teamId) return false;
|
||||
|
||||
if (collection.private) {
|
||||
if (collection.permission !== "read_write") {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"membership should be preloaded, did you forget withMembership scope?"
|
||||
|
@ -95,7 +95,7 @@ allow(User, ["publish", "update"], Collection, (user, collection) => {
|
|||
allow(User, "delete", Collection, (user, collection) => {
|
||||
if (!collection || user.teamId !== collection.teamId) return false;
|
||||
|
||||
if (collection.private) {
|
||||
if (collection.permission !== "read_write") {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"membership should be preloaded, did you forget withMembership scope?"
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
// @flow
|
||||
import { CollectionUser, Collection } from "../models";
|
||||
import { buildUser, buildTeam, buildCollection } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { serialize } from "./index";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("read_write permission", () => {
|
||||
it("should allow read write permissions for team member", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.export).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
});
|
||||
|
||||
it("should override read membership permission", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
let collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
});
|
||||
|
||||
// reload to get membership
|
||||
collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collection.id);
|
||||
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.export).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("read permission", () => {
|
||||
it("should allow read permissions for team member", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read",
|
||||
});
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.export).toEqual(true);
|
||||
expect(abilities.update).toEqual(false);
|
||||
expect(abilities.share).toEqual(false);
|
||||
});
|
||||
|
||||
it("should allow override with read_write membership permission", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
let collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read",
|
||||
});
|
||||
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
|
||||
// reload to get membership
|
||||
collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collection.id);
|
||||
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.export).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no permission", () => {
|
||||
it("should allow no permissions for team member", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: null,
|
||||
});
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(false);
|
||||
expect(abilities.export).toEqual(false);
|
||||
expect(abilities.update).toEqual(false);
|
||||
expect(abilities.share).toEqual(false);
|
||||
});
|
||||
|
||||
it("should allow override with team member membership permission", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
let collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: null,
|
||||
});
|
||||
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
|
||||
// reload to get membership
|
||||
collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collection.id);
|
||||
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.export).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
// @flow
|
||||
import {
|
||||
buildUser,
|
||||
buildTeam,
|
||||
buildDocument,
|
||||
buildCollection,
|
||||
} from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { serialize } from "./index";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("read_write collection", () => {
|
||||
it("should allow read write permissions for team member", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
const document = await buildDocument({
|
||||
teamId: team.id,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
const abilities = serialize(user, document);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.download).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.createChildDocument).toEqual(true);
|
||||
expect(abilities.archive).toEqual(true);
|
||||
expect(abilities.delete).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
expect(abilities.move).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("read collection", () => {
|
||||
it("should allow read only permissions permissions for team member", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read",
|
||||
});
|
||||
const document = await buildDocument({
|
||||
teamId: team.id,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
const abilities = serialize(user, document);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.download).toEqual(true);
|
||||
expect(abilities.update).toEqual(false);
|
||||
expect(abilities.createChildDocument).toEqual(false);
|
||||
expect(abilities.archive).toEqual(false);
|
||||
expect(abilities.delete).toEqual(false);
|
||||
expect(abilities.share).toEqual(false);
|
||||
expect(abilities.move).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("private collection", () => {
|
||||
it("should allow no permissions for team member", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: null,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
teamId: team.id,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
const abilities = serialize(user, document);
|
||||
expect(abilities.read).toEqual(false);
|
||||
expect(abilities.download).toEqual(false);
|
||||
expect(abilities.update).toEqual(false);
|
||||
expect(abilities.createChildDocument).toEqual(false);
|
||||
expect(abilities.archive).toEqual(false);
|
||||
expect(abilities.delete).toEqual(false);
|
||||
expect(abilities.share).toEqual(false);
|
||||
expect(abilities.move).toEqual(false);
|
||||
});
|
||||
});
|
|
@ -30,7 +30,7 @@ export default function present(collection: Collection) {
|
|||
icon: collection.icon,
|
||||
index: collection.index,
|
||||
color: collection.color || "#4E5C6E",
|
||||
private: collection.private,
|
||||
permission: collection.permission,
|
||||
sharing: collection.sharing,
|
||||
createdAt: collection.createdAt,
|
||||
updatedAt: collection.updatedAt,
|
||||
|
|
|
@ -122,7 +122,7 @@ export default class Notifications {
|
|||
],
|
||||
});
|
||||
if (!collection) return;
|
||||
if (collection.private) return;
|
||||
if (!collection.permission) return;
|
||||
|
||||
const notificationSettings = await NotificationSetting.findAll({
|
||||
where: {
|
||||
|
|
|
@ -64,7 +64,7 @@ describe("documents.publish", () => {
|
|||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
private: true,
|
||||
permission: null,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
teamId: user.teamId,
|
||||
|
|
|
@ -157,9 +157,9 @@ export default class Websockets {
|
|||
|
||||
socketio
|
||||
.to(
|
||||
collection.private
|
||||
? `collection-${collection.id}`
|
||||
: `team-${collection.teamId}`
|
||||
collection.permission
|
||||
? `team-${collection.teamId}`
|
||||
: `collection-${collection.id}`
|
||||
)
|
||||
.emit("entities", {
|
||||
event: event.name,
|
||||
|
@ -173,9 +173,9 @@ export default class Websockets {
|
|||
|
||||
return socketio
|
||||
.to(
|
||||
collection.private
|
||||
? `collection-${collection.id}`
|
||||
: `team-${collection.teamId}`
|
||||
collection.permission
|
||||
? `team-${collection.teamId}`
|
||||
: `collection-${collection.id}`
|
||||
)
|
||||
.emit("join", {
|
||||
event: event.name,
|
||||
|
|
|
@ -159,6 +159,7 @@ export async function buildCollection(overrides: Object = {}) {
|
|||
name: `Test Collection ${count}`,
|
||||
description: "Test collection description",
|
||||
createdById: overrides.userId,
|
||||
permission: "read_write",
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -78,6 +78,7 @@ export const seed = async () => {
|
|||
urlId: "collection",
|
||||
teamId: team.id,
|
||||
createdById: user.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
|
||||
const document = await Document.create({
|
||||
|
|
|
@ -86,6 +86,10 @@
|
|||
"Choose icon": "Choose icon",
|
||||
"Loading": "Loading",
|
||||
"Search": "Search",
|
||||
"Default access": "Default access",
|
||||
"View and edit": "View and edit",
|
||||
"View only": "View only",
|
||||
"No access": "No access",
|
||||
"Outline is available in your language {{optionLabel}}, would you like to change?": "Outline is available in your language {{optionLabel}}, would you like to change?",
|
||||
"Change Language": "Change Language",
|
||||
"Dismiss": "Dismiss",
|
||||
|
@ -134,8 +138,9 @@
|
|||
"New document": "New document",
|
||||
"Import document": "Import document",
|
||||
"Edit": "Edit",
|
||||
"Permissions": "Permissions",
|
||||
"Delete": "Delete",
|
||||
"Collection members": "Collection members",
|
||||
"Collection permissions": "Collection permissions",
|
||||
"Edit collection": "Edit collection",
|
||||
"Delete collection": "Delete collection",
|
||||
"Export collection": "Export collection",
|
||||
|
@ -199,8 +204,8 @@
|
|||
"<em>{{ collectionName }}</em> doesn’t contain any\n documents yet.": "<em>{{ collectionName }}</em> doesn’t contain any\n documents yet.",
|
||||
"Get started by creating a new one!": "Get started by creating a new one!",
|
||||
"Create a document": "Create a document",
|
||||
"Manage members": "Manage members",
|
||||
"This collection is only visible to people given access": "This collection is only visible to people given access",
|
||||
"Manage permissions": "Manage permissions",
|
||||
"This collection is only visible to those given access": "This collection is only visible to those given access",
|
||||
"Private": "Private",
|
||||
"Pinned": "Pinned",
|
||||
"Recently updated": "Recently updated",
|
||||
|
@ -211,13 +216,15 @@
|
|||
"You can edit the name and other details at any time, however doing so often might confuse your team mates.": "You can edit the name and other details at any time, however doing so often might confuse your team mates.",
|
||||
"Name": "Name",
|
||||
"Alphabetical": "Alphabetical",
|
||||
"Private collection": "Private collection",
|
||||
"A private collection will only be visible to invited team members.": "A private collection will only be visible to invited team members.",
|
||||
"Public document sharing": "Public document sharing",
|
||||
"When enabled, documents can be shared publicly on the internet.": "When enabled, documents can be shared publicly on the internet.",
|
||||
"Public sharing is currently disabled in the team security settings.": "Public sharing is currently disabled in the team security settings.",
|
||||
"Saving": "Saving",
|
||||
"Save": "Save",
|
||||
"Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.": "Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.",
|
||||
"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.": "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.",
|
||||
"Creating": "Creating",
|
||||
"Create": "Create",
|
||||
"{{ groupName }} was added to the collection": "{{ groupName }} was added to the collection",
|
||||
"Could not add user": "Could not add user",
|
||||
"Can’t find the group you’re looking for?": "Can’t find the group you’re looking for?",
|
||||
|
@ -234,16 +241,28 @@
|
|||
"Search people": "Search people",
|
||||
"No people matching your search": "No people matching your search",
|
||||
"No people left to add": "No people left to add",
|
||||
"Read only": "Read only",
|
||||
"Read & Edit": "Read & Edit",
|
||||
"Permissions": "Permissions",
|
||||
"Active <1></1> ago": "Active <1></1> ago",
|
||||
"Never signed in": "Never signed in",
|
||||
"Invited": "Invited",
|
||||
"Admin": "Admin",
|
||||
"Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.": "Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.",
|
||||
"Creating": "Creating",
|
||||
"Create": "Create",
|
||||
"{{ userName }} was removed from the collection": "{{ userName }} was removed from the collection",
|
||||
"Could not remove user": "Could not remove user",
|
||||
"{{ userName }} permissions were updated": "{{ userName }} permissions were updated",
|
||||
"Could not update user": "Could not update user",
|
||||
"The {{ groupName }} group was removed from the collection": "The {{ groupName }} group was removed from the collection",
|
||||
"Could not remove group": "Could not remove group",
|
||||
"{{ groupName }} permissions were updated": "{{ groupName }} permissions were updated",
|
||||
"Default access permissions were updated": "Default access permissions were updated",
|
||||
"Could not update permissions": "Could not update permissions",
|
||||
"The <em>{{ collectionName }}</em> collection is private. Team members have no access to it by default.": "The <em>{{ collectionName }}</em> collection is private. Team members have no access to it by default.",
|
||||
"Team members can view documents in the <em>{{ collectionName }}</em> collection by default.": "Team members can view documents in the <em>{{ collectionName }}</em> collection by default.",
|
||||
"Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by\n default.": "Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by\n default.",
|
||||
"Additional access": "Additional access",
|
||||
"Add groups": "Add groups",
|
||||
"Add people": "Add people",
|
||||
"Add specific access for individual groups and team members": "Add specific access for individual groups and team members",
|
||||
"Add groups to {{ collectionName }}": "Add groups to {{ collectionName }}",
|
||||
"Add people to {{ collectionName }}": "Add people to {{ collectionName }}",
|
||||
"Hide contents": "Hide contents",
|
||||
"Show contents": "Show contents",
|
||||
"Edit {{noun}}": "Edit {{noun}}",
|
||||
|
@ -274,8 +293,6 @@
|
|||
"Add team members below to give them access to the group. Need to add someone who’s not yet on the team yet?": "Add team members below to give them access to the group. Need to add someone who’s not yet on the team yet?",
|
||||
"Invite them to {{teamName}}": "Invite them to {{teamName}}",
|
||||
"{{userName}} was removed from the group": "{{userName}} was removed from the group",
|
||||
"Could not remove user": "Could not remove user",
|
||||
"Add people": "Add people",
|
||||
"This group has no members.": "This group has no members.",
|
||||
"Recently viewed": "Recently viewed",
|
||||
"Created by me": "Created by me",
|
||||
|
|
Reference in New Issue