Compare commits

...

18 Commits

Author SHA1 Message Date
Saumya Pandey 27ab73b0d3 fix: add space 2021-10-23 02:53:01 +05:30
Saumya Pandey c3cd72451d fix: remove use policy 2021-10-22 23:58:23 +05:30
Saumya Pandey 3f8b5b4be9 fix debounce search 2021-10-22 01:46:30 +05:30
Saumya Pandey 2251439dec update group policy to let user read group 2021-10-22 01:08:20 +05:30
Saumya Pandey 4c22d167bd use await 2021-10-22 00:52:16 +05:30
Saumya Pandey 6732cfca76 fix: don't change group-collection membership 2021-10-19 01:46:47 +05:30
Saumya Pandey 431617d4bd Update translations 2021-10-10 23:23:33 +05:30
Saumya Pandey 01b1ff65ff Add tests 2021-10-10 22:57:14 +05:30
Saumya Pandey f57b066b25 readonly in collections user is present 2021-10-10 22:57:14 +05:30
Saumya Pandey f32a61f193 Allow viewers to see groups 2021-10-10 22:57:14 +05:30
Saumya Pandey 6cc9b1a109 Add tests 2021-10-10 22:57:14 +05:30
Saumya Pandey 24a4f12095 Don't show invite option 2021-10-10 22:57:14 +05:30
Saumya Pandey 590f1481e2 Convert to functional component 2021-10-10 22:57:13 +05:30
Saumya Pandey af0be5bea6 Don't show create option 2021-10-10 22:57:13 +05:30
Saumya Pandey fa9edf5025 Convert to functional component 2021-10-10 22:57:13 +05:30
Saumya Pandey a74c16fb31 Update group policies 2021-10-10 22:57:13 +05:30
Saumya Pandey fd03582951 Handle isPrivate in group routes 2021-10-10 22:57:13 +05:30
Saumya Pandey 80d74b44ad Add isPrivate field 2021-10-10 22:57:13 +05:30
14 changed files with 426 additions and 221 deletions
+2
View File
@@ -4,12 +4,14 @@ import BaseModel from "./BaseModel";
class Group extends BaseModel {
id: string;
name: string;
isPrivate: boolean;
memberCount: number;
updatedAt: string;
toJS = () => {
return {
name: this.name,
isPrivate: this.isPrivate,
};
};
}
@@ -1,14 +1,9 @@
// @flow
import { debounce } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import CollectionGroupMembershipsStore from "stores/CollectionGroupMembershipsStore";
import GroupsStore from "stores/GroupsStore";
import ToastsStore from "stores/ToastsStore";
import Collection from "models/Collection";
import Group from "models/Group";
import GroupNew from "scenes/GroupNew";
@@ -21,132 +16,123 @@ import HelpText from "components/HelpText";
import Input from "components/Input";
import Modal from "components/Modal";
import PaginatedList from "components/PaginatedList";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
type Props = {
toasts: ToastsStore,
auth: AuthStore,
collection: Collection,
collectionGroupMemberships: CollectionGroupMembershipsStore,
groups: GroupsStore,
onSubmit: () => void,
t: TFunction,
};
@observer
class AddGroupsToCollection extends React.Component<Props> {
@observable newGroupModalOpen: boolean = false;
@observable query: string = "";
const AddGroupsToCollection = ({ collection, onSubmit }: Props) => {
const [newGroupModalOpen, setNewGroupModalOpen] = React.useState(false);
const [query, setQuery] = React.useState("");
const { groups, collectionGroupMemberships, policies } = useStores();
const { t } = useTranslation();
const { showToast } = useToasts();
const team = useCurrentTeam();
handleNewGroupModalOpen = () => {
this.newGroupModalOpen = true;
};
const can = policies.abilities(team.id);
const groupsExist = !!groups.orderedData.length;
handleNewGroupModalClose = () => {
this.newGroupModalOpen = false;
};
const debouncedFetch = React.useMemo(
() =>
debounce(async (query) => {
await groups.fetchPage({
query,
});
}, 250),
[groups]
);
handleFilter = (ev: SyntheticInputEvent<>) => {
this.query = ev.target.value;
this.debouncedFetch();
};
debouncedFetch = debounce(() => {
this.props.groups.fetchPage({
query: this.query,
});
}, 250);
handleAddGroup = (group: Group) => {
const { t } = this.props;
const handleFilter = React.useCallback(
(ev: SyntheticInputEvent<>) => {
setQuery(ev.target.value);
debouncedFetch(ev.target.value);
},
[debouncedFetch]
);
const handleAddGroup = async (group: Group) => {
try {
this.props.collectionGroupMemberships.create({
collectionId: this.props.collection.id,
await collectionGroupMemberships.create({
collectionId: collection.id,
groupId: group.id,
permission: "read_write",
});
this.props.toasts.showToast(
showToast(
t("{{ groupName }} was added to the collection", {
groupName: group.name,
}),
{ type: "success" }
);
} catch (err) {
this.props.toasts.showToast(t("Could not add user"), { type: "error" });
showToast(t("Could not add group"), { type: "error" });
console.error(err);
}
};
render() {
const { groups, collection, auth, t } = this.props;
const { user, team } = auth;
if (!user || !team) return null;
return (
<Flex column>
return (
<Flex column>
{can.createGroup && (
<HelpText>
{t("Cant find the group youre looking for?")}{" "}
<ButtonLink onClick={this.handleNewGroupModalOpen}>
<ButtonLink onClick={() => setNewGroupModalOpen(true)}>
{t("Create a group")}
</ButtonLink>
.
</HelpText>
)}
{groupsExist && (
<Input
type="search"
placeholder={`${t("Search by group name")}`}
value={this.query}
onChange={this.handleFilter}
value={query}
onChange={handleFilter}
label={t("Search groups")}
labelHidden
flex
/>
<PaginatedList
empty={
this.query ? (
<Empty>{t("No groups matching your search")}</Empty>
) : (
<Empty>{t("No groups left to add")}</Empty>
)
}
items={groups.notInCollection(collection.id, this.query)}
fetch={this.query ? undefined : groups.fetchPage}
renderItem={(item) => (
<GroupListItem
key={item.id}
group={item}
showFacepile
renderActions={() => (
<ButtonWrap>
<Button onClick={() => this.handleAddGroup(item)} neutral>
{t("Add")}
</Button>
</ButtonWrap>
)}
/>
)}
/>
<Modal
title={t("Create a group")}
onRequestClose={this.handleNewGroupModalClose}
isOpen={this.newGroupModalOpen}
>
<GroupNew onSubmit={this.handleNewGroupModalClose} />
</Modal>
</Flex>
);
}
}
)}
<PaginatedList
empty={
query ? (
<Empty>{t("No groups matching your search")}</Empty>
) : groupsExist ? (
<Empty>{t("No groups left to add")}</Empty>
) : (
<Empty>{t("No groups found to add")}</Empty>
)
}
items={groups.notInCollection(collection.id, query)}
fetch={query ? undefined : groups.fetchPage}
renderItem={(item) => (
<GroupListItem
key={item.id}
group={item}
showFacepile
renderActions={() => (
<ButtonWrap>
<Button onClick={() => handleAddGroup(item)} neutral>
{t("Add")}
</Button>
</ButtonWrap>
)}
/>
)}
/>
<Modal
title={t("Create a group")}
onRequestClose={() => setNewGroupModalOpen(false)}
isOpen={newGroupModalOpen}
>
<GroupNew onSubmit={() => setNewGroupModalOpen(false)} />
</Modal>
</Flex>
);
};
const ButtonWrap = styled.div`
margin-left: 6px;
`;
export default withTranslation()<AddGroupsToCollection>(
inject(
"auth",
"groups",
"collectionGroupMemberships",
"toasts"
)(AddGroupsToCollection)
);
export default observer(AddGroupsToCollection);
@@ -1,13 +1,8 @@
// @flow
import { debounce } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import AuthStore from "stores/AuthStore";
import MembershipsStore from "stores/MembershipsStore";
import ToastsStore from "stores/ToastsStore";
import UsersStore from "stores/UsersStore";
import { useTranslation } from "react-i18next";
import Collection from "models/Collection";
import User from "models/User";
import Invite from "scenes/Invite";
@@ -19,116 +14,109 @@ import Input from "components/Input";
import Modal from "components/Modal";
import PaginatedList from "components/PaginatedList";
import MemberListItem from "./components/MemberListItem";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
type Props = {
toasts: ToastsStore,
auth: AuthStore,
collection: Collection,
memberships: MembershipsStore,
users: UsersStore,
onSubmit: () => void,
t: TFunction,
};
@observer
class AddPeopleToCollection extends React.Component<Props> {
@observable inviteModalOpen: boolean = false;
@observable query: string = "";
const AddPeopleToCollection = ({ collection, onSubmit }: Props) => {
const [inviteModalOpen, setInviteModalOpen] = React.useState(false);
const [query, setQuery] = React.useState("");
const team = useCurrentTeam();
const { users, memberships, policies } = useStores();
const { t } = useTranslation();
const { showToast } = useToasts();
const can = policies.abilities(team.id);
handleInviteModalOpen = () => {
this.inviteModalOpen = true;
};
const debouncedFetch = React.useMemo(
() =>
debounce(async (query) => {
await users.fetchPage({
query,
});
}, 250),
[users]
);
handleInviteModalClose = () => {
this.inviteModalOpen = false;
};
const handleFilter = React.useCallback(
(ev: SyntheticInputEvent<>) => {
setQuery(ev.target.value);
debouncedFetch(ev.target.value);
},
[debouncedFetch]
);
handleFilter = (ev: SyntheticInputEvent<>) => {
this.query = ev.target.value;
this.debouncedFetch();
};
debouncedFetch = debounce(() => {
this.props.users.fetchPage({
query: this.query,
});
}, 250);
handleAddUser = (user: User) => {
const { t } = this.props;
const handleAddUser = (user: User) => {
try {
this.props.memberships.create({
collectionId: this.props.collection.id,
memberships.create({
collectionId: collection.id,
userId: user.id,
permission: "read_write",
});
this.props.toasts.showToast(
showToast(
t("{{ userName }} was added to the collection", {
userName: user.name,
}),
{ type: "success" }
);
} catch (err) {
this.props.toasts.showToast(t("Could not add user"), { type: "error" });
showToast(t("Could not add user"), { type: "error" });
}
};
render() {
const { users, collection, auth, t } = this.props;
const { user, team } = auth;
if (!user || !team) return null;
return (
<Flex column>
return (
<Flex column>
{can.inviteUser && (
<HelpText>
{t("Need to add someone whos not yet on the team yet?")}{" "}
<ButtonLink onClick={this.handleInviteModalOpen}>
<ButtonLink onClick={() => setInviteModalOpen(true)}>
{t("Invite people to {{ teamName }}", { teamName: team.name })}
</ButtonLink>
.
</HelpText>
)}
<Input
type="search"
placeholder={`${t("Search by name")}`}
value={this.query}
onChange={this.handleFilter}
label={t("Search people")}
autoFocus
labelHidden
flex
/>
<PaginatedList
empty={
this.query ? (
<Empty>{t("No people matching your search")}</Empty>
) : (
<Empty>{t("No people left to add")}</Empty>
)
}
items={users.notInCollection(collection.id, this.query)}
fetch={this.query ? undefined : users.fetchPage}
renderItem={(item) => (
<MemberListItem
key={item.id}
user={item}
onAdd={() => this.handleAddUser(item)}
canEdit
/>
)}
/>
<Modal
title={t("Invite people")}
onRequestClose={this.handleInviteModalClose}
isOpen={this.inviteModalOpen}
>
<Invite onSubmit={this.handleInviteModalClose} />
</Modal>
</Flex>
);
}
}
<Input
type="search"
placeholder={`${t("Search by name")}`}
value={query}
onChange={handleFilter}
label={t("Search people")}
autoFocus
labelHidden
flex
/>
<PaginatedList
empty={
query ? (
<Empty>{t("No people matching your search")}</Empty>
) : (
<Empty>{t("No people left to add")}</Empty>
)
}
items={users.notInCollection(collection.id, query)}
fetch={query ? undefined : users.fetchPage}
renderItem={(item) => (
<MemberListItem
key={item.id}
user={item}
onAdd={() => handleAddUser(item)}
canEdit
/>
)}
/>
<Modal
title={t("Invite people")}
onRequestClose={() => setInviteModalOpen(false)}
isOpen={inviteModalOpen}
>
<Invite onSubmit={() => setInviteModalOpen(false)} />
</Modal>
</Flex>
);
};
export default withTranslation()<AddPeopleToCollection>(
inject("auth", "users", "memberships", "toasts")(AddPeopleToCollection)
);
export default observer(AddPeopleToCollection);
+37 -2
View File
@@ -2,11 +2,13 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import styled from "styled-components";
import Group from "models/Group";
import Button from "components/Button";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import Input from "components/Input";
import Switch from "components/Switch";
import useToasts from "hooks/useToasts";
type Props = {
@@ -18,6 +20,7 @@ function GroupEdit({ group, onSubmit }: Props) {
const { showToast } = useToasts();
const { t } = useTranslation();
const [name, setName] = React.useState(group.name);
const [isPrivate, setIsPrivate] = React.useState(group.isPrivate);
const [isSaving, setIsSaving] = React.useState();
const handleSubmit = React.useCallback(
@@ -26,7 +29,7 @@ function GroupEdit({ group, onSubmit }: Props) {
setIsSaving(true);
try {
await group.save({ name: name });
await group.save({ name, isPrivate });
onSubmit();
} catch (err) {
showToast(err.message, { type: "error" });
@@ -34,7 +37,7 @@ function GroupEdit({ group, onSubmit }: Props) {
setIsSaving(false);
}
},
[group, onSubmit, showToast, name]
[group, isPrivate, name, onSubmit, showToast]
);
const handleNameChange = React.useCallback((ev: SyntheticInputEvent<*>) => {
@@ -60,6 +63,21 @@ function GroupEdit({ group, onSubmit }: Props) {
flex
/>
</Flex>
<SwitchWrapper>
<Switch
id="isPrivate"
label={t("Access to group")}
onChange={() => setIsPrivate((prev) => !prev)}
checked={!isPrivate}
/>
<SwitchLabel>
<SwitchText>
{isPrivate
? t("Only members present in the group know about the group")
: t("Everyone in the team can view the group")}
</SwitchText>
</SwitchLabel>
</SwitchWrapper>
<Button type="submit" disabled={isSaving || !name}>
{isSaving ? `${t("Saving")}` : t("Save")}
@@ -68,4 +86,21 @@ function GroupEdit({ group, onSubmit }: Props) {
);
}
const SwitchWrapper = styled.div`
margin: 20px 0;
`;
const SwitchLabel = styled(Flex)`
flex-align: center;
svg {
flex-shrink: 0;
}
`;
const SwitchText = styled(HelpText)`
margin: 0;
font-size: 15px;
`;
export default observer(GroupEdit);
+38 -1
View File
@@ -2,6 +2,7 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import styled from "styled-components";
import Group from "models/Group";
import GroupMembers from "scenes/GroupMembers";
import Button from "components/Button";
@@ -9,6 +10,7 @@ import Flex from "components/Flex";
import HelpText from "components/HelpText";
import Input from "components/Input";
import Modal from "components/Modal";
import Switch from "components/Switch";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
@@ -22,6 +24,7 @@ function GroupNew({ onSubmit }: Props) {
const { showToast } = useToasts();
const [name, setName] = React.useState();
const [isSaving, setIsSaving] = React.useState();
const [isPrivate, setIsPrivate] = React.useState(true);
const [group, setGroup] = React.useState();
const handleSubmit = async (ev: SyntheticEvent<>) => {
@@ -29,7 +32,8 @@ function GroupNew({ onSubmit }: Props) {
setIsSaving(true);
const group = new Group(
{
name: name,
name,
isPrivate,
},
groups
);
@@ -72,6 +76,22 @@ function GroupNew({ onSubmit }: Props) {
<Trans>Youll be able to add people to the group next.</Trans>
</HelpText>
<SwitchWrapper>
<Switch
id="isPrivate"
label={t("Access to group")}
onChange={() => setIsPrivate((prev) => !prev)}
checked={!isPrivate}
/>
<SwitchLabel>
<SwitchText>
{isPrivate
? t("Only members present in the group know about the group")
: t("Everyone in the team can view the group")}
</SwitchText>
</SwitchLabel>
</SwitchWrapper>
<Button type="submit" disabled={isSaving || !name}>
{isSaving ? `${t("Creating")}` : t("Continue")}
</Button>
@@ -87,4 +107,21 @@ function GroupNew({ onSubmit }: Props) {
);
}
const SwitchWrapper = styled.div`
margin: 20px 0;
`;
const SwitchLabel = styled(Flex)`
flex-align: center;
svg {
flex-shrink: 0;
}
`;
const SwitchText = styled(HelpText)`
margin: 0;
font-size: 15px;
`;
export default observer(GroupNew);
@@ -0,0 +1,14 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('groups', 'isPrivate', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('groups', 'isPrivate');
}
};
+25
View File
@@ -18,6 +18,11 @@ const Group = sequelize.define(
type: DataTypes.STRING,
allowNull: false,
},
isPrivate: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
},
{
timestamps: true,
@@ -70,6 +75,26 @@ Group.associate = (models) => {
],
order: [["name", "ASC"]],
});
Group.addScope("withCollection", (userId) => ({
include: [
{
association: "groupMemberships",
required: false,
},
{
model: models.CollectionGroup,
as: "collectionGroupMemberships",
required: false,
include: {
model: models.Collection.scope({
method: ["withMembership", userId],
}),
as: "collection",
},
},
],
order: [["name", "ASC"]],
}));
};
// Cascade deletes to group and collection relations
+11 -2
View File
@@ -3,7 +3,7 @@ import { AdminRequiredError } from "../errors";
import { Group, User, Team } from "../models";
import policy from "./policy";
const { allow } = policy;
const { allow, can } = policy;
allow(User, "createGroup", Team, (actor, team) => {
if (!team || actor.isViewer || actor.teamId !== team.id) return false;
@@ -13,10 +13,19 @@ allow(User, "createGroup", Team, (actor, team) => {
allow(User, "read", Group, (actor, group) => {
if (!group || actor.teamId !== group.teamId) return false;
if (actor.isAdmin) return true;
if (actor.isAdmin || !group.isPrivate) return true;
if (group.groupMemberships.filter((gm) => gm.userId === actor.id).length) {
return true;
}
if (
group.collectionGroupMemberships &&
group.collectionGroupMemberships.some((membership) =>
can(actor, "read", membership.collection)
)
)
return true;
return false;
});
+1
View File
@@ -5,6 +5,7 @@ export default function present(group: Group) {
return {
id: group.id,
name: group.name,
isPrivate: group.isPrivate,
memberCount: group.groupMemberships.length,
createdAt: group.createdAt,
updatedAt: group.updatedAt,
+19 -12
View File
@@ -157,14 +157,18 @@ router.post("collections.add_group", auth(), async (ctx) => {
const { id, groupId, permission = "read_write" } = ctx.body;
ctx.assertUuid(id, "id is required");
ctx.assertUuid(groupId, "groupId is required");
const user = ctx.state.user;
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
method: ["withMembership", user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
authorize(user, "update", collection);
const group = await Group.findByPk(groupId);
authorize(ctx.state.user, "read", group);
const group = await Group.scope({
method: ["withCollection", user.id],
}).findByPk(groupId);
authorize(user, "read", group);
let membership = await CollectionGroup.findOne({
where: {
@@ -178,9 +182,9 @@ router.post("collections.add_group", auth(), async (ctx) => {
collectionId: id,
groupId,
permission,
createdById: ctx.state.user.id,
createdById: user.id,
});
} else if (permission) {
} else {
membership.permission = permission;
await membership.save();
}
@@ -189,7 +193,7 @@ router.post("collections.add_group", auth(), async (ctx) => {
name: "collections.add_group",
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
actorId: user.id,
data: { name: group.name, groupId },
ip: ctx.request.ip,
});
@@ -207,14 +211,17 @@ router.post("collections.remove_group", auth(), async (ctx) => {
const { id, groupId } = ctx.body;
ctx.assertUuid(id, "id is required");
ctx.assertUuid(groupId, "groupId is required");
const user = ctx.state.user;
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
method: ["withMembership", user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
authorize(user, "update", collection);
const group = await Group.findByPk(groupId);
authorize(ctx.state.user, "read", group);
const group = await Group.scope({
method: ["withCollection", user.id],
}).findByPk(groupId);
authorize(user, "read", group);
await collection.removeGroup(group);
@@ -222,7 +229,7 @@ router.post("collections.remove_group", auth(), async (ctx) => {
name: "collections.remove_group",
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
actorId: user.id,
data: { name: group.name, groupId },
ip: ctx.request.ip,
});
+20 -9
View File
@@ -36,6 +36,7 @@ router.post("groups.list", auth(), pagination(), async (ctx) => {
if (!user.isAdmin) {
groups = groups.filter(
(group) =>
!group.isPrivate ||
group.groupMemberships.filter((gm) => gm.userId === user.id).length
);
}
@@ -62,7 +63,10 @@ router.post("groups.info", auth(), async (ctx) => {
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const group = await Group.findByPk(id);
const group = await Group.scope({
method: ["withCollection", user.id],
}).findByPk(id);
authorize(user, "read", group);
ctx.body = {
@@ -72,14 +76,13 @@ router.post("groups.info", auth(), async (ctx) => {
});
router.post("groups.create", auth(), async (ctx) => {
const { name } = ctx.body;
const { name, isPrivate } = ctx.body;
ctx.assertPresent(name, "name is required");
const user = ctx.state.user;
authorize(user, "createGroup", user.team);
let group = await Group.create({
name,
isPrivate: isPrivate ?? true,
teamId: user.teamId,
createdById: user.id,
});
@@ -92,7 +95,7 @@ router.post("groups.create", auth(), async (ctx) => {
actorId: user.id,
teamId: user.teamId,
modelId: group.id,
data: { name: group.name },
data: { name: group.name, isPrivate: group.isPrivate },
ip: ctx.request.ip,
});
@@ -103,8 +106,9 @@ router.post("groups.create", auth(), async (ctx) => {
});
router.post("groups.update", auth(), async (ctx) => {
const { id, name } = ctx.body;
const { id, name, isPrivate } = ctx.body;
ctx.assertPresent(name, "name is required");
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
@@ -114,6 +118,10 @@ router.post("groups.update", auth(), async (ctx) => {
group.name = name;
if (isPrivate !== undefined) {
group.isPrivate = !!isPrivate;
}
if (group.changed()) {
await group.save();
await Event.create({
@@ -121,7 +129,7 @@ router.post("groups.update", auth(), async (ctx) => {
teamId: user.teamId,
actorId: user.id,
modelId: group.id,
data: { name },
data: { name, isPrivate },
ip: ctx.request.ip,
});
}
@@ -147,7 +155,7 @@ router.post("groups.delete", auth(), async (ctx) => {
actorId: user.id,
modelId: group.id,
teamId: group.teamId,
data: { name: group.name },
data: { name: group.name, isPrivate: group.isPrivate },
ip: ctx.request.ip,
});
@@ -161,7 +169,10 @@ router.post("groups.memberships", auth(), pagination(), async (ctx) => {
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const group = await Group.findByPk(id);
const group = await Group.scope({
method: ["withCollection", user.id],
}).findByPk(id);
authorize(user, "read", group);
let userWhere;
+90 -7
View File
@@ -23,6 +23,7 @@ describe("#groups.create", () => {
expect(res.status).toEqual(200);
expect(body.data.name).toEqual(name);
expect(body.data.isPrivate).toEqual(true);
});
});
@@ -58,16 +59,21 @@ describe("#groups.update", () => {
});
describe("when user is admin", () => {
let user, group;
let admin, group;
beforeEach(async () => {
user = await buildAdmin();
group = await buildGroup({ teamId: user.teamId });
admin = await buildAdmin();
group = await buildGroup({ teamId: admin.teamId });
});
it("allows admin to edit a group", async () => {
const res = await server.post("/api/groups.update", {
body: { token: user.getJwtToken(), id: group.id, name: "Test" },
body: {
token: admin.getJwtToken(),
id: group.id,
name: "Test",
isPrivate: false,
},
});
const events = await Event.findAll();
@@ -76,11 +82,12 @@ describe("#groups.update", () => {
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.name).toBe("Test");
expect(body.data.isPrivate).toBe(false);
});
it("does not create an event if the update is a noop", async () => {
const res = await server.post("/api/groups.update", {
body: { token: user.getJwtToken(), id: group.id, name: group.name },
body: { token: admin.getJwtToken(), id: group.id, name: group.name },
});
const events = await Event.findAll();
@@ -93,13 +100,13 @@ describe("#groups.update", () => {
it("fails with validation error when name already taken", async () => {
await buildGroup({
teamId: user.teamId,
teamId: admin.teamId,
name: "test",
});
const res = await server.post("/api/groups.update", {
body: {
token: user.getJwtToken(),
token: admin.getJwtToken(),
id: group.id,
name: "TEST",
},
@@ -145,6 +152,48 @@ describe("#groups.list", () => {
expect(body.policies[0].abilities.read).toEqual(true);
});
it("should return groups with memberships if isPrivate is false", async () => {
const user = await buildUser();
const admin = await buildAdmin({ teamId: user.teamId });
const group = await buildGroup({ teamId: user.teamId, isPrivate: false });
await group.addUser(admin, { through: { createdById: admin.id } });
const res = await server.post("/api/groups.list", {
body: { token: user.getJwtToken() },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data["groups"].length).toEqual(1);
expect(body.data["groups"][0].id).toEqual(group.id);
expect(body.data["groupMemberships"].length).toEqual(1);
expect(body.data["groupMemberships"][0].groupId).toEqual(group.id);
expect(body.data["groupMemberships"][0].user.id).toEqual(admin.id);
expect(body.policies.length).toEqual(1);
expect(body.policies[0].abilities.read).toEqual(true);
});
it("should not return groups with memberships to non-member, if isPrivate is true", async () => {
const user = await buildUser();
await buildGroup({ teamId: user.teamId });
const res = await server.post("/api/groups.list", {
body: { token: user.getJwtToken() },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data["groups"].length).toEqual(0);
expect(body.data["groupMemberships"].length).toEqual(0);
expect(body.policies.length).toEqual(0);
});
it("should return groups when membership user is deleted", async () => {
const me = await buildUser();
const user = await buildUser({ teamId: me.teamId });
@@ -188,6 +237,20 @@ describe("#groups.info", () => {
expect(body.data.id).toEqual(group.id);
});
it("should return group info to non-member, if group isPrivate is false", async () => {
const user = await buildUser();
const group = await buildGroup({ teamId: user.teamId, isPrivate: false });
const res = await server.post("/api/groups.info", {
body: { token: user.getJwtToken(), id: group.id },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.id).toEqual(group.id);
});
it("should return group if member", async () => {
const user = await buildUser();
const group = await buildGroup({ teamId: user.teamId });
@@ -297,6 +360,26 @@ describe("#groups.memberships", () => {
expect(body.data.groupMemberships[0].user.id).toEqual(user.id);
});
it("should return members in a group to non-member, if isPrivate is false", async () => {
const user = await buildUser();
const admin = await buildAdmin({ teamId: user.teamId });
const group = await buildGroup({ teamId: admin.teamId, isPrivate: false });
await group.addUser(admin, { through: { createdById: admin.id } });
const res = await server.post("/api/groups.memberships", {
body: { token: user.getJwtToken(), id: group.id },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.users.length).toEqual(1);
expect(body.data.users[0].id).toEqual(admin.id);
expect(body.data.groupMemberships.length).toEqual(1);
expect(body.data.groupMemberships[0].user.id).toEqual(admin.id);
});
it("should allow filtering members in group by name", async () => {
const user = await buildUser();
const user2 = await buildUser({ name: "Won't find" });
+1
View File
@@ -206,6 +206,7 @@ export async function buildGroup(overrides: Object = {}) {
return Group.create({
name: `Test Group ${count}`,
createdById: overrides.userId,
isPrivate: true,
...overrides,
});
}
+7 -1
View File
@@ -284,6 +284,7 @@
"You can edit the name and other details at any time, however doing so often might confuse your team mates.": "You can edit the name and other details at any time, however doing so often might confuse your team mates.",
"Name": "Name",
"Alphabetical": "Alphabetical",
"Sort": "Sort",
"Saving": "Saving",
"Save": "Save",
"Export started, you will receive an email when its complete.": "Export started, you will receive an email when its complete.",
@@ -297,15 +298,17 @@
"Creating": "Creating",
"Create": "Create",
"{{ groupName }} was added to the collection": "{{ groupName }} was added to the collection",
"Could not add user": "Could not add user",
"Could not add group": "Could not add group",
"Cant find the group youre looking for?": "Cant find the group youre looking for?",
"Create a group": "Create a group",
"Search by group name": "Search by group name",
"Search groups": "Search groups",
"No groups matching your search": "No groups matching your search",
"No groups left to add": "No groups left to add",
"No groups found to add": "No groups found to add",
"Add": "Add",
"{{ userName }} was added to the collection": "{{ userName }} was added to the collection",
"Could not add user": "Could not add user",
"Need to add someone whos not yet on the team yet?": "Need to add someone whos not yet on the team yet?",
"Invite people to {{ teamName }}": "Invite people to {{ teamName }}",
"Search by name": "Search by name",
@@ -400,6 +403,9 @@
"A team admin (<em>{{ suspendedContactEmail }}</em>) has suspended your account. To re-activate your account, please reach out to them directly.": "A team admin (<em>{{ suspendedContactEmail }}</em>) has suspended your account. To re-activate your account, please reach out to them directly.",
"Are you sure about that? Deleting the <em>{{groupName}}</em> group will cause its members to lose access to collections and documents that it is associated with.": "Are you sure about that? Deleting the <em>{{groupName}}</em> group will cause its members to lose access to collections and documents that it is associated with.",
"You can edit the name of this group at any time, however doing so too often might confuse your team mates.": "You can edit the name of this group at any time, however doing so too often might confuse your team mates.",
"Access to group": "Access to group",
"Only members present in the group know about the group": "Only members present in the group know about the group",
"Everyone in the team can view the group": "Everyone in the team can view the group",
"{{userName}} was added to the group": "{{userName}} was added to the group",
"Add team members below to give them access to the group. Need to add someone whos not yet on the team yet?": "Add team members below to give them access to the group. Need to add someone whos not yet on the team yet?",
"Invite them to {{teamName}}": "Invite them to {{teamName}}",