Move invite dialog to centered design (#6740)
* wip * Update invite dialog
This commit is contained in:
@@ -1,19 +1,17 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { CloseIcon, CopyIcon } from "outline-icons";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import pluralize from "pluralize";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import { UserRole } from "@shared/types";
|
||||
import { UserValidation } from "@shared/validations";
|
||||
import Button from "~/components/Button";
|
||||
import CopyToClipboard from "~/components/CopyToClipboard";
|
||||
import Flex from "~/components/Flex";
|
||||
import Input from "~/components/Input";
|
||||
import InputSelect from "~/components/InputSelect";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
|
||||
import Text from "~/components/Text";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
@@ -29,7 +27,6 @@ type Props = {
|
||||
type InviteRequest = {
|
||||
email: string;
|
||||
name: string;
|
||||
role: UserRole;
|
||||
};
|
||||
|
||||
function Invite({ onSubmit }: Props) {
|
||||
@@ -38,7 +35,6 @@ function Invite({ onSubmit }: Props) {
|
||||
{
|
||||
email: "",
|
||||
name: "",
|
||||
role: UserRole.Member,
|
||||
},
|
||||
]);
|
||||
const { users, collections } = useStores();
|
||||
@@ -47,6 +43,7 @@ function Invite({ onSubmit }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const predictedDomain = user.email.split("@")[1];
|
||||
const can = usePolicy(team);
|
||||
const [role, setRole] = React.useState<UserRole>(UserRole.Member);
|
||||
|
||||
const handleSubmit = React.useCallback(
|
||||
async (ev: React.SyntheticEvent) => {
|
||||
@@ -54,7 +51,9 @@ function Invite({ onSubmit }: Props) {
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const data = await users.invite(invites.filter((i) => i.email));
|
||||
const data = await users.invite(
|
||||
invites.filter((i) => i.email).map((memo) => ({ ...memo, role }))
|
||||
);
|
||||
onSubmit();
|
||||
|
||||
if (data.sent.length > 0) {
|
||||
@@ -68,7 +67,7 @@ function Invite({ onSubmit }: Props) {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[onSubmit, invites, t, users]
|
||||
[onSubmit, invites, role, t, users]
|
||||
);
|
||||
|
||||
const handleChange = React.useCallback((ev, index) => {
|
||||
@@ -93,39 +92,11 @@ function Invite({ onSubmit }: Props) {
|
||||
newInvites.push({
|
||||
email: "",
|
||||
name: "",
|
||||
role: invites[invites.length - 1].role,
|
||||
});
|
||||
return newInvites;
|
||||
});
|
||||
}, [invites, t]);
|
||||
|
||||
const handleRemove = React.useCallback(
|
||||
(ev: React.SyntheticEvent, index: number) => {
|
||||
ev.preventDefault();
|
||||
setInvites((prevInvites) => {
|
||||
const newInvites = [...prevInvites];
|
||||
newInvites.splice(index, 1);
|
||||
return newInvites;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
toast.success(t("Share link copied"));
|
||||
}, [t]);
|
||||
|
||||
const handleRoleChange = React.useCallback(
|
||||
(role: UserRole, index: number) => {
|
||||
setInvites((prevInvites) => {
|
||||
const newInvites = [...prevInvites];
|
||||
newInvites[index]["role"] = role;
|
||||
return newInvites;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(ev: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (ev.key === "Enter") {
|
||||
@@ -136,10 +107,11 @@ function Invite({ onSubmit }: Props) {
|
||||
[handleAdd]
|
||||
);
|
||||
|
||||
const roleName = pluralize(role);
|
||||
const collectionCount = collections.nonPrivate.length;
|
||||
const collectionAccessNote = collectionCount ? (
|
||||
<span>
|
||||
<Trans>Invited members will receive access to</Trans>{" "}
|
||||
<Trans>Invited {{ roleName }} will receive access to</Trans>{" "}
|
||||
<Tooltip
|
||||
content={
|
||||
<>
|
||||
@@ -149,177 +121,138 @@ function Invite({ onSubmit }: Props) {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Def>
|
||||
<strong>
|
||||
<Trans>{{ collectionCount }} collections</Trans>
|
||||
</Def>
|
||||
</strong>
|
||||
</Tooltip>
|
||||
.
|
||||
</span>
|
||||
) : undefined;
|
||||
|
||||
const options = React.useMemo(() => {
|
||||
const options = [
|
||||
const memo = [
|
||||
{
|
||||
label: t("Editor"),
|
||||
description: t("Can create, edit, and delete documents"),
|
||||
value: "member",
|
||||
},
|
||||
{
|
||||
label: t("Viewer"),
|
||||
description: t("Can view documents and comment"),
|
||||
value: "viewer",
|
||||
},
|
||||
];
|
||||
|
||||
if (user.isAdmin) {
|
||||
options.push({
|
||||
memo.push({
|
||||
label: t("Admin"),
|
||||
description: t("Can manage all workspace settings"),
|
||||
value: "admin",
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
return memo;
|
||||
}, [t, user]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{team.guestSignin ? (
|
||||
<Text as="p" type="secondary">
|
||||
<Trans
|
||||
defaults="Invite members or guests to join your workspace. They can sign in with {{signinMethods}} or use their email address."
|
||||
values={{
|
||||
signinMethods: team.signinMethods,
|
||||
}}
|
||||
/>{" "}
|
||||
{collectionAccessNote}
|
||||
</Text>
|
||||
) : (
|
||||
<Text as="p" type="secondary">
|
||||
<Trans
|
||||
defaults="Invite members to join your workspace. They will need to sign in with {{signinMethods}}."
|
||||
values={{
|
||||
signinMethods: team.signinMethods,
|
||||
}}
|
||||
/>{" "}
|
||||
{can.update && (
|
||||
<Trans>
|
||||
As an admin you can also{" "}
|
||||
<Link to="/settings/security">enable email sign-in</Link>.
|
||||
</Trans>
|
||||
)}{" "}
|
||||
{collectionAccessNote}
|
||||
</Text>
|
||||
)}
|
||||
{team.subdomain && (
|
||||
<CopyBlock>
|
||||
<Flex align="flex-end" gap={8}>
|
||||
<Input
|
||||
type="text"
|
||||
value={team.url}
|
||||
label={t("Want a link to share directly with your team?")}
|
||||
readOnly
|
||||
flex
|
||||
/>
|
||||
<CopyToClipboard text={team.url} onCopy={handleCopy}>
|
||||
<Button
|
||||
type="button"
|
||||
icon={<CopyIcon />}
|
||||
style={{
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
neutral
|
||||
/>
|
||||
</CopyToClipboard>
|
||||
</Flex>
|
||||
</CopyBlock>
|
||||
)}
|
||||
<ResizingHeightContainer>
|
||||
{invites.map((invite, index) => (
|
||||
<Flex key={index} gap={8}>
|
||||
<Input
|
||||
type="email"
|
||||
name="email"
|
||||
label={t("Email")}
|
||||
labelHidden={index !== 0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(ev) => handleChange(ev, index)}
|
||||
placeholder={`example@${predictedDomain}`}
|
||||
value={invite.email}
|
||||
required={index === 0}
|
||||
autoFocus
|
||||
flex
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
name="name"
|
||||
label={t("Name")}
|
||||
labelHidden={index !== 0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(ev) => handleChange(ev, index)}
|
||||
value={invite.name}
|
||||
required={!!invite.email}
|
||||
flex
|
||||
/>
|
||||
<InputSelect
|
||||
label={t("Role")}
|
||||
ariaLabel={t("Role")}
|
||||
options={options}
|
||||
onChange={(role: UserRole) => handleRoleChange(role, index)}
|
||||
value={invite.role}
|
||||
labelHidden={index !== 0}
|
||||
short
|
||||
/>
|
||||
{index !== 0 && (
|
||||
<Remove>
|
||||
<Tooltip content={t("Remove invite")} placement="top">
|
||||
<NudeButton onClick={(ev) => handleRemove(ev, index)}>
|
||||
<CloseIcon />
|
||||
</NudeButton>
|
||||
</Tooltip>
|
||||
</Remove>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</ResizingHeightContainer>
|
||||
|
||||
<Flex justify="space-between">
|
||||
{invites.length <= UserValidation.maxInvitesPerRequest ? (
|
||||
<Button type="button" onClick={handleAdd} neutral>
|
||||
{t("Add another")}…
|
||||
</Button>
|
||||
<Flex gap={8} column>
|
||||
{team.guestSignin ? (
|
||||
<Text as="p" type="secondary">
|
||||
<Trans
|
||||
defaults="Invite people to join your workspace. They can sign in with {{signinMethods}} or use their email address."
|
||||
values={{
|
||||
signinMethods: team.signinMethods,
|
||||
}}
|
||||
/>{" "}
|
||||
{collectionAccessNote}
|
||||
</Text>
|
||||
) : (
|
||||
<span />
|
||||
<Text as="p" type="secondary">
|
||||
<Trans
|
||||
defaults="Invite members to join your workspace. They will need to sign in with {{signinMethods}}."
|
||||
values={{
|
||||
signinMethods: team.signinMethods,
|
||||
}}
|
||||
/>{" "}
|
||||
{collectionAccessNote}
|
||||
{can.update && (
|
||||
<Trans>
|
||||
As an admin you can also{" "}
|
||||
<Link to="/settings/security">enable email sign-in</Link>.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
<Flex gap={12} column>
|
||||
<InputSelect
|
||||
label={t("Invite as")}
|
||||
ariaLabel={t("Role")}
|
||||
options={options}
|
||||
onChange={(r) => setRole(r as UserRole)}
|
||||
value={role}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
data-on="click"
|
||||
data-event-category="invite"
|
||||
data-event-action="sendInvites"
|
||||
>
|
||||
{isSaving ? `${t("Inviting")}…` : t("Send Invites")}
|
||||
</Button>
|
||||
<ResizingHeightContainer style={{ minHeight: 72, marginBottom: 8 }}>
|
||||
{invites.map((invite, index) => (
|
||||
<Flex key={index} gap={8}>
|
||||
<StyledInput
|
||||
type="email"
|
||||
name="email"
|
||||
label={t("Email")}
|
||||
labelHidden={index !== 0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(ev) => handleChange(ev, index)}
|
||||
placeholder={`example@${predictedDomain}`}
|
||||
value={invite.email}
|
||||
required={index === 0}
|
||||
autoFocus
|
||||
flex
|
||||
/>
|
||||
<StyledInput
|
||||
type="text"
|
||||
name="name"
|
||||
label={t("Name")}
|
||||
labelHidden={index !== 0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(ev) => handleChange(ev, index)}
|
||||
value={invite.name}
|
||||
required={!!invite.email}
|
||||
flex
|
||||
/>
|
||||
</Flex>
|
||||
))}
|
||||
</ResizingHeightContainer>
|
||||
</Flex>
|
||||
|
||||
<Flex justify="space-between">
|
||||
{invites.length <= UserValidation.maxInvitesPerRequest ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleAdd}
|
||||
icon={<PlusIcon />}
|
||||
neutral
|
||||
>
|
||||
{t("Add another")}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
data-on="click"
|
||||
data-event-category="invite"
|
||||
data-event-action="sendInvites"
|
||||
>
|
||||
{isSaving ? `${t("Inviting")}…` : t("Send Invites")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<br />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const CopyBlock = styled("div")`
|
||||
margin: 2em 0;
|
||||
font-size: 14px;
|
||||
background: ${s("secondaryBackground")};
|
||||
border-radius: 8px;
|
||||
padding: 16px 16px 8px;
|
||||
`;
|
||||
|
||||
const Remove = styled("div")`
|
||||
color: ${s("textTertiary")};
|
||||
margin-top: 4px;
|
||||
margin-right: -32px;
|
||||
`;
|
||||
|
||||
const Def = styled("span")`
|
||||
text-decoration: underline dotted;
|
||||
const StyledInput = styled(Input)`
|
||||
margin-bottom: -4px;
|
||||
`;
|
||||
|
||||
export default observer(Invite);
|
||||
|
||||
@@ -7,17 +7,16 @@ import { useHistory, useLocation } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { PAGINATION_SYMBOL } from "~/stores/base/Store";
|
||||
import User from "~/models/User";
|
||||
import Invite from "~/scenes/Invite";
|
||||
import { Action } from "~/components/Actions";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
import Heading from "~/components/Heading";
|
||||
import InputSearch from "~/components/InputSearch";
|
||||
import Modal from "~/components/Modal";
|
||||
import Scene from "~/components/Scene";
|
||||
import Text from "~/components/Text";
|
||||
import { inviteUser } from "~/actions/definitions/users";
|
||||
import env from "~/env";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useQuery from "~/hooks/useQuery";
|
||||
@@ -28,9 +27,8 @@ import UserStatusFilter from "./components/UserStatusFilter";
|
||||
function Members() {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const [inviteModalOpen, handleInviteModalOpen, handleInviteModalClose] =
|
||||
useBoolean();
|
||||
const team = useCurrentTeam();
|
||||
const context = useActionContext();
|
||||
const { users } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const params = useQuery();
|
||||
@@ -155,7 +153,8 @@ function Members() {
|
||||
data-on="click"
|
||||
data-event-category="invite"
|
||||
data-event-action="peoplePage"
|
||||
onClick={handleInviteModalOpen}
|
||||
action={inviteUser}
|
||||
context={context}
|
||||
icon={<PlusIcon />}
|
||||
>
|
||||
{t("Invite people")}…
|
||||
@@ -195,15 +194,6 @@ function Members() {
|
||||
totalPages={totalPages}
|
||||
defaultSortDirection="ASC"
|
||||
/>
|
||||
{can.inviteUser && (
|
||||
<Modal
|
||||
title={t("Invite people")}
|
||||
onRequestClose={handleInviteModalClose}
|
||||
isOpen={inviteModalOpen}
|
||||
>
|
||||
<Invite onSubmit={handleInviteModalClose} />
|
||||
</Modal>
|
||||
)}
|
||||
</Scene>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user