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

@@ -11,7 +11,6 @@ import {
import * as React from "react";
import stores from "~/stores";
import Collection from "~/models/Collection";
import CollectionPermissions from "~/scenes/CollectionPermissions";
import { CollectionEdit } from "~/components/Collection/CollectionEdit";
import { CollectionNew } from "~/components/Collection/CollectionNew";
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
@@ -21,7 +20,6 @@ import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import { createAction } from "~/actions";
import { CollectionSection } from "~/actions/sections";
import { setPersistedState } from "~/hooks/usePersistedState";
import { Feature, FeatureFlags } from "~/utils/FeatureFlags";
import history from "~/utils/history";
import { searchPath } from "~/utils/routeHelpers";
@@ -111,24 +109,16 @@ export const editCollectionPermissions = createAction({
return;
}
if (FeatureFlags.isEnabled(Feature.newCollectionSharing)) {
stores.dialogs.openModal({
title: t("Share this collection"),
content: (
<SharePopover
collection={collection}
onRequestClose={stores.dialogs.closeAllModals}
visible
/>
),
});
} else {
stores.dialogs.openModal({
title: t("Collection permissions"),
fullscreen: true,
content: <CollectionPermissions collectionId={activeCollectionId} />,
});
}
stores.dialogs.openModal({
title: t("Share this collection"),
content: (
<SharePopover
collection={collection}
onRequestClose={stores.dialogs.closeAllModals}
visible
/>
),
});
},
});

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);

View File

@@ -518,43 +518,6 @@
"Recently published": "Recently published",
"Least recently updated": "Least recently updated",
"AZ": "AZ",
"{{ groupName }} was added to the collection": "{{ groupName }} was added to the collection",
"Could not add user": "Could not add user",
"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",
"Need to add someone whos not on the team yet?": "Need to add someone whos not on the team yet?",
"Invite people to {{ teamName }}": "Invite people to {{ teamName }}",
"Ask an admin to invite them first": "Ask an admin to invite them first",
"Search by name": "Search by name",
"Search people": "Search people",
"No people matching your search": "No people matching your search",
"No people left to add": "No people left to add",
"Active <1></1> ago": "Active <1></1> ago",
"Never signed in": "Never signed in",
"{{ userName }} was removed from the collection": "{{ userName }} was removed from the collection",
"{{ userName }} permissions were updated": "{{ userName }} permissions were updated",
"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",
"Public document sharing permissions were updated": "Public document sharing permissions were updated",
"Could not update public document sharing": "Could not update public document sharing",
"The <em>{{ collectionName }}</em> collection is private. Workspace members have no access to it by default.": "The <em>{{ collectionName }}</em> collection is private. Workspace members have no access to it by default.",
"Workspace members can view and edit documents in the <em>{{ collectionName }}</em> collection by default.": "Workspace members can view and edit documents in the <em>{{ collectionName }}</em> collection by default.",
"Workspace members can view documents in the <em>{{ collectionName }}</em> collection by\n default.": "Workspace members can view documents in the <em>{{ collectionName }}</em> collection by\n default.",
"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 workspace security settings.": "Public sharing is currently disabled in the workspace security settings.",
"Additional access": "Additional access",
"Add groups": "Add groups",
"Add people": "Add people",
"Add additional access for individual members and groups": "Add additional access for individual members and groups",
"Add groups to {{ collectionName }}": "Add groups to {{ collectionName }}",
"Add people to {{ collectionName }}": "Add people to {{ collectionName }}",
"Signing in": "Signing in",
"You can safely close this window once the Outline desktop app has opened": "You can safely close this window once the Outline desktop app has opened",
"Error creating comment": "Error creating comment",
@@ -658,10 +621,19 @@
"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.",
"{{userName}} was added to the group": "{{userName}} was added to the group",
"Could not add user": "Could not add user",
"Add members below to give them access to the group. Need to add someone whos not yet a member?": "Add members below to give them access to the group. Need to add someone whos not yet a member?",
"Invite them to {{teamName}}": "Invite them to {{teamName}}",
"Ask an admin to invite them first": "Ask an admin to invite them first",
"Search by name": "Search by name",
"Search people": "Search people",
"No people matching your search": "No people matching your search",
"No people left to add": "No people left to add",
"Active <1></1> ago": "Active <1></1> ago",
"Never signed in": "Never signed in",
"{{userName}} was removed from the group": "{{userName}} was removed from the group",
"Add and remove members to the <em>{{groupName}}</em> group. Members of the group will have access to any collections this group has been added to.": "Add and remove members to the <em>{{groupName}}</em> group. Members of the group will have access to any collections this group has been added to.",
"Add people": "Add people",
"Listing members of the <em>{{groupName}}</em> group.": "Listing members of the <em>{{groupName}}</em> group.",
"This group has no members.": "This group has no members.",
"Add people to {{groupName}}": "Add people to {{groupName}}",
@@ -876,6 +848,7 @@
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"No groups have been created yet": "No groups have been created yet",
"All": "All",
"Create a group": "Create a group",
"Quickly transfer your existing documents, pages, and files from other tools and services into {{appName}}. You can also drag and drop any HTML, Markdown, and text documents directly into Collections in the app.": "Quickly transfer your existing documents, pages, and files from other tools and services into {{appName}}. You can also drag and drop any HTML, Markdown, and text documents directly into Collections in the app.",
"Import a zip file of Markdown documents (exported from version 0.67.0 or earlier)": "Import a zip file of Markdown documents (exported from version 0.67.0 or earlier)",
"Import data": "Import data",