chore: Remove old collection permissions UI (#6995)

This commit is contained in:
Tom Moor
2024-06-05 06:33:39 -04:00
committed by GitHub
parent 7eb6dcf00b
commit 1d97a6c10b
8 changed files with 20 additions and 853 deletions

View File

@@ -1,132 +0,0 @@
import debounce from "lodash/debounce";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import Collection from "~/models/Collection";
import Group from "~/models/Group";
import GroupNew from "~/scenes/GroupNew";
import Button from "~/components/Button";
import ButtonLink from "~/components/ButtonLink";
import Empty from "~/components/Empty";
import Flex from "~/components/Flex";
import GroupListItem from "~/components/GroupListItem";
import Input from "~/components/Input";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
type Props = {
collection: Collection;
};
function AddGroupsToCollection(props: Props) {
const { collection } = props;
const [newGroupModalOpen, handleNewGroupModalOpen, handleNewGroupModalClose] =
useBoolean(false);
const [query, setQuery] = React.useState("");
const team = useCurrentTeam();
const { collectionGroupMemberships, groups, policies } = useStores();
const { t } = useTranslation();
const { fetchPage: fetchGroups } = groups;
const can = policies.abilities(team.id);
const debouncedFetch = React.useMemo(
() => debounce((query) => fetchGroups({ query }), 250),
[fetchGroups]
);
const handleFilter = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
const updatedQuery = ev.target.value;
setQuery(updatedQuery);
void debouncedFetch(updatedQuery);
},
[debouncedFetch]
);
const handleAddGroup = async (group: Group) => {
try {
await collectionGroupMemberships.create({
collectionId: collection.id,
groupId: group.id,
});
toast.success(
t("{{ groupName }} was added to the collection", {
groupName: group.name,
})
);
} catch (err) {
toast.error(t("Could not add user"));
}
};
return (
<Flex column>
{can.createGroup ? (
<Text as="p" type="secondary">
{t("Cant find the group youre looking for?")}{" "}
<ButtonLink onClick={handleNewGroupModalOpen}>
{t("Create a group")}
</ButtonLink>
.
</Text>
) : null}
<Input
type="search"
placeholder={`${t("Search by group name")}`}
value={query}
onChange={handleFilter}
label={t("Search groups")}
labelHidden
flex
/>
<PaginatedList
empty={
query ? (
<Empty>{t("No groups matching your search")}</Empty>
) : (
<Empty>{t("No groups left to add")}</Empty>
)
}
items={groups.notInCollection(collection.id, query)}
fetch={query ? undefined : fetchGroups}
renderItem={(item: Group) => (
<GroupListItem
key={item.id}
group={item}
showFacepile
renderActions={() => (
<ButtonWrap>
<Button onClick={() => handleAddGroup(item)} neutral>
{t("Add")}
</Button>
</ButtonWrap>
)}
/>
)}
/>
{can.createGroup ? (
<Modal
title={t("Create a group")}
onRequestClose={handleNewGroupModalClose}
isOpen={newGroupModalOpen}
>
<GroupNew onSubmit={handleNewGroupModalClose} />
</Modal>
) : null}
</Flex>
);
}
const ButtonWrap = styled.div`
margin-left: 6px;
`;
export default observer(AddGroupsToCollection);

View File

@@ -1,130 +0,0 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { CollectionPermission, UserRole } from "@shared/types";
import Collection from "~/models/Collection";
import User from "~/models/User";
import Invite from "~/scenes/Invite";
import Avatar from "~/components/Avatar";
import { AvatarSize } from "~/components/Avatar/Avatar";
import ButtonLink from "~/components/ButtonLink";
import Empty from "~/components/Empty";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import useThrottledCallback from "~/hooks/useThrottledCallback";
import MemberListItem from "./components/MemberListItem";
type Props = {
collection: Collection;
};
function AddPeopleToCollection({ collection }: Props) {
const { memberships, users } = useStores();
const team = useCurrentTeam();
const { t } = useTranslation();
const can = usePolicy(team);
const [inviteModalOpen, setInviteModalOpen, setInviteModalClosed] =
useBoolean();
const [query, setQuery] = React.useState("");
const debouncedFetch = useThrottledCallback(
(query) =>
users.fetchPage({
query,
}),
250
);
const handleFilter = (ev: React.ChangeEvent<HTMLInputElement>) => {
setQuery(ev.target.value);
void debouncedFetch(ev.target.value);
};
const handleAddUser = async (user: User) => {
try {
await memberships.create({
permission:
user.role === UserRole.Viewer || user.role === UserRole.Guest
? CollectionPermission.Read
: CollectionPermission.ReadWrite,
collectionId: collection.id,
userId: user.id,
});
toast.success(
t("{{ userName }} was added to the collection", {
userName: user.name,
}),
{
icon: <Avatar model={user} size={AvatarSize.Toast} />,
}
);
} catch (err) {
toast.error(t("Could not add user"));
}
};
return (
<Flex column>
<Text as="p" type="secondary">
{t("Need to add someone whos not on the team yet?")}{" "}
{can.inviteUser ? (
<ButtonLink onClick={setInviteModalOpen}>
{t("Invite people to {{ teamName }}", {
teamName: team.name,
})}
</ButtonLink>
) : (
t("Ask an admin to invite them first")
)}
.
</Text>
<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: User) => (
<MemberListItem
key={item.id}
user={item}
onAdd={() => handleAddUser(item)}
canEdit
/>
)}
/>
<Modal
title={t("Invite people")}
onRequestClose={setInviteModalClosed}
isOpen={inviteModalOpen}
>
<Invite onSubmit={setInviteModalClosed} />
</Modal>
</Flex>
);
}
export default observer(AddPeopleToCollection);

View File

@@ -1,59 +0,0 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { CollectionPermission } from "@shared/types";
import CollectionGroupMembership from "~/models/CollectionGroupMembership";
import Group from "~/models/Group";
import GroupListItem from "~/components/GroupListItem";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import CollectionGroupMemberMenu from "~/menus/CollectionGroupMemberMenu";
type Props = {
group: Group;
collectionGroupMembership: CollectionGroupMembership | null | undefined;
onUpdate: (permission: CollectionPermission) => void;
onRemove: () => void;
};
const CollectionGroupMemberListItem = ({
group,
collectionGroupMembership,
onUpdate,
onRemove,
}: Props) => {
const { t } = useTranslation();
return (
<GroupListItem
group={group}
showAvatar
renderActions={({ openMembersModal }) => (
<>
<InputMemberPermissionSelect
value={collectionGroupMembership?.permission}
onChange={onUpdate}
permissions={[
{
label: t("View only"),
value: CollectionPermission.Read,
},
{
label: t("Can edit"),
value: CollectionPermission.ReadWrite,
},
{
label: t("Admin"),
value: CollectionPermission.Admin,
},
]}
/>
<CollectionGroupMemberMenu
onMembers={openMembersModal}
onRemove={onRemove}
/>
</>
)}
/>
);
};
export default CollectionGroupMemberListItem;

View File

@@ -1,92 +0,0 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { CollectionPermission } from "@shared/types";
import Membership from "~/models/Membership";
import User from "~/models/User";
import UserMembership from "~/models/UserMembership";
import Avatar from "~/components/Avatar";
import Badge from "~/components/Badge";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import ListItem from "~/components/List/Item";
import Time from "~/components/Time";
import MemberMenu from "~/menus/MemberMenu";
type Props = {
user: User;
membership?: Membership | UserMembership | undefined;
canEdit: boolean;
onAdd?: () => void;
onRemove?: () => void;
onUpdate?: (permission: CollectionPermission) => void;
};
const MemberListItem = ({
user,
membership,
onRemove,
onUpdate,
onAdd,
canEdit,
}: Props) => {
const { t } = useTranslation();
return (
<ListItem
title={user.name}
subtitle={
<>
{user.lastActiveAt ? (
<Trans>
Active <Time dateTime={user.lastActiveAt} /> ago
</Trans>
) : (
t("Never signed in")
)}
{user.isInvited && <Badge>{t("Invited")}</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
</>
}
image={<Avatar model={user} size={32} />}
actions={
<Flex align="center" gap={8}>
{onUpdate && (
<InputMemberPermissionSelect
permissions={[
{
label: t("View only"),
value: CollectionPermission.Read,
},
{
label: t("Can edit"),
value: CollectionPermission.ReadWrite,
},
{
label: t("Admin"),
value: CollectionPermission.Admin,
},
]}
value={membership?.permission}
onChange={onUpdate}
disabled={!canEdit}
/>
)}
{canEdit && (
<>
{onRemove && <MemberMenu user={user} onRemove={onRemove} />}
{onAdd && (
<Button onClick={onAdd} neutral>
{t("Add")}
</Button>
)}
</>
)}
</Flex>
}
/>
);
};
export default observer(MemberListItem);

View File

@@ -1,49 +0,0 @@
import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
import Badge from "~/components/Badge";
import Button from "~/components/Button";
import ListItem from "~/components/List/Item";
import Time from "~/components/Time";
type Props = {
user: User;
canEdit: boolean;
onAdd: () => void;
};
const UserListItem = ({ user, onAdd, canEdit }: Props) => {
const { t } = useTranslation();
return (
<ListItem
title={user.name}
image={<Avatar model={user} size={32} />}
subtitle={
<>
{user.lastActiveAt ? (
<Trans>
Active <Time dateTime={user.lastActiveAt} /> ago
</Trans>
) : (
t("Never signed in")
)}
{user.isInvited && <Badge>{t("Invited")}</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
</>
}
actions={
canEdit ? (
<Button type="button" onClick={onAdd} icon={<PlusIcon />} neutral>
{t("Add")}
</Button>
) : undefined
}
/>
);
};
export default observer(UserListItem);

View File

@@ -1,334 +0,0 @@
import invariant from "invariant";
import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { CollectionPermission } from "@shared/types";
import Group from "~/models/Group";
import User from "~/models/User";
import Button from "~/components/Button";
import Divider from "~/components/Divider";
import Flex from "~/components/Flex";
import InputSelectPermission from "~/components/InputSelectPermission";
import Labeled from "~/components/Labeled";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import AddGroupsToCollection from "./AddGroupsToCollection";
import AddPeopleToCollection from "./AddPeopleToCollection";
import CollectionGroupMemberListItem from "./components/CollectionGroupMemberListItem";
import MemberListItem from "./components/MemberListItem";
type Props = {
collectionId: string;
};
function CollectionPermissions({ collectionId }: Props) {
const { t } = useTranslation();
const user = useCurrentUser();
const {
collections,
memberships,
collectionGroupMemberships,
users,
groups,
auth,
} = useStores();
const collection = collections.get(collectionId);
invariant(collection, "Collection not found");
const [addGroupModalOpen, handleAddGroupModalOpen, handleAddGroupModalClose] =
useBoolean();
const [
addMemberModalOpen,
handleAddMemberModalOpen,
handleAddMemberModalClose,
] = useBoolean();
const handleRemoveUser = React.useCallback(
async (user) => {
try {
await memberships.delete({
collectionId: collection.id,
userId: user.id,
});
toast.success(
t(`{{ userName }} was removed from the collection`, {
userName: user.name,
})
);
} catch (err) {
toast.error(t("Could not remove user"));
}
},
[memberships, collection, t]
);
const handleUpdateUser = React.useCallback(
async (user, permission) => {
try {
await memberships.create({
collectionId: collection.id,
userId: user.id,
permission,
});
toast.success(
t(`{{ userName }} permissions were updated`, {
userName: user.name,
})
);
} catch (err) {
toast.error(t("Could not update user"));
}
},
[memberships, collection, t]
);
const handleRemoveGroup = React.useCallback(
async (group) => {
try {
await collectionGroupMemberships.delete({
collectionId: collection.id,
groupId: group.id,
});
toast.success(
t(`The {{ groupName }} group was removed from the collection`, {
groupName: group.name,
})
);
} catch (err) {
toast.error(t("Could not remove group"));
}
},
[collectionGroupMemberships, collection, t]
);
const handleUpdateGroup = React.useCallback(
async (group, permission) => {
try {
await collectionGroupMemberships.create({
collectionId: collection.id,
groupId: group.id,
permission,
});
toast.success(
t(`{{ groupName }} permissions were updated`, {
groupName: group.name,
})
);
} catch (err) {
toast.error(t("Could not update user"));
}
},
[collectionGroupMemberships, collection, t]
);
const handleChangePermission = React.useCallback(
async (permission: CollectionPermission) => {
try {
await collection.save({
permission,
});
toast.success(t("Default access permissions were updated"));
} catch (err) {
toast.error(t("Could not update permissions"));
}
},
[collection, t]
);
const fetchOptions = React.useMemo(
() => ({
id: collection.id,
}),
[collection.id]
);
const handleSharingChange = React.useCallback(
async (ev: React.ChangeEvent<HTMLInputElement>) => {
try {
await collection.save({
sharing: ev.target.checked,
});
toast.success(t("Public document sharing permissions were updated"));
} catch (err) {
toast.error(t("Could not update public document sharing"));
}
},
[collection, t]
);
const collectionName = collection.name;
const collectionGroups = groups.inCollection(collection.id);
const collectionUsers = users.inCollection(collection.id);
const isEmpty = !collectionGroups.length && !collectionUsers.length;
const sharing = collection.sharing;
const teamSharingEnabled = !!auth.team && auth.team.sharing;
return (
<Flex column>
<InputSelectPermission
onChange={handleChangePermission}
value={collection.permission || ""}
/>
<PermissionExplainer size="small">
{collection.isPrivate && (
<Trans
defaults="The <em>{{ collectionName }}</em> collection is private. Workspace members have no access to it by default."
values={{
collectionName,
}}
components={{
em: <strong />,
}}
/>
)}
{collection.permission === CollectionPermission.ReadWrite && (
<Trans
defaults="Workspace members can view and edit documents in the <em>{{ collectionName }}</em> collection by default."
values={{
collectionName,
}}
components={{
em: <strong />,
}}
/>
)}
{collection.permission === CollectionPermission.Read && (
<Trans
defaults="Workspace members can view documents in the <em>{{ collectionName }}</em> collection by
default."
values={{
collectionName,
}}
components={{
em: <strong />,
}}
/>
)}
</PermissionExplainer>
<Switch
id="sharing"
label={t("Public document sharing")}
onChange={handleSharingChange}
checked={sharing && teamSharingEnabled}
disabled={!teamSharingEnabled}
note={
teamSharingEnabled ? (
<Trans>
When enabled, documents can be shared publicly on the internet.
</Trans>
) : (
<Trans>
Public sharing is currently disabled in the workspace security
settings.
</Trans>
)
}
/>
<Labeled label={t("Additional access")}>
<Actions gap={8}>
<Button
type="button"
onClick={handleAddGroupModalOpen}
icon={<PlusIcon />}
neutral
>
{t("Add groups")}
</Button>
<Button
type="button"
onClick={handleAddMemberModalOpen}
icon={<PlusIcon />}
neutral
>
{t("Add people")}
</Button>
</Actions>
</Labeled>
<Divider />
{isEmpty && (
<Empty>
<Trans>Add additional access for individual members and groups</Trans>
</Empty>
)}
<PaginatedList
items={collectionGroups}
fetch={collectionGroupMemberships.fetchPage}
options={fetchOptions}
renderItem={(group: Group) => (
<CollectionGroupMemberListItem
key={group.id}
group={group}
collectionGroupMembership={collectionGroupMemberships.find({
collectionId: collection.id,
groupId: group.id,
})}
onRemove={() => handleRemoveGroup(group)}
onUpdate={(permission) => handleUpdateGroup(group, permission)}
/>
)}
/>
{collectionGroups.length ? <Divider /> : null}
<PaginatedList
key={`collection-users-${collection.permission || "none"}`}
items={collectionUsers}
fetch={memberships.fetchPage}
options={fetchOptions}
renderItem={(item: User) => (
<MemberListItem
key={item.id}
user={item}
membership={memberships.find({
collectionId: collection.id,
userId: item.id,
})}
canEdit={item.id !== user.id || user.isAdmin}
onRemove={() => handleRemoveUser(item)}
onUpdate={(permission) => handleUpdateUser(item, permission)}
/>
)}
/>
<Modal
title={t(`Add groups to {{ collectionName }}`, {
collectionName: collection.name,
})}
onRequestClose={handleAddGroupModalClose}
isOpen={addGroupModalOpen}
>
<AddGroupsToCollection collection={collection} />
</Modal>
<Modal
title={t(`Add people to {{ collectionName }}`, {
collectionName: collection.name,
})}
onRequestClose={handleAddMemberModalClose}
isOpen={addMemberModalOpen}
>
<AddPeopleToCollection collection={collection} />
</Modal>
</Flex>
);
}
const Empty = styled(Text)`
margin-top: 8px;
`;
const PermissionExplainer = styled(Text)`
margin-top: -8px;
margin-bottom: 24px;
`;
const Actions = styled(Flex)`
margin-bottom: 12px;
`;
export default observer(CollectionPermissions);