@@ -4,7 +4,6 @@ import {
|
|||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
GoToIcon,
|
GoToIcon,
|
||||||
PadlockIcon,
|
|
||||||
ShapesIcon,
|
ShapesIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "outline-icons";
|
} from "outline-icons";
|
||||||
@@ -103,11 +102,6 @@ const Breadcrumb = ({ document, children, onlyText }: Props) => {
|
|||||||
if (onlyText === true) {
|
if (onlyText === true) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{collection.private && (
|
|
||||||
<>
|
|
||||||
<SmallPadlockIcon color="currentColor" size={16} />{" "}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{collection.name}
|
{collection.name}
|
||||||
{path.map((n) => (
|
{path.map((n) => (
|
||||||
<React.Fragment key={n.id}>
|
<React.Fragment key={n.id}>
|
||||||
@@ -154,11 +148,6 @@ export const Slash = styled(GoToIcon)`
|
|||||||
fill: ${(props) => props.theme.divider};
|
fill: ${(props) => props.theme.divider};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SmallPadlockIcon = styled(PadlockIcon)`
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: sub;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SmallSlash = styled(GoToIcon)`
|
const SmallSlash = styled(GoToIcon)`
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 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
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer, inject } from "mobx-react";
|
import { observer, inject } from "mobx-react";
|
||||||
|
import { GroupIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { MAX_AVATAR_DISPLAY } from "shared/constants";
|
import { MAX_AVATAR_DISPLAY } from "shared/constants";
|
||||||
@@ -17,7 +18,8 @@ type Props = {
|
|||||||
group: Group,
|
group: Group,
|
||||||
groupMemberships: GroupMembershipsStore,
|
groupMemberships: GroupMembershipsStore,
|
||||||
membership?: CollectionGroupMembership,
|
membership?: CollectionGroupMembership,
|
||||||
showFacepile: boolean,
|
showFacepile?: boolean,
|
||||||
|
showAvatar?: boolean,
|
||||||
renderActions: ({ openMembersModal: () => void }) => React.Node,
|
renderActions: ({ openMembersModal: () => void }) => React.Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,6 +50,11 @@ class GroupListItem extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem
|
||||||
|
image={
|
||||||
|
<Image>
|
||||||
|
<GroupIcon size={28} />
|
||||||
|
</Image>
|
||||||
|
}
|
||||||
title={
|
title={
|
||||||
<Title onClick={this.handleMembersModalOpen}>{group.name}</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`
|
const Title = styled.span`
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const Wrapper = styled.label`
|
|||||||
max-width: ${(props) => (props.short ? "350px" : "100%")};
|
max-width: ${(props) => (props.short ? "350px" : "100%")};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type Option = { label: string, value: string };
|
export type Option = { label: string, value: string };
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
value?: string,
|
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)`
|
export const Label = styled(Flex)`
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
padding-bottom: 4px;
|
||||||
color: ${(props) => props.theme.textTertiary};
|
display: inline-block;
|
||||||
letter-spacing: 0.04em;
|
color: ${(props) => props.theme.text};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default observer(Labeled);
|
export default observer(Labeled);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const ListItem = ({ image, title, subtitle, actions }: Props) => {
|
|||||||
|
|
||||||
const Wrapper = styled.li`
|
const Wrapper = styled.li`
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: ${(props) => (props.compact ? "8px" : "12px")} 0;
|
padding: 8px 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-bottom: 1px solid ${(props) => props.theme.divider};
|
border-bottom: 1px solid ${(props) => props.theme.divider};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Collection from "models/Collection";
|
|||||||
import CollectionDelete from "scenes/CollectionDelete";
|
import CollectionDelete from "scenes/CollectionDelete";
|
||||||
import CollectionEdit from "scenes/CollectionEdit";
|
import CollectionEdit from "scenes/CollectionEdit";
|
||||||
import CollectionExport from "scenes/CollectionExport";
|
import CollectionExport from "scenes/CollectionExport";
|
||||||
import CollectionMembers from "scenes/CollectionMembers";
|
import CollectionPermissions from "scenes/CollectionPermissions";
|
||||||
import ContextMenu from "components/ContextMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||||
import Template from "components/ContextMenu/Template";
|
import Template from "components/ContextMenu/Template";
|
||||||
@@ -42,9 +42,10 @@ function CollectionMenu({
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const file = React.useRef<?HTMLInputElement>();
|
const file = React.useRef<?HTMLInputElement>();
|
||||||
const [showCollectionMembers, setShowCollectionMembers] = React.useState(
|
const [
|
||||||
false
|
showCollectionPermissions,
|
||||||
);
|
setShowCollectionPermissions,
|
||||||
|
] = React.useState(false);
|
||||||
const [showCollectionEdit, setShowCollectionEdit] = React.useState(false);
|
const [showCollectionEdit, setShowCollectionEdit] = React.useState(false);
|
||||||
const [showCollectionDelete, setShowCollectionDelete] = React.useState(false);
|
const [showCollectionDelete, setShowCollectionDelete] = React.useState(false);
|
||||||
const [showCollectionExport, setShowCollectionExport] = React.useState(false);
|
const [showCollectionExport, setShowCollectionExport] = React.useState(false);
|
||||||
@@ -155,9 +156,9 @@ function CollectionMenu({
|
|||||||
onClick: () => setShowCollectionEdit(true),
|
onClick: () => setShowCollectionEdit(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Members")}…`,
|
title: `${t("Permissions")}…`,
|
||||||
visible: can.update,
|
visible: can.update,
|
||||||
onClick: () => setShowCollectionMembers(true),
|
onClick: () => setShowCollectionPermissions(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: `${t("Export")}…`,
|
title: `${t("Export")}…`,
|
||||||
@@ -178,15 +179,11 @@ function CollectionMenu({
|
|||||||
{renderModals && (
|
{renderModals && (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Collection members")}
|
title={t("Collection permissions")}
|
||||||
onRequestClose={() => setShowCollectionMembers(false)}
|
onRequestClose={() => setShowCollectionPermissions(false)}
|
||||||
isOpen={showCollectionMembers}
|
isOpen={showCollectionPermissions}
|
||||||
>
|
>
|
||||||
<CollectionMembers
|
<CollectionPermissions collection={collection} />
|
||||||
collection={collection}
|
|
||||||
onSubmit={() => setShowCollectionMembers(false)}
|
|
||||||
onEdit={() => setShowCollectionEdit(true)}
|
|
||||||
/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Edit collection")}
|
title={t("Edit collection")}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default class Collection extends BaseModel {
|
|||||||
description: string;
|
description: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
color: string;
|
color: string;
|
||||||
private: boolean;
|
permission: "read" | "read_write" | void;
|
||||||
sharing: boolean;
|
sharing: boolean;
|
||||||
index: string;
|
index: string;
|
||||||
documents: NavigationNode[];
|
documents: NavigationNode[];
|
||||||
@@ -25,11 +25,6 @@ export default class Collection extends BaseModel {
|
|||||||
sort: { field: string, direction: "asc" | "desc" };
|
sort: { field: string, direction: "asc" | "desc" };
|
||||||
url: string;
|
url: string;
|
||||||
|
|
||||||
@computed
|
|
||||||
get isPrivate(): boolean {
|
|
||||||
return this.private;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
get isEmpty(): boolean {
|
get isEmpty(): boolean {
|
||||||
return this.documents.length === 0;
|
return this.documents.length === 0;
|
||||||
@@ -121,7 +116,7 @@ export default class Collection extends BaseModel {
|
|||||||
"description",
|
"description",
|
||||||
"sharing",
|
"sharing",
|
||||||
"icon",
|
"icon",
|
||||||
"private",
|
"permission",
|
||||||
"sort",
|
"sort",
|
||||||
"index",
|
"index",
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import DocumentsStore from "stores/DocumentsStore";
|
|||||||
import PoliciesStore from "stores/PoliciesStore";
|
import PoliciesStore from "stores/PoliciesStore";
|
||||||
import UiStore from "stores/UiStore";
|
import UiStore from "stores/UiStore";
|
||||||
import Collection from "models/Collection";
|
import Collection from "models/Collection";
|
||||||
import CollectionEdit from "scenes/CollectionEdit";
|
import CollectionPermissions from "scenes/CollectionPermissions";
|
||||||
import CollectionMembers from "scenes/CollectionMembers";
|
|
||||||
import Search from "scenes/Search";
|
import Search from "scenes/Search";
|
||||||
import { Action, Separator } from "components/Actions";
|
import { Action, Separator } from "components/Actions";
|
||||||
import Badge from "components/Badge";
|
import Badge from "components/Badge";
|
||||||
@@ -53,7 +52,6 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
@observable collection: ?Collection;
|
@observable collection: ?Collection;
|
||||||
@observable isFetching: boolean = true;
|
@observable isFetching: boolean = true;
|
||||||
@observable permissionsModalOpen: boolean = false;
|
@observable permissionsModalOpen: boolean = false;
|
||||||
@observable editModalOpen: boolean = false;
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { id } = this.props.match.params;
|
const { id } = this.props.match.params;
|
||||||
@@ -113,14 +111,6 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
this.permissionsModalOpen = false;
|
this.permissionsModalOpen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
handleEditModalOpen = () => {
|
|
||||||
this.editModalOpen = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleEditModalClose = () => {
|
|
||||||
this.editModalOpen = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
renderActions() {
|
renderActions() {
|
||||||
const { match, policies, t } = this.props;
|
const { match, policies, t } = this.props;
|
||||||
const can = policies.abilities(match.params.id || "");
|
const can = policies.abilities(match.params.id || "");
|
||||||
@@ -221,32 +211,16 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{collection.private && (
|
<Button onClick={this.onPermissions} neutral>
|
||||||
<Button onClick={this.onPermissions} neutral>
|
{t("Manage permissions")}…
|
||||||
{t("Manage members")}…
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Empty>
|
</Empty>
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Collection members")}
|
title={t("Collection permissions")}
|
||||||
onRequestClose={this.handlePermissionsModalClose}
|
onRequestClose={this.handlePermissionsModalClose}
|
||||||
isOpen={this.permissionsModalOpen}
|
isOpen={this.permissionsModalOpen}
|
||||||
>
|
>
|
||||||
<CollectionMembers
|
<CollectionPermissions collection={this.collection} />
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</Centered>
|
</Centered>
|
||||||
) : (
|
) : (
|
||||||
@@ -254,10 +228,10 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
<Heading>
|
<Heading>
|
||||||
<CollectionIcon collection={collection} size={40} expanded />{" "}
|
<CollectionIcon collection={collection} size={40} expanded />{" "}
|
||||||
{collection.name}{" "}
|
{collection.name}{" "}
|
||||||
{collection.private && (
|
{!collection.permission && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltip={t(
|
tooltip={t(
|
||||||
"This collection is only visible to people given access"
|
"This collection is only visible to those given access"
|
||||||
)}
|
)}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class CollectionEdit extends React.Component<Props> {
|
|||||||
@observable sharing: boolean = this.props.collection.sharing;
|
@observable sharing: boolean = this.props.collection.sharing;
|
||||||
@observable icon: string = this.props.collection.icon;
|
@observable icon: string = this.props.collection.icon;
|
||||||
@observable color: string = this.props.collection.color || "#4E5C6E";
|
@observable color: string = this.props.collection.color || "#4E5C6E";
|
||||||
@observable private: boolean = this.props.collection.private;
|
|
||||||
@observable sort: { field: string, direction: "asc" | "desc" } = this.props
|
@observable sort: { field: string, direction: "asc" | "desc" } = this.props
|
||||||
.collection.sort;
|
.collection.sort;
|
||||||
@observable isSaving: boolean;
|
@observable isSaving: boolean;
|
||||||
@@ -43,7 +42,6 @@ class CollectionEdit extends React.Component<Props> {
|
|||||||
name: this.name,
|
name: this.name,
|
||||||
icon: this.icon,
|
icon: this.icon,
|
||||||
color: this.color,
|
color: this.color,
|
||||||
private: this.private,
|
|
||||||
sharing: this.sharing,
|
sharing: this.sharing,
|
||||||
sort: this.sort,
|
sort: this.sort,
|
||||||
});
|
});
|
||||||
@@ -75,10 +73,6 @@ class CollectionEdit extends React.Component<Props> {
|
|||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePrivateChange = (ev: SyntheticInputEvent<*>) => {
|
|
||||||
this.private = ev.target.checked;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSharingChange = (ev: SyntheticInputEvent<*>) => {
|
handleSharingChange = (ev: SyntheticInputEvent<*>) => {
|
||||||
this.sharing = ev.target.checked;
|
this.sharing = ev.target.checked;
|
||||||
};
|
};
|
||||||
@@ -122,17 +116,6 @@ class CollectionEdit extends React.Component<Props> {
|
|||||||
value={`${this.sort.field}.${this.sort.direction}`}
|
value={`${this.sort.field}.${this.sort.direction}`}
|
||||||
onChange={this.handleSortChange}
|
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
|
<Switch
|
||||||
id="sharing"
|
id="sharing"
|
||||||
label={t("Public document 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 HelpText from "components/HelpText";
|
||||||
import IconPicker, { icons } from "components/IconPicker";
|
import IconPicker, { icons } from "components/IconPicker";
|
||||||
import Input from "components/Input";
|
import Input from "components/Input";
|
||||||
|
import InputSelectPermission from "components/InputSelectPermission";
|
||||||
import Switch from "components/Switch";
|
import Switch from "components/Switch";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -31,7 +32,7 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
@observable icon: string = "";
|
@observable icon: string = "";
|
||||||
@observable color: string = "#4E5C6E";
|
@observable color: string = "#4E5C6E";
|
||||||
@observable sharing: boolean = true;
|
@observable sharing: boolean = true;
|
||||||
@observable private: boolean = false;
|
@observable permission: string = "read_write";
|
||||||
@observable isSaving: boolean;
|
@observable isSaving: boolean;
|
||||||
hasOpenedIconPicker: boolean = false;
|
hasOpenedIconPicker: boolean = false;
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
sharing: this.sharing,
|
sharing: this.sharing,
|
||||||
icon: this.icon,
|
icon: this.icon,
|
||||||
color: this.color,
|
color: this.color,
|
||||||
private: this.private,
|
permission: this.permission,
|
||||||
},
|
},
|
||||||
this.props.collections
|
this.props.collections
|
||||||
);
|
);
|
||||||
@@ -87,8 +88,8 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
this.hasOpenedIconPicker = true;
|
this.hasOpenedIconPicker = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePrivateChange = (ev: SyntheticInputEvent<HTMLInputElement>) => {
|
handlePermissionChange = (ev: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
this.private = ev.target.checked;
|
this.permission = ev.target.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSharingChange = (ev: SyntheticInputEvent<HTMLInputElement>) => {
|
handleSharingChange = (ev: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
@@ -131,15 +132,16 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
icon={this.icon}
|
icon={this.icon}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Switch
|
<InputSelectPermission
|
||||||
id="private"
|
value={this.permission}
|
||||||
label={t("Private collection")}
|
onChange={this.handlePermissionChange}
|
||||||
onChange={this.handlePrivateChange}
|
short
|
||||||
checked={this.private}
|
|
||||||
/>
|
/>
|
||||||
<HelpText>
|
<HelpText>
|
||||||
<Trans>
|
<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>
|
</Trans>
|
||||||
</HelpText>
|
</HelpText>
|
||||||
{teamSharingEnabled && (
|
{teamSharingEnabled && (
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import GroupListItem from "components/GroupListItem";
|
|||||||
import InputSelect from "components/InputSelect";
|
import InputSelect from "components/InputSelect";
|
||||||
import CollectionGroupMemberMenu from "menus/CollectionGroupMemberMenu";
|
import CollectionGroupMemberMenu from "menus/CollectionGroupMemberMenu";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
group: Group,
|
group: Group,
|
||||||
collectionGroupMembership: ?CollectionGroupMembership,
|
collectionGroupMembership: ?CollectionGroupMembership,
|
||||||
onUpdate: (permission: string) => void,
|
onUpdate: (permission: string) => any,
|
||||||
onRemove: () => void,
|
onRemove: () => any,
|
||||||
};
|
|};
|
||||||
|
|
||||||
const MemberListItem = ({
|
const CollectionGroupMemberListItem = ({
|
||||||
group,
|
group,
|
||||||
collectionGroupMembership,
|
collectionGroupMembership,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
@@ -25,8 +25,8 @@ const MemberListItem = ({
|
|||||||
|
|
||||||
const PERMISSIONS = React.useMemo(
|
const PERMISSIONS = React.useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ label: t("Read only"), value: "read" },
|
{ label: t("View only"), value: "read" },
|
||||||
{ label: t("Read & Edit"), value: "read_write" },
|
{ label: t("View and edit"), value: "read_write" },
|
||||||
],
|
],
|
||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
@@ -36,6 +36,7 @@ const MemberListItem = ({
|
|||||||
group={group}
|
group={group}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
|
showAvatar
|
||||||
renderActions={({ openMembersModal }) => (
|
renderActions={({ openMembersModal }) => (
|
||||||
<>
|
<>
|
||||||
<Select
|
<Select
|
||||||
@@ -48,13 +49,11 @@ const MemberListItem = ({
|
|||||||
}
|
}
|
||||||
onChange={(ev) => onUpdate(ev.target.value)}
|
onChange={(ev) => onUpdate(ev.target.value)}
|
||||||
labelHidden
|
labelHidden
|
||||||
|
/>{" "}
|
||||||
|
<CollectionGroupMemberMenu
|
||||||
|
onMembers={openMembersModal}
|
||||||
|
onRemove={onRemove}
|
||||||
/>
|
/>
|
||||||
<ButtonWrap>
|
|
||||||
<CollectionGroupMemberMenu
|
|
||||||
onMembers={openMembersModal}
|
|
||||||
onRemove={onRemove}
|
|
||||||
/>
|
|
||||||
</ButtonWrap>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -64,10 +63,7 @@ const MemberListItem = ({
|
|||||||
const Select = styled(InputSelect)`
|
const Select = styled(InputSelect)`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
border-color: transparent;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ButtonWrap = styled.div`
|
export default CollectionGroupMemberListItem;
|
||||||
margin-left: 6px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default MemberListItem;
|
|
||||||
@@ -17,9 +17,9 @@ type Props = {
|
|||||||
user: User,
|
user: User,
|
||||||
membership?: ?Membership,
|
membership?: ?Membership,
|
||||||
canEdit: boolean,
|
canEdit: boolean,
|
||||||
onAdd?: () => void,
|
onAdd?: () => any,
|
||||||
onRemove?: () => void,
|
onRemove?: () => any,
|
||||||
onUpdate?: (permission: string) => void,
|
onUpdate?: (permission: string) => any,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MemberListItem = ({
|
const MemberListItem = ({
|
||||||
@@ -34,8 +34,8 @@ const MemberListItem = ({
|
|||||||
|
|
||||||
const PERMISSIONS = React.useMemo(
|
const PERMISSIONS = React.useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ label: t("Read only"), value: "read" },
|
{ label: t("View only"), value: "read" },
|
||||||
{ label: t("Read & Edit"), value: "read_write" },
|
{ label: t("View and edit"), value: "read_write" },
|
||||||
],
|
],
|
||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
@@ -67,8 +67,7 @@ const MemberListItem = ({
|
|||||||
onChange={(ev) => onUpdate(ev.target.value)}
|
onChange={(ev) => onUpdate(ev.target.value)}
|
||||||
labelHidden
|
labelHidden
|
||||||
/>
|
/>
|
||||||
)}
|
)}{" "}
|
||||||
|
|
||||||
{canEdit && onRemove && <MemberMenu onRemove={onRemove} />}
|
{canEdit && onRemove && <MemberMenu onRemove={onRemove} />}
|
||||||
{canEdit && onAdd && (
|
{canEdit && onAdd && (
|
||||||
<Button onClick={onAdd} neutral>
|
<Button onClick={onAdd} neutral>
|
||||||
@@ -84,6 +83,7 @@ const MemberListItem = ({
|
|||||||
const Select = styled(InputSelect)`
|
const Select = styled(InputSelect)`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
border-color: transparent;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default MemberListItem;
|
export default MemberListItem;
|
||||||
@@ -12,7 +12,7 @@ import Time from "components/Time";
|
|||||||
type Props = {
|
type Props = {
|
||||||
user: User,
|
user: User,
|
||||||
canEdit: boolean,
|
canEdit: boolean,
|
||||||
onAdd: () => void,
|
onAdd: () => any,
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserListItem = ({ user, onAdd, canEdit }: Props) => {
|
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>
|
<HelpText>
|
||||||
Add and remove team members in the <strong>{group.name}</strong>{" "}
|
Add and remove team members in the <strong>{group.name}</strong>{" "}
|
||||||
group. Adding people to the group will give them access to any
|
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>
|
</HelpText>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<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
|
* 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",
|
"sequelize:migrate": "sequelize db:migrate",
|
||||||
"db:create-migration": "sequelize migration:create",
|
"db:create-migration": "sequelize migration:create",
|
||||||
"db:migrate": "sequelize db:migrate",
|
"db:migrate": "sequelize db:migrate",
|
||||||
|
"db:rollback": "sequelize db:migrate:undo",
|
||||||
"upgrade": "git fetch && git pull && yarn install && yarn heroku-postbuild",
|
"upgrade": "git fetch && git pull && yarn install && yarn heroku-postbuild",
|
||||||
"test": "yarn test:app && yarn test:server",
|
"test": "yarn test:app && yarn test:server",
|
||||||
"test:app": "jest",
|
"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 () => {
|
it("should not allow deleting an attachment belonging to a document user does not have access to", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: collection.teamId,
|
teamId: collection.teamId,
|
||||||
@@ -184,7 +184,7 @@ describe("#attachments.redirect", () => {
|
|||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: user.teamId,
|
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 () => {
|
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 user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: collection.teamId,
|
teamId: collection.teamId,
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ router.post("collections.create", auth(), async (ctx) => {
|
|||||||
name,
|
name,
|
||||||
color,
|
color,
|
||||||
description,
|
description,
|
||||||
|
permission,
|
||||||
sharing,
|
sharing,
|
||||||
icon,
|
icon,
|
||||||
sort = Collection.DEFAULT_SORT,
|
sort = Collection.DEFAULT_SORT,
|
||||||
} = ctx.body;
|
} = ctx.body;
|
||||||
|
|
||||||
let { index } = ctx.body;
|
let { index } = ctx.body;
|
||||||
const isPrivate = ctx.body.private;
|
|
||||||
ctx.assertPresent(name, "name is required");
|
ctx.assertPresent(name, "name is required");
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
@@ -89,7 +89,7 @@ router.post("collections.create", auth(), async (ctx) => {
|
|||||||
color,
|
color,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
createdById: user.id,
|
createdById: user.id,
|
||||||
private: isPrivate,
|
permission: permission ? permission : null,
|
||||||
sharing,
|
sharing,
|
||||||
sort,
|
sort,
|
||||||
index,
|
index,
|
||||||
@@ -105,11 +105,9 @@ router.post("collections.create", auth(), async (ctx) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// we must reload the collection to get memberships for policy presenter
|
// we must reload the collection to get memberships for policy presenter
|
||||||
if (isPrivate) {
|
collection = await Collection.scope({
|
||||||
collection = await Collection.scope({
|
method: ["withMembership", user.id],
|
||||||
method: ["withMembership", user.id],
|
}).findByPk(collection.id);
|
||||||
}).findByPk(collection.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: presentCollection(collection),
|
data: presentCollection(collection),
|
||||||
@@ -514,8 +512,16 @@ router.post("collections.export_all", auth(), async (ctx) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post("collections.update", auth(), async (ctx) => {
|
router.post("collections.update", auth(), async (ctx) => {
|
||||||
let { id, name, description, icon, color, sort, sharing } = ctx.body;
|
let {
|
||||||
const isPrivate = ctx.body.private;
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
permission,
|
||||||
|
color,
|
||||||
|
sort,
|
||||||
|
sharing,
|
||||||
|
} = ctx.body;
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
ctx.assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
|
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);
|
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
|
// 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({
|
await CollectionUser.findOrCreate({
|
||||||
where: {
|
where: {
|
||||||
collectionId: collection.id,
|
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) {
|
if (name !== undefined) {
|
||||||
collection.name = name;
|
collection.name = name;
|
||||||
@@ -557,8 +563,8 @@ router.post("collections.update", auth(), async (ctx) => {
|
|||||||
if (color !== undefined) {
|
if (color !== undefined) {
|
||||||
collection.color = color;
|
collection.color = color;
|
||||||
}
|
}
|
||||||
if (isPrivate !== undefined) {
|
if (permission !== undefined) {
|
||||||
collection.private = isPrivate;
|
collection.permission = permission ? permission : null;
|
||||||
}
|
}
|
||||||
if (sharing !== undefined) {
|
if (sharing !== undefined) {
|
||||||
collection.sharing = sharing;
|
collection.sharing = sharing;
|
||||||
@@ -580,7 +586,7 @@ router.post("collections.update", auth(), async (ctx) => {
|
|||||||
|
|
||||||
// must reload to update collection membership for correct policy calculation
|
// must reload to update collection membership for correct policy calculation
|
||||||
// if the privacy level has changed. Otherwise skip this query for speed.
|
// if the privacy level has changed. Otherwise skip this query for speed.
|
||||||
if (isPrivacyChanged) {
|
if (permissionChanged) {
|
||||||
await collection.reload();
|
await collection.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe("#collections.list", () => {
|
|||||||
it("should not return private collections actor is not a member of", async () => {
|
it("should not return private collections actor is not a member of", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
await buildCollection({
|
await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
const res = await server.post("/api/collections.list", {
|
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 () => {
|
it("should return private collections actor is a member of", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
await buildCollection({
|
await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
await buildCollection({
|
await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
@@ -82,13 +82,13 @@ describe("#collections.list", () => {
|
|||||||
it("should return private collections actor is a group-member of", async () => {
|
it("should return private collections actor is a group-member of", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
await buildCollection({
|
await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ describe("#collections.export", () => {
|
|||||||
it("should now allow export of private collection not a member", async () => {
|
it("should now allow export of private collection not a member", async () => {
|
||||||
const { user } = await seed();
|
const { user } = await seed();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
const res = await server.post("/api/collections.export", {
|
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 () => {
|
it("should allow export of private collection when the actor is a member", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
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 () => {
|
it("should allow export of private collection when the actor is a group member", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ describe("#collections.add_user", () => {
|
|||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const anotherUser = await buildUser({ teamId: user.teamId });
|
const anotherUser = await buildUser({ teamId: user.teamId });
|
||||||
const res = await server.post("/api/collections.add_user", {
|
const res = await server.post("/api/collections.add_user", {
|
||||||
@@ -389,7 +389,7 @@ describe("#collections.add_user", () => {
|
|||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const anotherUser = await buildUser();
|
const anotherUser = await buildUser();
|
||||||
const res = await server.post("/api/collections.add_user", {
|
const res = await server.post("/api/collections.add_user", {
|
||||||
@@ -433,7 +433,7 @@ describe("#collections.add_group", () => {
|
|||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const group = await buildGroup({ teamId: user.teamId });
|
const group = await buildGroup({ teamId: user.teamId });
|
||||||
const res = await server.post("/api/collections.add_group", {
|
const res = await server.post("/api/collections.add_group", {
|
||||||
@@ -454,7 +454,7 @@ describe("#collections.add_group", () => {
|
|||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const group = await buildGroup();
|
const group = await buildGroup();
|
||||||
const res = await server.post("/api/collections.add_group", {
|
const res = await server.post("/api/collections.add_group", {
|
||||||
@@ -496,7 +496,7 @@ describe("#collections.remove_group", () => {
|
|||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const group = await buildGroup({ teamId: user.teamId });
|
const group = await buildGroup({ teamId: user.teamId });
|
||||||
|
|
||||||
@@ -528,7 +528,7 @@ describe("#collections.remove_group", () => {
|
|||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const group = await buildGroup();
|
const group = await buildGroup();
|
||||||
const res = await server.post("/api/collections.remove_group", {
|
const res = await server.post("/api/collections.remove_group", {
|
||||||
@@ -572,7 +572,7 @@ describe("#collections.remove_user", () => {
|
|||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const anotherUser = await buildUser({ teamId: user.teamId });
|
const anotherUser = await buildUser({ teamId: user.teamId });
|
||||||
|
|
||||||
@@ -601,7 +601,7 @@ describe("#collections.remove_user", () => {
|
|||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const anotherUser = await buildUser();
|
const anotherUser = await buildUser();
|
||||||
const res = await server.post("/api/collections.remove_user", {
|
const res = await server.post("/api/collections.remove_user", {
|
||||||
@@ -642,7 +642,7 @@ describe("#collections.remove_user", () => {
|
|||||||
describe("#collections.users", () => {
|
describe("#collections.users", () => {
|
||||||
it("should return users in private collection", async () => {
|
it("should return users in private collection", async () => {
|
||||||
const { collection, user } = await seed();
|
const { collection, user } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -684,7 +684,7 @@ describe("#collections.group_memberships", () => {
|
|||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const group = await buildGroup({ teamId: user.teamId });
|
const group = await buildGroup({ teamId: user.teamId });
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -721,7 +721,7 @@ describe("#collections.group_memberships", () => {
|
|||||||
const group = await buildGroup({ name: "will find", teamId: user.teamId });
|
const group = await buildGroup({ name: "will find", teamId: user.teamId });
|
||||||
const group2 = await buildGroup({ name: "wont find", teamId: user.teamId });
|
const group2 = await buildGroup({ name: "wont find", teamId: user.teamId });
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -766,7 +766,7 @@ describe("#collections.group_memberships", () => {
|
|||||||
const group = await buildGroup({ teamId: user.teamId });
|
const group = await buildGroup({ teamId: user.teamId });
|
||||||
const group2 = await buildGroup({ teamId: user.teamId });
|
const group2 = await buildGroup({ teamId: user.teamId });
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -817,7 +817,7 @@ describe("#collections.group_memberships", () => {
|
|||||||
it("should require authorization", async () => {
|
it("should require authorization", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -831,7 +831,7 @@ describe("#collections.group_memberships", () => {
|
|||||||
describe("#collections.memberships", () => {
|
describe("#collections.memberships", () => {
|
||||||
it("should return members in private collection", async () => {
|
it("should return members in private collection", async () => {
|
||||||
const { collection, user } = await seed();
|
const { collection, user } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -945,7 +945,7 @@ describe("#collections.info", () => {
|
|||||||
|
|
||||||
it("should require user member of collection", async () => {
|
it("should require user member of collection", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
const res = await server.post("/api/collections.info", {
|
const res = await server.post("/api/collections.info", {
|
||||||
@@ -956,7 +956,7 @@ describe("#collections.info", () => {
|
|||||||
|
|
||||||
it("should allow user member of collection", async () => {
|
it("should allow user member of collection", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -1034,12 +1034,12 @@ describe("#collections.create", () => {
|
|||||||
it("should return correct policies with private collection", async () => {
|
it("should return correct policies with private collection", async () => {
|
||||||
const { user } = await seed();
|
const { user } = await seed();
|
||||||
const res = await server.post("/api/collections.create", {
|
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();
|
const body = await res.json();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
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.length).toBe(1);
|
||||||
expect(body.policies[0].abilities.read).toBeTruthy();
|
expect(body.policies[0].abilities.read).toBeTruthy();
|
||||||
expect(body.policies[0].abilities.export).toBeTruthy();
|
expect(body.policies[0].abilities.export).toBeTruthy();
|
||||||
@@ -1176,11 +1176,11 @@ describe("#collections.update", () => {
|
|||||||
it("allows editing individual fields", async () => {
|
it("allows editing individual fields", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
const res = await server.post("/api/collections.update", {
|
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();
|
const body = await res.json();
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body.data.private).toBe(true);
|
expect(body.data.permission).toBe(null);
|
||||||
expect(body.data.name).toBe(collection.name);
|
expect(body.data.name).toBe(collection.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1190,14 +1190,14 @@ describe("#collections.update", () => {
|
|||||||
body: {
|
body: {
|
||||||
token: user.getJwtToken(),
|
token: user.getJwtToken(),
|
||||||
id: collection.id,
|
id: collection.id,
|
||||||
private: true,
|
permission: null,
|
||||||
name: "Test",
|
name: "Test",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body.data.name).toBe("Test");
|
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
|
// ensure we return with a write level policy
|
||||||
expect(body.policies.length).toBe(1);
|
expect(body.policies.length).toBe(1);
|
||||||
@@ -1206,7 +1206,7 @@ describe("#collections.update", () => {
|
|||||||
|
|
||||||
it("allows editing from private to non-private collection", async () => {
|
it("allows editing from private to non-private collection", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -1220,14 +1220,14 @@ describe("#collections.update", () => {
|
|||||||
body: {
|
body: {
|
||||||
token: user.getJwtToken(),
|
token: user.getJwtToken(),
|
||||||
id: collection.id,
|
id: collection.id,
|
||||||
private: false,
|
permission: "read_write",
|
||||||
name: "Test",
|
name: "Test",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body.data.name).toBe("Test");
|
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
|
// ensure we return with a write level policy
|
||||||
expect(body.policies.length).toBe(1);
|
expect(body.policies.length).toBe(1);
|
||||||
@@ -1236,7 +1236,7 @@ describe("#collections.update", () => {
|
|||||||
|
|
||||||
it("allows editing by read-write collection user", async () => {
|
it("allows editing by read-write collection user", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -1258,7 +1258,7 @@ describe("#collections.update", () => {
|
|||||||
it("allows editing by read-write collection group user", async () => {
|
it("allows editing by read-write collection group user", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1280,7 +1280,7 @@ describe("#collections.update", () => {
|
|||||||
|
|
||||||
it("does not allow editing by read-only collection user", async () => {
|
it("does not allow editing by read-only collection user", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -1393,7 +1393,7 @@ describe("#collections.delete", () => {
|
|||||||
it("allows deleting by read-write collection group user", async () => {
|
it("allows deleting by read-write collection group user", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
await buildCollection({
|
await buildCollection({
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ describe("#documents.info", () => {
|
|||||||
it("should not return published document in collection not a member of", async () => {
|
it("should not return published document in collection not a member of", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
const document = await buildDocument({ collectionId: collection.id });
|
const document = await buildDocument({ collectionId: collection.id });
|
||||||
@@ -209,7 +209,7 @@ describe("#documents.info", () => {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
const res = await server.post("/api/documents.info", {
|
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 () => {
|
it("should not return published document in collection not a member of", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
const document = await buildDocument({ collectionId: collection.id });
|
const document = await buildDocument({ collectionId: collection.id });
|
||||||
@@ -400,7 +400,7 @@ describe("#documents.export", () => {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
const res = await server.post("/api/documents.export", {
|
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 () => {
|
it("should not return documents in private collections not a member of", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
const res = await server.post("/api/documents.list", {
|
const res = await server.post("/api/documents.list", {
|
||||||
@@ -573,7 +573,7 @@ describe("#documents.list", () => {
|
|||||||
|
|
||||||
it("should allow filtering to private collection", async () => {
|
it("should allow filtering to private collection", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -647,7 +647,7 @@ describe("#documents.pinned", () => {
|
|||||||
|
|
||||||
it("should return pinned documents in private collections member of", async () => {
|
it("should return pinned documents in private collections member of", async () => {
|
||||||
const { user, collection, document } = await seed();
|
const { user, collection, document } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
document.pinnedById = user.id;
|
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 () => {
|
it("should not return pinned documents in private collections not a member of", async () => {
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = await buildUser({ teamId: collection.teamId });
|
const user = await buildUser({ teamId: collection.teamId });
|
||||||
@@ -710,7 +710,7 @@ describe("#documents.drafts", () => {
|
|||||||
document.publishedAt = null;
|
document.publishedAt = null;
|
||||||
await document.save();
|
await document.save();
|
||||||
|
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
const res = await server.post("/api/documents.drafts", {
|
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 () => {
|
it("should return documents for a specific private collection", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -1061,7 +1061,7 @@ describe("#documents.search", () => {
|
|||||||
|
|
||||||
it("should not return documents in private collections not a member of", async () => {
|
it("should not return documents in private collections not a member of", async () => {
|
||||||
const { user } = await seed();
|
const { user } = await seed();
|
||||||
const collection = await buildCollection({ private: true });
|
const collection = await buildCollection({ permission: null });
|
||||||
|
|
||||||
await buildDocument({
|
await buildDocument({
|
||||||
title: "search term",
|
title: "search term",
|
||||||
@@ -1158,7 +1158,7 @@ describe("#documents.archived", () => {
|
|||||||
|
|
||||||
it("should not return documents in private collections not a member of", async () => {
|
it("should not return documents in private collections not a member of", async () => {
|
||||||
const { user } = await seed();
|
const { user } = await seed();
|
||||||
const collection = await buildCollection({ private: true });
|
const collection = await buildCollection({ permission: null });
|
||||||
|
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
@@ -1224,7 +1224,7 @@ describe("#documents.viewed", () => {
|
|||||||
it("should not return recently viewed documents in collection not a member of", async () => {
|
it("should not return recently viewed documents in collection not a member of", async () => {
|
||||||
const { user, document, collection } = await seed();
|
const { user, document, collection } = await seed();
|
||||||
await View.increment({ documentId: document.id, userId: user.id });
|
await View.increment({ documentId: document.id, userId: user.id });
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
const res = await server.post("/api/documents.viewed", {
|
const res = await server.post("/api/documents.viewed", {
|
||||||
@@ -1808,7 +1808,7 @@ describe("#documents.update", () => {
|
|||||||
document.publishedAt = null;
|
document.publishedAt = null;
|
||||||
await document.save();
|
await document.save();
|
||||||
|
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -1903,7 +1903,7 @@ describe("#documents.update", () => {
|
|||||||
|
|
||||||
it("allows editing by read-write collection user", async () => {
|
it("allows editing by read-write collection user", async () => {
|
||||||
const { admin, document, collection } = await seed();
|
const { admin, document, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -1931,7 +1931,7 @@ describe("#documents.update", () => {
|
|||||||
|
|
||||||
it("does not allow editing by read-only collection user", async () => {
|
it("does not allow editing by read-only collection user", async () => {
|
||||||
const { user, document, collection } = await seed();
|
const { user, document, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -1953,6 +1953,23 @@ describe("#documents.update", () => {
|
|||||||
expect(res.status).toEqual(403);
|
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 () => {
|
it("should append document with text", async () => {
|
||||||
const { user, document } = await seed();
|
const { user, document } = await seed();
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ describe("#revisions.list", () => {
|
|||||||
const { user, document, collection } = await seed();
|
const { user, document, collection } = await seed();
|
||||||
await Revision.createFromDocument(document);
|
await Revision.createFromDocument(document);
|
||||||
|
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
const res = await server.post("/api/revisions.list", {
|
const res = await server.post("/api/revisions.list", {
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ describe("#shares.list", () => {
|
|||||||
userId: admin.id,
|
userId: admin.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
const res = await server.post("/api/shares.list", {
|
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 () => {
|
it("should not allow creating a share record with read-only permissions", async () => {
|
||||||
const { user, document, collection } = await seed();
|
const { user, document, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
|
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ describe("#views.list", () => {
|
|||||||
|
|
||||||
it("should return views for a document in read-only collection", async () => {
|
it("should return views for a document in read-only collection", async () => {
|
||||||
const { user, document, collection } = await seed();
|
const { user, document, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
@@ -84,7 +84,7 @@ describe("#views.create", () => {
|
|||||||
|
|
||||||
it("should allow creating a view record for document in read-only collection", async () => {
|
it("should allow creating a view record for document in read-only collection", async () => {
|
||||||
const { user, document, collection } = await seed();
|
const { user, document, collection } = await seed();
|
||||||
collection.private = true;
|
collection.permission = null;
|
||||||
await collection.save();
|
await collection.save();
|
||||||
|
|
||||||
await CollectionUser.create({
|
await CollectionUser.create({
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default async function collectionImporter({
|
|||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
createdById: user.id,
|
createdById: user.id,
|
||||||
private: false,
|
permission: "read_write",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ export default async function collectionImporter({
|
|||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
createdById: user.id,
|
createdById: user.id,
|
||||||
name,
|
name,
|
||||||
private: false,
|
permission: "read_write",
|
||||||
});
|
});
|
||||||
await Event.create({
|
await Event.create({
|
||||||
name: "collections.create",
|
name: "collections.create",
|
||||||
|
|||||||
38
server/migrations/20210327005406-read-only-collections.js
Normal file
38
server/migrations/20210327005406-read-only-collections.js
Normal file
@@ -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,
|
type: DataTypes.STRING,
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
},
|
},
|
||||||
private: DataTypes.BOOLEAN,
|
permission: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
defaultValue: null,
|
||||||
|
allowNull: true,
|
||||||
|
validate: {
|
||||||
|
isIn: [["read", "read_write"]],
|
||||||
|
},
|
||||||
|
},
|
||||||
maintainerApprovalRequired: DataTypes.BOOLEAN,
|
maintainerApprovalRequired: DataTypes.BOOLEAN,
|
||||||
documentStructure: DataTypes.JSONB,
|
documentStructure: DataTypes.JSONB,
|
||||||
sharing: {
|
sharing: {
|
||||||
@@ -199,7 +206,7 @@ Collection.addHook("afterDestroy", async (model: Collection) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Collection.addHook("afterCreate", (model: Collection, options) => {
|
Collection.addHook("afterCreate", (model: Collection, options) => {
|
||||||
if (model.private) {
|
if (model.permission !== "read_write") {
|
||||||
return CollectionUser.findOrCreate({
|
return CollectionUser.findOrCreate({
|
||||||
where: {
|
where: {
|
||||||
collectionId: model.id,
|
collectionId: model.id,
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ describe("#membershipUserIds", () => {
|
|||||||
|
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
userId: users[0].id,
|
userId: users[0].id,
|
||||||
private: true,
|
permission: null,
|
||||||
teamId,
|
teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ describe("#searchForTeam", () => {
|
|||||||
test("should not return search results from private collections", async () => {
|
test("should not return search results from private collections", async () => {
|
||||||
const team = await buildTeam();
|
const team = await buildTeam();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
});
|
});
|
||||||
await buildDocument({
|
await buildDocument({
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ describe("afterDestroy hook", () => {
|
|||||||
const user2 = await buildUser({ teamId });
|
const user2 = await buildUser({ teamId });
|
||||||
|
|
||||||
const collection1 = await buildCollection({
|
const collection1 = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId,
|
teamId,
|
||||||
});
|
});
|
||||||
const collection2 = await buildCollection({
|
const collection2 = await buildCollection({
|
||||||
private: true,
|
permission: null,
|
||||||
teamId,
|
teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -225,10 +225,16 @@ Team.prototype.activateUser = async function (user: User, admin: User) {
|
|||||||
|
|
||||||
Team.prototype.collectionIds = async function (paranoid: boolean = true) {
|
Team.prototype.collectionIds = async function (paranoid: boolean = true) {
|
||||||
let models = await Collection.findAll({
|
let models = await Collection.findAll({
|
||||||
attributes: ["id", "private"],
|
attributes: ["id"],
|
||||||
where: { teamId: this.id, private: false },
|
where: {
|
||||||
|
teamId: this.id,
|
||||||
|
permission: {
|
||||||
|
[Op.ne]: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
paranoid,
|
paranoid,
|
||||||
});
|
});
|
||||||
|
|
||||||
return models.map((c) => c.id);
|
return models.map((c) => c.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,56 @@
|
|||||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
// @flow
|
||||||
import { buildTeam } from "../test/factories";
|
import { buildTeam, buildCollection } from "../test/factories";
|
||||||
import { flushdb } from "../test/support";
|
import { flushdb } from "../test/support";
|
||||||
|
|
||||||
beforeEach(() => flushdb());
|
beforeEach(() => flushdb());
|
||||||
|
|
||||||
it("should set subdomain if available", async () => {
|
describe("collectionIds", () => {
|
||||||
const team = await buildTeam();
|
it("should return non-private collection ids", async () => {
|
||||||
const subdomain = await team.provisionSubdomain("testy");
|
const team = await buildTeam();
|
||||||
expect(subdomain).toEqual("testy");
|
const collection = await buildCollection({ teamId: team.id });
|
||||||
expect(team.subdomain).toEqual("testy");
|
|
||||||
|
// 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 () => {
|
describe("provisionSubdomain", () => {
|
||||||
await buildTeam({ subdomain: "myteam" });
|
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();
|
it("should set subdomain append if unavailable", async () => {
|
||||||
const subdomain = await team.provisionSubdomain("myteam");
|
await buildTeam({ subdomain: "myteam" });
|
||||||
expect(subdomain).toEqual("myteam1");
|
|
||||||
expect(team.subdomain).toEqual("myteam1");
|
const team = await buildTeam();
|
||||||
});
|
const subdomain = await team.provisionSubdomain("myteam");
|
||||||
|
expect(subdomain).toEqual("myteam1");
|
||||||
it("should increment subdomain append if unavailable", async () => {
|
expect(team.subdomain).toEqual("myteam1");
|
||||||
await buildTeam({ subdomain: "myteam" });
|
});
|
||||||
await buildTeam({ subdomain: "myteam1" });
|
|
||||||
|
it("should increment subdomain append if unavailable", async () => {
|
||||||
const team = await buildTeam();
|
await buildTeam({ subdomain: "myteam" });
|
||||||
const subdomain = await team.provisionSubdomain("myteam");
|
await buildTeam({ subdomain: "myteam1" });
|
||||||
expect(subdomain).toEqual("myteam2");
|
|
||||||
expect(team.subdomain).toEqual("myteam2");
|
const team = await buildTeam();
|
||||||
});
|
const subdomain = await team.provisionSubdomain("myteam");
|
||||||
|
expect(subdomain).toEqual("myteam2");
|
||||||
it("should do nothing if subdomain already set", async () => {
|
expect(team.subdomain).toEqual("myteam2");
|
||||||
const team = await buildTeam({ subdomain: "example" });
|
});
|
||||||
const subdomain = await team.provisionSubdomain("myteam");
|
|
||||||
expect(subdomain).toEqual("example");
|
it("should do nothing if subdomain already set", async () => {
|
||||||
expect(team.subdomain).toEqual("example");
|
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({
|
const collectionStubs = await Collection.scope({
|
||||||
method: ["withMembership", this.id],
|
method: ["withMembership", this.id],
|
||||||
}).findAll({
|
}).findAll({
|
||||||
attributes: ["id", "private"],
|
attributes: ["id", "permission"],
|
||||||
where: { teamId: this.teamId },
|
where: { teamId: this.teamId },
|
||||||
paranoid: true,
|
paranoid: true,
|
||||||
...options,
|
...options,
|
||||||
@@ -100,7 +100,8 @@ User.prototype.collectionIds = async function (options = {}) {
|
|||||||
return collectionStubs
|
return collectionStubs
|
||||||
.filter(
|
.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
!c.private ||
|
c.permission === "read" ||
|
||||||
|
c.permission === "read_write" ||
|
||||||
c.memberships.length > 0 ||
|
c.memberships.length > 0 ||
|
||||||
c.collectionGroupMemberships.length > 0
|
c.collectionGroupMemberships.length > 0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,75 @@
|
|||||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
// @flow
|
||||||
import { buildUser } from "../test/factories";
|
import { CollectionUser } from "../models";
|
||||||
|
import { buildUser, buildTeam, buildCollection } from "../test/factories";
|
||||||
import { flushdb } from "../test/support";
|
import { flushdb } from "../test/support";
|
||||||
|
|
||||||
beforeEach(() => flushdb());
|
beforeEach(() => flushdb());
|
||||||
|
|
||||||
it("should set JWT secret", async () => {
|
describe("user model", () => {
|
||||||
const user = await buildUser();
|
describe("getJwtToken", () => {
|
||||||
expect(user.getJwtToken()).toBeTruthy();
|
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) => {
|
allow(User, ["read", "export"], Collection, (user, collection) => {
|
||||||
if (!collection || user.teamId !== collection.teamId) return false;
|
if (!collection || user.teamId !== collection.teamId) return false;
|
||||||
|
|
||||||
if (collection.private) {
|
if (!collection.permission) {
|
||||||
invariant(
|
invariant(
|
||||||
collection.memberships,
|
collection.memberships,
|
||||||
"membership should be preloaded, did you forget withMembership scope?"
|
"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 || user.teamId !== collection.teamId) return false;
|
||||||
if (!collection.sharing) return false;
|
if (!collection.sharing) return false;
|
||||||
|
|
||||||
if (collection.private) {
|
if (collection.permission !== "read_write") {
|
||||||
invariant(
|
invariant(
|
||||||
collection.memberships,
|
collection.memberships,
|
||||||
"membership should be preloaded, did you forget withMembership scope?"
|
"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) => {
|
allow(User, ["publish", "update"], Collection, (user, collection) => {
|
||||||
if (!collection || user.teamId !== collection.teamId) return false;
|
if (!collection || user.teamId !== collection.teamId) return false;
|
||||||
|
|
||||||
if (collection.private) {
|
if (collection.permission !== "read_write") {
|
||||||
invariant(
|
invariant(
|
||||||
collection.memberships,
|
collection.memberships,
|
||||||
"membership should be preloaded, did you forget withMembership scope?"
|
"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) => {
|
allow(User, "delete", Collection, (user, collection) => {
|
||||||
if (!collection || user.teamId !== collection.teamId) return false;
|
if (!collection || user.teamId !== collection.teamId) return false;
|
||||||
|
|
||||||
if (collection.private) {
|
if (collection.permission !== "read_write") {
|
||||||
invariant(
|
invariant(
|
||||||
collection.memberships,
|
collection.memberships,
|
||||||
"membership should be preloaded, did you forget withMembership scope?"
|
"membership should be preloaded, did you forget withMembership scope?"
|
||||||
|
|||||||
136
server/policies/collection.test.js
Normal file
136
server/policies/collection.test.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
83
server/policies/document.test.js
Normal file
83
server/policies/document.test.js
Normal file
@@ -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,
|
icon: collection.icon,
|
||||||
index: collection.index,
|
index: collection.index,
|
||||||
color: collection.color || "#4E5C6E",
|
color: collection.color || "#4E5C6E",
|
||||||
private: collection.private,
|
permission: collection.permission,
|
||||||
sharing: collection.sharing,
|
sharing: collection.sharing,
|
||||||
createdAt: collection.createdAt,
|
createdAt: collection.createdAt,
|
||||||
updatedAt: collection.updatedAt,
|
updatedAt: collection.updatedAt,
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default class Notifications {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
if (!collection) return;
|
if (!collection) return;
|
||||||
if (collection.private) return;
|
if (!collection.permission) return;
|
||||||
|
|
||||||
const notificationSettings = await NotificationSetting.findAll({
|
const notificationSettings = await NotificationSetting.findAll({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ describe("documents.publish", () => {
|
|||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
private: true,
|
permission: null,
|
||||||
});
|
});
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
|
|||||||
@@ -157,9 +157,9 @@ export default class Websockets {
|
|||||||
|
|
||||||
socketio
|
socketio
|
||||||
.to(
|
.to(
|
||||||
collection.private
|
collection.permission
|
||||||
? `collection-${collection.id}`
|
? `team-${collection.teamId}`
|
||||||
: `team-${collection.teamId}`
|
: `collection-${collection.id}`
|
||||||
)
|
)
|
||||||
.emit("entities", {
|
.emit("entities", {
|
||||||
event: event.name,
|
event: event.name,
|
||||||
@@ -173,9 +173,9 @@ export default class Websockets {
|
|||||||
|
|
||||||
return socketio
|
return socketio
|
||||||
.to(
|
.to(
|
||||||
collection.private
|
collection.permission
|
||||||
? `collection-${collection.id}`
|
? `team-${collection.teamId}`
|
||||||
: `team-${collection.teamId}`
|
: `collection-${collection.id}`
|
||||||
)
|
)
|
||||||
.emit("join", {
|
.emit("join", {
|
||||||
event: event.name,
|
event: event.name,
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export async function buildCollection(overrides: Object = {}) {
|
|||||||
name: `Test Collection ${count}`,
|
name: `Test Collection ${count}`,
|
||||||
description: "Test collection description",
|
description: "Test collection description",
|
||||||
createdById: overrides.userId,
|
createdById: overrides.userId,
|
||||||
|
permission: "read_write",
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export const seed = async () => {
|
|||||||
urlId: "collection",
|
urlId: "collection",
|
||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
createdById: user.id,
|
createdById: user.id,
|
||||||
|
permission: "read_write",
|
||||||
});
|
});
|
||||||
|
|
||||||
const document = await Document.create({
|
const document = await Document.create({
|
||||||
|
|||||||
@@ -86,6 +86,10 @@
|
|||||||
"Choose icon": "Choose icon",
|
"Choose icon": "Choose icon",
|
||||||
"Loading": "Loading",
|
"Loading": "Loading",
|
||||||
"Search": "Search",
|
"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?",
|
"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",
|
"Change Language": "Change Language",
|
||||||
"Dismiss": "Dismiss",
|
"Dismiss": "Dismiss",
|
||||||
@@ -134,8 +138,9 @@
|
|||||||
"New document": "New document",
|
"New document": "New document",
|
||||||
"Import document": "Import document",
|
"Import document": "Import document",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
|
"Permissions": "Permissions",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
"Collection members": "Collection members",
|
"Collection permissions": "Collection permissions",
|
||||||
"Edit collection": "Edit collection",
|
"Edit collection": "Edit collection",
|
||||||
"Delete collection": "Delete collection",
|
"Delete collection": "Delete collection",
|
||||||
"Export collection": "Export 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.",
|
"<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!",
|
"Get started by creating a new one!": "Get started by creating a new one!",
|
||||||
"Create a document": "Create a document",
|
"Create a document": "Create a document",
|
||||||
"Manage members": "Manage members",
|
"Manage permissions": "Manage permissions",
|
||||||
"This collection is only visible to people given access": "This collection is only visible to people given access",
|
"This collection is only visible to those given access": "This collection is only visible to those given access",
|
||||||
"Private": "Private",
|
"Private": "Private",
|
||||||
"Pinned": "Pinned",
|
"Pinned": "Pinned",
|
||||||
"Recently updated": "Recently updated",
|
"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.",
|
"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",
|
"Name": "Name",
|
||||||
"Alphabetical": "Alphabetical",
|
"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",
|
"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.",
|
"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.",
|
"Public sharing is currently disabled in the team security settings.": "Public sharing is currently disabled in the team security settings.",
|
||||||
"Saving": "Saving",
|
"Saving": "Saving",
|
||||||
"Save": "Save",
|
"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",
|
"{{ groupName }} was added to the collection": "{{ groupName }} was added to the collection",
|
||||||
"Could not add user": "Could not add user",
|
"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?",
|
"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",
|
"Search people": "Search people",
|
||||||
"No people matching your search": "No people matching your search",
|
"No people matching your search": "No people matching your search",
|
||||||
"No people left to add": "No people left to add",
|
"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",
|
"Active <1></1> ago": "Active <1></1> ago",
|
||||||
"Never signed in": "Never signed in",
|
"Never signed in": "Never signed in",
|
||||||
"Invited": "Invited",
|
"Invited": "Invited",
|
||||||
"Admin": "Admin",
|
"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.",
|
"{{ userName }} was removed from the collection": "{{ userName }} was removed from the collection",
|
||||||
"Creating": "Creating",
|
"Could not remove user": "Could not remove user",
|
||||||
"Create": "Create",
|
"{{ 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",
|
"Hide contents": "Hide contents",
|
||||||
"Show contents": "Show contents",
|
"Show contents": "Show contents",
|
||||||
"Edit {{noun}}": "Edit {{noun}}",
|
"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?",
|
"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}}",
|
"Invite them to {{teamName}}": "Invite them to {{teamName}}",
|
||||||
"{{userName}} was removed from the group": "{{userName}} was removed from the group",
|
"{{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.",
|
"This group has no members.": "This group has no members.",
|
||||||
"Recently viewed": "Recently viewed",
|
"Recently viewed": "Recently viewed",
|
||||||
"Created by me": "Created by me",
|
"Created by me": "Created by me",
|
||||||
|
|||||||
Reference in New Issue
Block a user