DRY sharing interface
This commit is contained in:
@@ -1,12 +1,11 @@
|
|||||||
import { isEmail } from "class-validator";
|
import { isEmail } from "class-validator";
|
||||||
import { m } from "framer-motion";
|
import { m } from "framer-motion";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { BackIcon, LinkIcon, UserIcon } from "outline-icons";
|
import { BackIcon, UserIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import styled, { useTheme } from "styled-components";
|
import { useTheme } from "styled-components";
|
||||||
import Flex from "@shared/components/Flex";
|
|
||||||
import Squircle from "@shared/components/Squircle";
|
import Squircle from "@shared/components/Squircle";
|
||||||
import { CollectionPermission } from "@shared/types";
|
import { CollectionPermission } from "@shared/types";
|
||||||
import Collection from "~/models/Collection";
|
import Collection from "~/models/Collection";
|
||||||
@@ -14,16 +13,10 @@ import Group from "~/models/Group";
|
|||||||
import Share from "~/models/Share";
|
import Share from "~/models/Share";
|
||||||
import User from "~/models/User";
|
import User from "~/models/User";
|
||||||
import Avatar, { AvatarSize } from "~/components/Avatar/Avatar";
|
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 InputSelectPermission from "~/components/InputSelectPermission";
|
||||||
import NudeButton from "~/components/NudeButton";
|
import NudeButton from "~/components/NudeButton";
|
||||||
import Tooltip from "~/components/Tooltip";
|
|
||||||
import { createAction } from "~/actions";
|
import { createAction } from "~/actions";
|
||||||
import { UserSection } from "~/actions/sections";
|
import { UserSection } from "~/actions/sections";
|
||||||
import useActionContext from "~/hooks/useActionContext";
|
|
||||||
import useBoolean from "~/hooks/useBoolean";
|
import useBoolean from "~/hooks/useBoolean";
|
||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||||
import useKeyDown from "~/hooks/useKeyDown";
|
import useKeyDown from "~/hooks/useKeyDown";
|
||||||
@@ -32,12 +25,15 @@ import useStores from "~/hooks/useStores";
|
|||||||
import { Permission } from "~/types";
|
import { Permission } from "~/types";
|
||||||
import { collectionPath, urlify } from "~/utils/routeHelpers";
|
import { collectionPath, urlify } from "~/utils/routeHelpers";
|
||||||
import { Wrapper, presence } from "../components";
|
import { Wrapper, presence } from "../components";
|
||||||
|
import { CopyLinkButton } from "../components/CopyLinkButton";
|
||||||
import { ListItem } from "../components/ListItem";
|
import { ListItem } from "../components/ListItem";
|
||||||
|
import { PermissionAction } from "../components/PermissionAction";
|
||||||
import { SearchInput } from "../components/SearchInput";
|
import { SearchInput } from "../components/SearchInput";
|
||||||
import { Suggestions } from "../components/Suggestions";
|
import { Suggestions } from "../components/Suggestions";
|
||||||
import CollectionMemberList from "./CollectionMemberList";
|
import CollectionMemberList from "./CollectionMemberList";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
/** The collection to share. */
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
/** The existing share model, if any. */
|
/** The existing share model, if any. */
|
||||||
share: Share | null | undefined;
|
share: Share | null | undefined;
|
||||||
@@ -62,8 +58,6 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
|
|||||||
const [permission, setPermission] = React.useState<CollectionPermission>(
|
const [permission, setPermission] = React.useState<CollectionPermission>(
|
||||||
CollectionPermission.Read
|
CollectionPermission.Read
|
||||||
);
|
);
|
||||||
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
|
|
||||||
const context = useActionContext();
|
|
||||||
|
|
||||||
useKeyDown(
|
useKeyDown(
|
||||||
"Escape",
|
"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
|
// Clear the query when picker is closed
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!picker) {
|
if (!picker) {
|
||||||
@@ -95,20 +97,6 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
|
|||||||
}
|
}
|
||||||
}, [visible]);
|
}, [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(
|
const handleQuery = React.useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
showPicker();
|
showPicker();
|
||||||
@@ -284,35 +272,19 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
|
|||||||
|
|
||||||
const rightButton = picker ? (
|
const rightButton = picker ? (
|
||||||
pendingIds.length ? (
|
pendingIds.length ? (
|
||||||
<Flex gap={4} key="invite">
|
<PermissionAction
|
||||||
<InputPermissionSelect
|
permission={permission}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
|
action={inviteAction}
|
||||||
onChange={(value: CollectionPermission) => setPermission(value)}
|
onChange={(value: CollectionPermission) => setPermission(value)}
|
||||||
value={permission}
|
key="invite"
|
||||||
labelHidden
|
|
||||||
nude
|
|
||||||
/>
|
/>
|
||||||
<ButtonSmall action={inviteAction} context={context}>
|
|
||||||
{t("Add")}
|
|
||||||
</ButtonSmall>
|
|
||||||
</Flex>
|
|
||||||
) : null
|
) : null
|
||||||
) : (
|
) : (
|
||||||
<Tooltip
|
<CopyLinkButton
|
||||||
content={t("Copy link")}
|
url={urlify(collectionPath(collection.path))}
|
||||||
delay={500}
|
onCopy={onRequestClose}
|
||||||
placement="top"
|
/>
|
||||||
key="copy-link"
|
|
||||||
>
|
|
||||||
<CopyToClipboard
|
|
||||||
text={urlify(collectionPath(collection.path))}
|
|
||||||
onCopy={handleCopied}
|
|
||||||
>
|
|
||||||
<NudeButton type="button">
|
|
||||||
<LinkIcon size={20} />
|
|
||||||
</NudeButton>
|
|
||||||
</CopyToClipboard>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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);
|
export default observer(SharePopover);
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
import { isEmail } from "class-validator";
|
import { isEmail } from "class-validator";
|
||||||
import { m } from "framer-motion";
|
import { m } from "framer-motion";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { BackIcon, LinkIcon } from "outline-icons";
|
import { BackIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import styled from "styled-components";
|
|
||||||
import Flex from "@shared/components/Flex";
|
|
||||||
import { DocumentPermission } from "@shared/types";
|
import { DocumentPermission } from "@shared/types";
|
||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
import Share from "~/models/Share";
|
import Share from "~/models/Share";
|
||||||
import User from "~/models/User";
|
import User from "~/models/User";
|
||||||
import Avatar from "~/components/Avatar";
|
import Avatar from "~/components/Avatar";
|
||||||
import { AvatarSize } from "~/components/Avatar/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 NudeButton from "~/components/NudeButton";
|
||||||
import Tooltip from "~/components/Tooltip";
|
|
||||||
import { createAction } from "~/actions";
|
import { createAction } from "~/actions";
|
||||||
import { UserSection } from "~/actions/sections";
|
import { UserSection } from "~/actions/sections";
|
||||||
import useActionContext from "~/hooks/useActionContext";
|
|
||||||
import useBoolean from "~/hooks/useBoolean";
|
import useBoolean from "~/hooks/useBoolean";
|
||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||||
import useKeyDown from "~/hooks/useKeyDown";
|
import useKeyDown from "~/hooks/useKeyDown";
|
||||||
@@ -30,6 +22,8 @@ import useStores from "~/hooks/useStores";
|
|||||||
import { Permission } from "~/types";
|
import { Permission } from "~/types";
|
||||||
import { documentPath, urlify } from "~/utils/routeHelpers";
|
import { documentPath, urlify } from "~/utils/routeHelpers";
|
||||||
import { Separator, Wrapper, presence } from "../components";
|
import { Separator, Wrapper, presence } from "../components";
|
||||||
|
import { CopyLinkButton } from "../components/CopyLinkButton";
|
||||||
|
import { PermissionAction } from "../components/PermissionAction";
|
||||||
import { SearchInput } from "../components/SearchInput";
|
import { SearchInput } from "../components/SearchInput";
|
||||||
import { Suggestions } from "../components/Suggestions";
|
import { Suggestions } from "../components/Suggestions";
|
||||||
import DocumentMembersList from "./DocumentMemberList";
|
import DocumentMembersList from "./DocumentMemberList";
|
||||||
@@ -59,13 +53,10 @@ function SharePopover({
|
|||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const can = usePolicy(document);
|
const can = usePolicy(document);
|
||||||
const linkButtonRef = React.useRef<HTMLButtonElement>(null);
|
|
||||||
const context = useActionContext();
|
|
||||||
const [hasRendered, setHasRendered] = React.useState(visible);
|
const [hasRendered, setHasRendered] = React.useState(visible);
|
||||||
const { users, userMemberships } = useStores();
|
const { users, userMemberships } = useStores();
|
||||||
const [query, setQuery] = React.useState("");
|
const [query, setQuery] = React.useState("");
|
||||||
const [picker, showPicker, hidePicker] = useBoolean();
|
const [picker, showPicker, hidePicker] = useBoolean();
|
||||||
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
|
|
||||||
const [invitedInSession, setInvitedInSession] = React.useState<string[]>([]);
|
const [invitedInSession, setInvitedInSession] = React.useState<string[]>([]);
|
||||||
const [pendingIds, setPendingIds] = React.useState<string[]>([]);
|
const [pendingIds, setPendingIds] = React.useState<string[]>([]);
|
||||||
const collectionSharingDisabled = document.collection?.sharing === false;
|
const collectionSharingDisabled = document.collection?.sharing === false;
|
||||||
@@ -113,20 +104,6 @@ function SharePopover({
|
|||||||
}
|
}
|
||||||
}, [picker]);
|
}, [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(
|
const inviteAction = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
createAction({
|
createAction({
|
||||||
@@ -262,35 +239,19 @@ function SharePopover({
|
|||||||
|
|
||||||
const rightButton = picker ? (
|
const rightButton = picker ? (
|
||||||
pendingIds.length ? (
|
pendingIds.length ? (
|
||||||
<Flex gap={4} key="invite">
|
<PermissionAction
|
||||||
<InputPermissionSelect
|
permission={permission}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
|
action={inviteAction}
|
||||||
onChange={(value: DocumentPermission) => setPermission(value)}
|
onChange={(value: DocumentPermission) => setPermission(value)}
|
||||||
value={permission}
|
key="invite"
|
||||||
labelHidden
|
|
||||||
nude
|
|
||||||
/>
|
/>
|
||||||
<ButtonSmall action={inviteAction} context={context}>
|
|
||||||
{t("Add")}
|
|
||||||
</ButtonSmall>
|
|
||||||
</Flex>
|
|
||||||
) : null
|
) : null
|
||||||
) : (
|
) : (
|
||||||
<Tooltip
|
<CopyLinkButton
|
||||||
content={t("Copy link")}
|
url={urlify(documentPath(document))}
|
||||||
delay={500}
|
onCopy={onRequestClose}
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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);
|
export default observer(SharePopover);
|
||||||
|
|||||||
47
app/components/Sharing/components/CopyLinkButton.tsx
Normal file
47
app/components/Sharing/components/CopyLinkButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
app/components/Sharing/components/PermissionAction.tsx
Normal file
53
app/components/Sharing/components/PermissionAction.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -271,9 +271,9 @@
|
|||||||
"{{ count }} people added to the collection_plural": "{{ count }} people added to the collection",
|
"{{ 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": "{{ 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",
|
"{{ 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",
|
"All members": "All members",
|
||||||
"Everyone in the workspace": "Everyone in the workspace",
|
"Everyone in the workspace": "Everyone in the workspace",
|
||||||
|
"Add": "Add",
|
||||||
"Add or invite": "Add or invite",
|
"Add or invite": "Add or invite",
|
||||||
"Viewer": "Viewer",
|
"Viewer": "Viewer",
|
||||||
"Editor": "Editor",
|
"Editor": "Editor",
|
||||||
|
|||||||
Reference in New Issue
Block a user