DRY sharing interface

This commit is contained in:
Tom Moor
2024-05-27 08:57:29 -04:00
parent 20642f4225
commit 7858133e71
5 changed files with 139 additions and 126 deletions

View File

@@ -1,12 +1,11 @@
import { isEmail } from "class-validator";
import { m } from "framer-motion";
import { observer } from "mobx-react";
import { BackIcon, LinkIcon, UserIcon } from "outline-icons";
import { BackIcon, UserIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled, { useTheme } from "styled-components";
import Flex from "@shared/components/Flex";
import { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import { CollectionPermission } from "@shared/types";
import Collection from "~/models/Collection";
@@ -14,16 +13,10 @@ import Group from "~/models/Group";
import Share from "~/models/Share";
import User from "~/models/User";
import Avatar, { AvatarSize } from "~/components/Avatar/Avatar";
import { Inner } from "~/components/Button";
import ButtonSmall from "~/components/ButtonSmall";
import CopyToClipboard from "~/components/CopyToClipboard";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import InputSelectPermission from "~/components/InputSelectPermission";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import { createAction } from "~/actions";
import { UserSection } from "~/actions/sections";
import useActionContext from "~/hooks/useActionContext";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useKeyDown from "~/hooks/useKeyDown";
@@ -32,12 +25,15 @@ import useStores from "~/hooks/useStores";
import { Permission } from "~/types";
import { collectionPath, urlify } from "~/utils/routeHelpers";
import { Wrapper, presence } from "../components";
import { CopyLinkButton } from "../components/CopyLinkButton";
import { ListItem } from "../components/ListItem";
import { PermissionAction } from "../components/PermissionAction";
import { SearchInput } from "../components/SearchInput";
import { Suggestions } from "../components/Suggestions";
import CollectionMemberList from "./CollectionMemberList";
type Props = {
/** The collection to share. */
collection: Collection;
/** The existing share model, if any. */
share: Share | null | undefined;
@@ -62,8 +58,6 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
const [permission, setPermission] = React.useState<CollectionPermission>(
CollectionPermission.Read
);
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
const context = useActionContext();
useKeyDown(
"Escape",
@@ -82,6 +76,14 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
}
);
// Hide the picker when the popover is closed
React.useEffect(() => {
if (visible) {
setPendingIds([]);
hidePicker();
}
}, [hidePicker, visible]);
// Clear the query when picker is closed
React.useEffect(() => {
if (!picker) {
@@ -95,20 +97,6 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
}
}, [visible]);
const handleCopied = React.useCallback(() => {
onRequestClose();
timeout.current = setTimeout(() => {
toast.message(t("Link copied to clipboard"));
}, 100);
return () => {
if (timeout.current) {
clearTimeout(timeout.current);
}
};
}, [onRequestClose, t]);
const handleQuery = React.useCallback(
(event) => {
showPicker();
@@ -284,35 +272,19 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
const rightButton = picker ? (
pendingIds.length ? (
<Flex gap={4} key="invite">
<InputPermissionSelect
permissions={permissions}
onChange={(value: CollectionPermission) => setPermission(value)}
value={permission}
labelHidden
nude
/>
<ButtonSmall action={inviteAction} context={context}>
{t("Add")}
</ButtonSmall>
</Flex>
<PermissionAction
permission={permission}
permissions={permissions}
action={inviteAction}
onChange={(value: CollectionPermission) => setPermission(value)}
key="invite"
/>
) : null
) : (
<Tooltip
content={t("Copy link")}
delay={500}
placement="top"
key="copy-link"
>
<CopyToClipboard
text={urlify(collectionPath(collection.path))}
onCopy={handleCopied}
>
<NudeButton type="button">
<LinkIcon size={20} />
</NudeButton>
</CopyToClipboard>
</Tooltip>
<CopyLinkButton
url={urlify(collectionPath(collection.path))}
onCopy={onRequestClose}
/>
);
return (
@@ -373,14 +345,4 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
);
}
const InputPermissionSelect = styled(InputMemberPermissionSelect)`
font-size: 13px;
height: 26px;
${Inner} {
line-height: 26px;
min-height: 26px;
}
`;
export default observer(SharePopover);

View File

@@ -1,27 +1,19 @@
import { isEmail } from "class-validator";
import { m } from "framer-motion";
import { observer } from "mobx-react";
import { BackIcon, LinkIcon } from "outline-icons";
import { BackIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import Flex from "@shared/components/Flex";
import { DocumentPermission } from "@shared/types";
import Document from "~/models/Document";
import Share from "~/models/Share";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
import { AvatarSize } from "~/components/Avatar/Avatar";
import { Inner } from "~/components/Button";
import ButtonSmall from "~/components/ButtonSmall";
import CopyToClipboard from "~/components/CopyToClipboard";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import { createAction } from "~/actions";
import { UserSection } from "~/actions/sections";
import useActionContext from "~/hooks/useActionContext";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useKeyDown from "~/hooks/useKeyDown";
@@ -30,6 +22,8 @@ import useStores from "~/hooks/useStores";
import { Permission } from "~/types";
import { documentPath, urlify } from "~/utils/routeHelpers";
import { Separator, Wrapper, presence } from "../components";
import { CopyLinkButton } from "../components/CopyLinkButton";
import { PermissionAction } from "../components/PermissionAction";
import { SearchInput } from "../components/SearchInput";
import { Suggestions } from "../components/Suggestions";
import DocumentMembersList from "./DocumentMemberList";
@@ -59,13 +53,10 @@ function SharePopover({
const team = useCurrentTeam();
const { t } = useTranslation();
const can = usePolicy(document);
const linkButtonRef = React.useRef<HTMLButtonElement>(null);
const context = useActionContext();
const [hasRendered, setHasRendered] = React.useState(visible);
const { users, userMemberships } = useStores();
const [query, setQuery] = React.useState("");
const [picker, showPicker, hidePicker] = useBoolean();
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
const [invitedInSession, setInvitedInSession] = React.useState<string[]>([]);
const [pendingIds, setPendingIds] = React.useState<string[]>([]);
const collectionSharingDisabled = document.collection?.sharing === false;
@@ -113,20 +104,6 @@ function SharePopover({
}
}, [picker]);
const handleCopied = React.useCallback(() => {
onRequestClose();
timeout.current = setTimeout(() => {
toast.message(t("Link copied to clipboard"));
}, 100);
return () => {
if (timeout.current) {
clearTimeout(timeout.current);
}
};
}, [onRequestClose, t]);
const inviteAction = React.useMemo(
() =>
createAction({
@@ -262,35 +239,19 @@ function SharePopover({
const rightButton = picker ? (
pendingIds.length ? (
<Flex gap={4} key="invite">
<InputPermissionSelect
permissions={permissions}
onChange={(value: DocumentPermission) => setPermission(value)}
value={permission}
labelHidden
nude
/>
<ButtonSmall action={inviteAction} context={context}>
{t("Add")}
</ButtonSmall>
</Flex>
<PermissionAction
permission={permission}
permissions={permissions}
action={inviteAction}
onChange={(value: DocumentPermission) => setPermission(value)}
key="invite"
/>
) : null
) : (
<Tooltip
content={t("Copy link")}
delay={500}
placement="top"
key="copy-link"
>
<CopyToClipboard
text={urlify(documentPath(document))}
onCopy={handleCopied}
>
<NudeButton type="button" disabled={!share} ref={linkButtonRef}>
<LinkIcon size={20} />
</NudeButton>
</CopyToClipboard>
</Tooltip>
<CopyLinkButton
url={urlify(documentPath(document))}
onCopy={onRequestClose}
/>
);
return (
@@ -341,14 +302,4 @@ function SharePopover({
);
}
const InputPermissionSelect = styled(InputMemberPermissionSelect)`
font-size: 13px;
height: 26px;
${Inner} {
line-height: 26px;
min-height: 26px;
}
`;
export default observer(SharePopover);

View File

@@ -0,0 +1,47 @@
import { LinkIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import CopyToClipboard from "~/components/CopyToClipboard";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
export function CopyLinkButton({
url,
onCopy,
}: {
url: string;
onCopy: () => void;
}) {
const { t } = useTranslation();
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
const handleCopied = React.useCallback(() => {
onCopy();
timeout.current = setTimeout(() => {
toast.message(t("Link copied to clipboard"));
}, 100);
return () => {
if (timeout.current) {
clearTimeout(timeout.current);
}
};
}, [onCopy, t]);
return (
<Tooltip
content={t("Copy link")}
delay={500}
placement="top"
key="copy-link"
>
<CopyToClipboard text={url} onCopy={handleCopied}>
<NudeButton type="button">
<LinkIcon size={20} />
</NudeButton>
</CopyToClipboard>
</Tooltip>
);
}

View File

@@ -0,0 +1,53 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Flex from "@shared/components/Flex";
import { CollectionPermission, DocumentPermission } from "@shared/types";
import { Inner } from "~/components/Button";
import ButtonSmall from "~/components/ButtonSmall";
import Fade from "~/components/Fade";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import useActionContext from "~/hooks/useActionContext";
import { Action, Permission } from "~/types";
export function PermissionAction({
permission,
permissions,
action,
onChange,
}: {
permission: CollectionPermission | DocumentPermission;
permissions: Permission[];
action: Action;
onChange: (permission: CollectionPermission | DocumentPermission) => void;
}) {
const { t } = useTranslation();
const context = useActionContext();
return (
<Fade timing="150ms" key="invite">
<Flex gap={4}>
<InputPermissionSelect
permissions={permissions}
onChange={onChange}
value={permission}
labelHidden
nude
/>
<ButtonSmall action={action} context={context}>
{t("Add")}
</ButtonSmall>
</Flex>
</Fade>
);
}
const InputPermissionSelect = styled(InputMemberPermissionSelect)`
font-size: 13px;
height: 26px;
${Inner} {
line-height: 26px;
min-height: 26px;
}
`;

View File

@@ -271,9 +271,9 @@
"{{ count }} people added to the collection_plural": "{{ count }} people added to the collection",
"{{ count }} people and {{ count2 }} groups added to the collection": "{{ count }} people and {{ count2 }} groups added to the collection",
"{{ count }} people and {{ count2 }} groups added to the collection_plural": "{{ count }} people and {{ count2 }} groups added to the collection",
"Add": "Add",
"All members": "All members",
"Everyone in the workspace": "Everyone in the workspace",
"Add": "Add",
"Add or invite": "Add or invite",
"Viewer": "Viewer",
"Editor": "Editor",