Quality of life improvements on 'Invite' screen

This commit is contained in:
Tom Moor
2023-12-16 11:11:57 -05:00
parent 7df0f63ce6
commit fcec796130
4 changed files with 67 additions and 76 deletions

View File

@@ -18,7 +18,7 @@ const InputSelectRole = (
label={t("Role")} label={t("Role")}
options={[ options={[
{ {
label: t("Member"), label: t("Editor"),
value: "member", value: "member",
}, },
{ {

View File

@@ -1,5 +1,5 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { LinkIcon, CloseIcon } from "outline-icons"; import { CloseIcon, CopyIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation, Trans } from "react-i18next"; import { useTranslation, Trans } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -14,6 +14,7 @@ import Flex from "~/components/Flex";
import Input from "~/components/Input"; import Input from "~/components/Input";
import InputSelectRole from "~/components/InputSelectRole"; import InputSelectRole from "~/components/InputSelectRole";
import NudeButton from "~/components/NudeButton"; import NudeButton from "~/components/NudeButton";
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
import Text from "~/components/Text"; import Text from "~/components/Text";
import Tooltip from "~/components/Tooltip"; import Tooltip from "~/components/Tooltip";
import useCurrentTeam from "~/hooks/useCurrentTeam"; import useCurrentTeam from "~/hooks/useCurrentTeam";
@@ -33,23 +34,12 @@ type InviteRequest = {
function Invite({ onSubmit }: Props) { function Invite({ onSubmit }: Props) {
const [isSaving, setIsSaving] = React.useState(false); const [isSaving, setIsSaving] = React.useState(false);
const [linkCopied, setLinkCopied] = React.useState<boolean>(false);
const [invites, setInvites] = React.useState<InviteRequest[]>([ const [invites, setInvites] = React.useState<InviteRequest[]>([
{ {
email: "", email: "",
name: "", name: "",
role: UserRole.Member, role: UserRole.Member,
}, },
{
email: "",
name: "",
role: UserRole.Member,
},
{
email: "",
name: "",
role: UserRole.Member,
},
]); ]);
const { users } = useStores(); const { users } = useStores();
const user = useCurrentUser(); const user = useCurrentUser();
@@ -103,7 +93,7 @@ function Invite({ onSubmit }: Props) {
newInvites.push({ newInvites.push({
email: "", email: "",
name: "", name: "",
role: UserRole.Member, role: invites[invites.length - 1].role,
}); });
return newInvites; return newInvites;
}); });
@@ -122,7 +112,6 @@ function Invite({ onSubmit }: Props) {
); );
const handleCopy = React.useCallback(() => { const handleCopy = React.useCallback(() => {
setLinkCopied(true);
toast.success(t("Share link copied")); toast.success(t("Share link copied"));
}, [t]); }, [t]);
@@ -137,6 +126,16 @@ function Invite({ onSubmit }: Props) {
[] []
); );
const handleKeyDown = React.useCallback(
(ev: React.KeyboardEvent<HTMLInputElement>) => {
if (ev.key === "Enter") {
ev.preventDefault();
handleAdd();
}
},
[handleAdd]
);
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
{team.guestSignin ? ( {team.guestSignin ? (
@@ -166,7 +165,7 @@ function Invite({ onSubmit }: Props) {
)} )}
{team.subdomain && ( {team.subdomain && (
<CopyBlock> <CopyBlock>
<Flex align="flex-end"> <Flex align="flex-end" gap={8}>
<Input <Input
type="text" type="text"
value={team.url} value={team.url}
@@ -174,73 +173,69 @@ function Invite({ onSubmit }: Props) {
readOnly readOnly
flex flex
/> />
&nbsp;&nbsp;
<CopyToClipboard text={team.url} onCopy={handleCopy}> <CopyToClipboard text={team.url} onCopy={handleCopy}>
<Button <Button
type="button" type="button"
icon={<LinkIcon />} icon={<CopyIcon />}
style={{ style={{
marginBottom: "16px", marginBottom: "16px",
}} }}
neutral neutral
> />
{linkCopied ? t("Link copied") : t("Copy link")}
</Button>
</CopyToClipboard> </CopyToClipboard>
</Flex> </Flex>
</CopyBlock> </CopyBlock>
)} )}
{invites.map((invite, index) => ( <ResizingHeightContainer>
<Flex key={index} gap={8}> {invites.map((invite, index) => (
<Input <Flex key={index} gap={8}>
type="email" <Input
name="email" type="email"
label={t("Email")} name="email"
labelHidden={index !== 0} label={t("Email")}
onChange={(ev) => handleChange(ev, index)} labelHidden={index !== 0}
placeholder={`example@${predictedDomain}`} onKeyDown={handleKeyDown}
value={invite.email} onChange={(ev) => handleChange(ev, index)}
required={index === 0} placeholder={`example@${predictedDomain}`}
autoFocus={index === 0} value={invite.email}
flex required={index === 0}
/> autoFocus
<Input flex
type="text" />
name="name" <Input
label={t("Full name")} type="text"
labelHidden={index !== 0} name="name"
onChange={(ev) => handleChange(ev, index)} label={t("Name")}
value={invite.name} labelHidden={index !== 0}
required={!!invite.email} onKeyDown={handleKeyDown}
flex onChange={(ev) => handleChange(ev, index)}
/> value={invite.name}
<InputSelectRole required={!!invite.email}
onChange={(role: UserRole) => handleRoleChange(role, index)} flex
value={invite.role} />
labelHidden={index !== 0} <InputSelectRole
short onChange={(role: UserRole) => handleRoleChange(role, index)}
/> value={invite.role}
{index !== 0 && ( labelHidden={index !== 0}
<Remove> short
<Tooltip tooltip={t("Remove invite")} placement="top"> />
<NudeButton onClick={(ev) => handleRemove(ev, index)}> {index !== 0 && (
<CloseIcon /> <Remove>
</NudeButton> <Tooltip tooltip={t("Remove invite")} placement="top">
</Tooltip> <NudeButton onClick={(ev) => handleRemove(ev, index)}>
</Remove> <CloseIcon />
)} </NudeButton>
{index === 0 && invites.length > 1 && ( </Tooltip>
<Remove> </Remove>
<Spacer /> )}
</Remove> </Flex>
)} ))}
</Flex> </ResizingHeightContainer>
))}
<Flex justify="space-between"> <Flex justify="space-between">
{invites.length <= UserValidation.maxInvitesPerRequest ? ( {invites.length <= UserValidation.maxInvitesPerRequest ? (
<Button type="button" onClick={handleAdd} neutral> <Button type="button" onClick={handleAdd} neutral>
<Trans>Add another</Trans> {t("Add another")}
</Button> </Button>
) : ( ) : (
<span /> <span />
@@ -270,12 +265,9 @@ const CopyBlock = styled("div")`
`; `;
const Remove = styled("div")` const Remove = styled("div")`
color: ${s("textTertiary")};
margin-top: 4px; margin-top: 4px;
`; margin-right: -32px;
const Spacer = styled.div`
width: 24px;
height: 24px;
`; `;
export default observer(Invite); export default observer(Invite);

View File

@@ -227,7 +227,7 @@ function Security() {
value={data.defaultUserRole} value={data.defaultUserRole}
options={[ options={[
{ {
label: t("Member"), label: t("Editor"),
value: "member", value: "member",
}, },
{ {

View File

@@ -222,7 +222,7 @@
"View only": "View only", "View only": "View only",
"No access": "No access", "No access": "No access",
"Role": "Role", "Role": "Role",
"Member": "Member", "Editor": "Editor",
"Viewer": "Viewer", "Viewer": "Viewer",
"Admin": "Admin", "Admin": "Admin",
"{{appName}} is available in your language {{optionLabel}}, would you like to change?": "{{appName}} is available in your language {{optionLabel}}, would you like to change?", "{{appName}} is available in your language {{optionLabel}}, would you like to change?": "{{appName}} is available in your language {{optionLabel}}, would you like to change?",
@@ -637,7 +637,6 @@
"As an admin you can also <2>enable email sign-in</2>.": "As an admin you can also <2>enable email sign-in</2>.", "As an admin you can also <2>enable email sign-in</2>.": "As an admin you can also <2>enable email sign-in</2>.",
"Want a link to share directly with your team?": "Want a link to share directly with your team?", "Want a link to share directly with your team?": "Want a link to share directly with your team?",
"Email": "Email", "Email": "Email",
"Full name": "Full name",
"Remove invite": "Remove invite", "Remove invite": "Remove invite",
"Add another": "Add another", "Add another": "Add another",
"Inviting": "Inviting", "Inviting": "Inviting",