fix: Do not show suspended users in picker

This commit is contained in:
Tom Moor
2024-02-03 15:15:41 -05:00
parent e38796d14b
commit 02711c29e3
4 changed files with 249 additions and 230 deletions

View File

@@ -0,0 +1,158 @@
import { observer } from "mobx-react";
import { MoreIcon, QuestionMarkIcon, UserIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useTheme } from "styled-components";
import { CollectionPermission } from "@shared/types";
import type Collection from "~/models/Collection";
import type Document from "~/models/Document";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import useCurrentUser from "~/hooks/useCurrentUser";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import Avatar from "../Avatar";
import { AvatarSize } from "../Avatar/Avatar";
import CollectionIcon from "../Icons/CollectionIcon";
import Squircle from "../Squircle";
import Tooltip from "../Tooltip";
import { StyledListItem } from "./MemberListItem";
export const OtherAccess = observer(
({
document,
children,
}: {
document: Document;
children: React.ReactNode;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const collection = document.collection;
const usersInCollection = useUsersInCollection(collection);
const user = useCurrentUser();
return (
<>
{collection ? (
<>
{collection.permission ? (
<StyledListItem
image={
<Squircle color={theme.accent} size={AvatarSize.Medium}>
<UserIcon color={theme.accentText} size={16} />
</Squircle>
}
title={t("All members")}
subtitle={t("Everyone in the workspace")}
actions={
<AccessTooltip>
{collection?.permission === CollectionPermission.ReadWrite
? t("Can edit")
: t("Can view")}
</AccessTooltip>
}
/>
) : usersInCollection ? (
<StyledListItem
image={
<Squircle color={collection.color} size={AvatarSize.Medium}>
<CollectionIcon
collection={collection}
color={theme.white}
size={16}
/>
</Squircle>
}
title={collection.name}
subtitle={t("Everyone in the collection")}
actions={<AccessTooltip>{t("Can view")}</AccessTooltip>}
/>
) : (
<StyledListItem
image={<Avatar model={user} showBorder={false} />}
title={user.name}
subtitle={t("You have full access")}
actions={<AccessTooltip>{t("Can edit")}</AccessTooltip>}
/>
)}
{children}
</>
) : document.isDraft ? (
<>
<StyledListItem
image={<Avatar model={document.createdBy} showBorder={false} />}
title={document.createdBy.name}
actions={
<AccessTooltip tooltip={t("Created the document")}>
{t("Can edit")}
</AccessTooltip>
}
/>
{children}
</>
) : (
<>
{children}
<StyledListItem
image={
<Squircle color={theme.accent} size={AvatarSize.Medium}>
<MoreIcon color={theme.accentText} size={16} />
</Squircle>
}
title={t("Other people")}
subtitle={t("Other workspace members may have access")}
actions={
<AccessTooltip
tooltip={t(
"This document may be shared with more workspace members through a parent document or collection you do not have access to"
)}
/>
}
/>
</>
)}
</>
);
}
);
const AccessTooltip = ({
children,
tooltip,
}: {
children?: React.ReactNode;
tooltip?: string;
}) => {
const { t } = useTranslation();
return (
<Flex align="center" gap={2}>
<Text type="secondary" size="small">
{children}
</Text>
<Tooltip tooltip={tooltip ?? t("Access inherited from collection")}>
<QuestionMarkIcon size={18} />
</Tooltip>
</Flex>
);
};
function useUsersInCollection(collection?: Collection) {
const { users, memberships } = useStores();
const { request } = useRequest(() =>
memberships.fetchPage({ limit: 1, id: collection!.id })
);
React.useEffect(() => {
if (collection && !collection.permission) {
void request();
}
}, [collection]);
return collection
? collection.permission
? true
: users.inCollection(collection.id).length > 1
: false;
}

View File

@@ -1,49 +1,33 @@
import { AnimatePresence, m } from "framer-motion";
import { observer } from "mobx-react";
import {
BackIcon,
LinkIcon,
MoreIcon,
QuestionMarkIcon,
UserIcon,
} from "outline-icons";
import { BackIcon, LinkIcon } from "outline-icons";
import { darken } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled, { useTheme } from "styled-components";
import styled from "styled-components";
import { s } from "@shared/styles";
import { CollectionPermission } from "@shared/types";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import Share from "~/models/Share";
import User from "~/models/User";
import CopyToClipboard from "~/components/CopyToClipboard";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
import useKeyDown from "~/hooks/useKeyDown";
import useMobile from "~/hooks/useMobile";
import usePolicy from "~/hooks/usePolicy";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import useThrottledCallback from "~/hooks/useThrottledCallback";
import { hover } from "~/styles";
import { documentPath, urlify } from "~/utils/routeHelpers";
import Avatar from "../Avatar";
import { AvatarSize } from "../Avatar/Avatar";
import ButtonSmall from "../ButtonSmall";
import Empty from "../Empty";
import CollectionIcon from "../Icons/CollectionIcon";
import Input, { NativeInput } from "../Input";
import NudeButton from "../NudeButton";
import Squircle from "../Squircle";
import Tooltip from "../Tooltip";
import DocumentMembersList from "./DocumentMemberList";
import { InviteIcon, StyledListItem } from "./MemberListItem";
import { OtherAccess } from "./OtherAccess";
import PublicAccess from "./PublicAccess";
import { UserSuggestions } from "./UserSuggestions";
type Props = {
/** The document to share. */
@@ -81,25 +65,6 @@ const presence = {
},
};
function useUsersInCollection(collection?: Collection) {
const { users, memberships } = useStores();
const { request } = useRequest(() =>
memberships.fetchPage({ limit: 1, id: collection!.id })
);
React.useEffect(() => {
if (collection && !collection.permission) {
void request();
}
}, [collection]);
return collection
? collection.permission
? true
: users.inCollection(collection.id).length > 1
: false;
}
function SharePopover({
document,
share,
@@ -274,17 +239,21 @@ function SharePopover({
{picker && (
<div>
<Picker document={document} query={query} onInvite={handleInvite} />
<UserSuggestions
document={document}
query={query}
onInvite={handleInvite}
/>
</div>
)}
<div style={{ display: picker ? "none" : "block" }}>
<DocumentOtherAccessList document={document}>
<OtherAccess document={document}>
<DocumentMembersList
document={document}
invitedInSession={invitedInSession}
/>
</DocumentOtherAccessList>
</OtherAccess>
{team.sharing && can.share && !collectionSharingDisabled && (
<>
@@ -302,184 +271,6 @@ function SharePopover({
);
}
const Picker = observer(
({
document,
query,
onInvite,
}: {
document: Document;
query: string;
onInvite: (user: User) => Promise<void>;
}) => {
const { users } = useStores();
const { t } = useTranslation();
const user = useCurrentUser();
const fetchUsersByQuery = useThrottledCallback(
(query) => users.fetchPage({ query }),
250
);
const suggestions = React.useMemo(
() =>
users
.notInDocument(document.id, query)
.filter((u) => u.id !== user.id && !user.isSuspended),
[users, users.orderedData, document.id, document.members, user.id, query]
);
React.useEffect(() => {
if (query) {
void fetchUsersByQuery(query);
}
}, [query, fetchUsersByQuery]);
return suggestions.length ? (
<>
{suggestions.map((suggestion) => (
<StyledListItem
key={suggestion.id}
onClick={() => onInvite(suggestion)}
title={suggestion.name}
subtitle={suggestion.isViewer ? t("Viewer") : t("Editor")}
image={
<Avatar
model={suggestion}
size={AvatarSize.Medium}
showBorder={false}
/>
}
actions={<InviteIcon />}
/>
))}
</>
) : (
<Empty style={{ marginTop: 22 }}>{t("No matches")}</Empty>
);
}
);
const DocumentOtherAccessList = observer(
({
document,
children,
}: {
document: Document;
children: React.ReactNode;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const collection = document.collection;
const usersInCollection = useUsersInCollection(collection);
const user = useCurrentUser();
return (
<>
{collection ? (
<>
{collection.permission ? (
<StyledListItem
image={
<Squircle color={theme.accent} size={AvatarSize.Medium}>
<UserIcon color={theme.accentText} size={16} />
</Squircle>
}
title={t("All members")}
subtitle={t("Everyone in the workspace")}
actions={
<AccessTooltip>
{collection?.permission === CollectionPermission.ReadWrite
? t("Can edit")
: t("Can view")}
</AccessTooltip>
}
/>
) : usersInCollection ? (
<StyledListItem
image={
<Squircle color={collection.color} size={AvatarSize.Medium}>
<CollectionIcon
collection={collection}
color={theme.white}
size={16}
/>
</Squircle>
}
title={collection.name}
subtitle={t("Everyone in the collection")}
actions={<AccessTooltip>{t("Can view")}</AccessTooltip>}
/>
) : (
<StyledListItem
image={<Avatar model={user} showBorder={false} />}
title={user.name}
subtitle={t("You have full access")}
actions={<AccessTooltip>{t("Can edit")}</AccessTooltip>}
/>
)}
{children}
</>
) : document.isDraft ? (
<>
<StyledListItem
image={<Avatar model={document.createdBy} showBorder={false} />}
title={document.createdBy.name}
actions={
<AccessTooltip tooltip={t("Created the document")}>
{t("Can edit")}
</AccessTooltip>
}
/>
{children}
</>
) : (
<>
{children}
<StyledListItem
image={
<Squircle color={theme.accent} size={AvatarSize.Medium}>
<MoreIcon color={theme.accentText} size={16} />
</Squircle>
}
title={t("Other people")}
subtitle={t("Other workspace members may have access")}
actions={
<AccessTooltip
tooltip={t(
"This document may be shared with more workspace members through a parent document or collection you do not have access to"
)}
/>
}
/>
</>
)}
</>
);
}
);
const AccessTooltip = ({
children,
tooltip,
}: {
children?: React.ReactNode;
tooltip?: string;
}) => {
const { t } = useTranslation();
return (
<Flex align="center" gap={2}>
<Text type="secondary" size="small">
{children}
</Text>
<Tooltip tooltip={tooltip ?? t("Access inherited from collection")}>
<QuestionMarkIcon size={18} />
</Tooltip>
</Flex>
);
};
// TODO: Temp until Button/NudeButton styles are normalized
const Wrapper = styled.div`
${NudeButton}:${hover},

View File

@@ -0,0 +1,70 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import Document from "~/models/Document";
import User from "~/models/User";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import useThrottledCallback from "~/hooks/useThrottledCallback";
import Avatar from "../Avatar";
import { AvatarSize } from "../Avatar/Avatar";
import Empty from "../Empty";
import { InviteIcon, StyledListItem } from "./MemberListItem";
export const UserSuggestions = observer(
({
document,
query,
onInvite,
}: {
document: Document;
query: string;
onInvite: (user: User) => Promise<void>;
}) => {
const { users } = useStores();
const { t } = useTranslation();
const user = useCurrentUser();
const fetchUsersByQuery = useThrottledCallback(
(query) => users.fetchPage({ query }),
250
);
const suggestions = React.useMemo(
() =>
users
.notInDocument(document.id, query)
.filter((u) => u.id !== user.id && !u.isSuspended),
[users, users.orderedData, document.id, document.members, user.id, query]
);
React.useEffect(() => {
if (query) {
void fetchUsersByQuery(query);
}
}, [query, fetchUsersByQuery]);
return suggestions.length ? (
<>
{suggestions.map((suggestion) => (
<StyledListItem
key={suggestion.id}
onClick={() => onInvite(suggestion)}
title={suggestion.name}
subtitle={suggestion.isViewer ? t("Viewer") : t("Editor")}
image={
<Avatar
model={suggestion}
size={AvatarSize.Medium}
showBorder={false}
/>
}
actions={<InviteIcon />}
/>
))}
</>
) : (
<Empty style={{ marginTop: 22 }}>{t("No matches")}</Empty>
);
}
);

View File

@@ -257,6 +257,16 @@
"Suspended": "Suspended",
"Invited": "Invited",
"Leave": "Leave",
"All members": "All members",
"Everyone in the workspace": "Everyone in the workspace",
"Can view": "Can view",
"Everyone in the collection": "Everyone in the collection",
"You have full access": "You have full access",
"Created the document": "Created the document",
"Other people": "Other people",
"Other workspace members may have access": "Other workspace members may have access",
"This document may be shared with more workspace members through a parent document or collection you do not have access to": "This document may be shared with more workspace members through a parent document or collection you do not have access to",
"Access inherited from collection": "Access inherited from collection",
"Only lowercase letters, digits and dashes allowed": "Only lowercase letters, digits and dashes allowed",
"Sorry, this link has already been used": "Sorry, this link has already been used",
"Public link copied to clipboard": "Public link copied to clipboard",
@@ -270,16 +280,6 @@
"Done": "Done",
"Invite by name": "Invite by name",
"No matches": "No matches",
"All members": "All members",
"Everyone in the workspace": "Everyone in the workspace",
"Can view": "Can view",
"Everyone in the collection": "Everyone in the collection",
"You have full access": "You have full access",
"Created the document": "Created the document",
"Other people": "Other people",
"Other workspace members may have access": "Other workspace members may have access",
"This document may be shared with more workspace members through a parent document or collection you do not have access to": "This document may be shared with more workspace members through a parent document or collection you do not have access to",
"Access inherited from collection": "Access inherited from collection",
"Logo": "Logo",
"Move document": "Move document",
"New doc": "New doc",