diff --git a/app/actions/definitions/users.tsx b/app/actions/definitions/users.tsx
index df6082d6b..95c4f301f 100644
--- a/app/actions/definitions/users.tsx
+++ b/app/actions/definitions/users.tsx
@@ -16,8 +16,7 @@ export const inviteUser = createAction({
stores.policies.abilities(stores.auth.team?.id || "").inviteUser,
perform: ({ t }) => {
stores.dialogs.openModal({
- title: t("Invite people"),
- fullscreen: true,
+ title: t("Invite to workspace"),
content: ,
});
},
diff --git a/app/components/InputSelect.tsx b/app/components/InputSelect.tsx
index dfb29304f..26980f650 100644
--- a/app/components/InputSelect.tsx
+++ b/app/components/InputSelect.tsx
@@ -28,6 +28,7 @@ import { LabelText } from "./Input";
export type Option = {
label: string | JSX.Element;
value: string;
+ description?: string;
};
export type Props = {
@@ -112,7 +113,7 @@ const InputSelect = (props: Props, ref: React.RefObject) => {
const wrappedLabel = {label};
const selectedValueIndex = options.findIndex(
- (option) => option.value === select.selectedValue
+ (opt) => opt.value === select.selectedValue
);
// Custom click outside handling rather than using `hideOnClickOutside` from reakit so that we can
@@ -120,6 +121,9 @@ const InputSelect = (props: Props, ref: React.RefObject) => {
useOnClickOutside(
contentRef,
(event) => {
+ if (buttonRef.current?.contains(event.target as Node)) {
+ return;
+ }
if (select.visible) {
event.stopPropagation();
event.preventDefault();
@@ -163,6 +167,24 @@ const InputSelect = (props: Props, ref: React.RefObject) => {
}
}, [select.visible, selectedValueIndex]);
+ function labelForOption(opt: Option) {
+ return (
+ <>
+ {opt.label}
+ {opt.description && (
+ <>
+
+
+ – {opt.description}
+
+ >
+ )}
+ >
+ );
+ }
+
+ const option = getOptionFromValue(options, select.selectedValue);
+
return (
<>
@@ -174,28 +196,30 @@ const InputSelect = (props: Props, ref: React.RefObject) => {
))}
- {(props: InnerProps) => {
- const topAnchor = props.style?.top === "0";
- const rightAnchor = props.placement === "bottom-end";
+ {(popoverProps: InnerProps) => {
+ const topAnchor = popoverProps.style?.top === "0";
+ const rightAnchor = popoverProps.placement === "bottom-end";
return (
-
+
) => {
}
>
{select.visible
- ? options.map((option) => {
- const isSelected =
- select.selectedValue === option.value;
+ ? options.map((opt) => {
+ const isSelected = select.selectedValue === opt.value;
const Icon = isSelected ? CheckmarkIcon : Spacer;
return (
- {option.label}
+ {labelForOption(opt)}
);
})
diff --git a/app/scenes/Invite.tsx b/app/scenes/Invite.tsx
index 84de52ea4..aec77fd18 100644
--- a/app/scenes/Invite.tsx
+++ b/app/scenes/Invite.tsx
@@ -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.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) => {
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 ? (
- Invited members will receive access to{" "}
+ Invited {{ roleName }} will receive access to{" "}
@@ -149,177 +121,138 @@ function Invite({ onSubmit }: Props) {
>
}
>
-
+
{{ collectionCount }} collections
-
+
.
) : 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 (
);
}
-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);
diff --git a/app/scenes/Settings/Members.tsx b/app/scenes/Settings/Members.tsx
index 044ec6f6b..2d8e4aac3 100644
--- a/app/scenes/Settings/Members.tsx
+++ b/app/scenes/Settings/Members.tsx
@@ -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={}
>
{t("Invite people")}…
@@ -195,15 +194,6 @@ function Members() {
totalPages={totalPages}
defaultSortDirection="ASC"
/>
- {can.inviteUser && (
-
-
-
- )}
);
}
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index 9ef1b0ff8..66b7df51d 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -102,6 +102,7 @@
"New workspace": "New workspace",
"Create a workspace": "Create a workspace",
"Invite people": "Invite people",
+ "Invite to workspace": "Invite to workspace",
"Delete user": "Delete user",
"Collection": "Collection",
"Debug": "Debug",
@@ -656,15 +657,17 @@
"We sent out your invites!": "We sent out your invites!",
"Those email addresses are already invited": "Those email addresses are already invited",
"Sorry, you can only send {{MAX_INVITES}} invites at a time": "Sorry, you can only send {{MAX_INVITES}} invites at a time",
- "Invited members will receive access to": "Invited members will receive access to",
+ "Invited {{roleName}} will receive access to": "Invited {{roleName}} will receive access to",
"{{collectionCount}} collections": "{{collectionCount}} collections",
- "Invite members or guests to join your workspace. They can sign in with {{signinMethods}} or use their email address.": "Invite members or guests to join your workspace. They can sign in with {{signinMethods}} or use their email address.",
+ "Can create, edit, and delete documents": "Can create, edit, and delete documents",
+ "Can view documents and comment": "Can view documents and comment",
+ "Can manage all workspace settings": "Can manage all workspace settings",
+ "Invite people to join your workspace. They can sign in with {{signinMethods}} or use their email address.": "Invite people to join your workspace. They can sign in with {{signinMethods}} or use their email address.",
"Invite members to join your workspace. They will need to sign in with {{signinMethods}}.": "Invite members to join your workspace. They will need to sign in with {{signinMethods}}.",
"As an admin you can also <2>enable email sign-in2>.": "As an admin you can also <2>enable email sign-in2>.",
- "Want a link to share directly with your team?": "Want a link to share directly with your team?",
- "Email": "Email",
+ "Invite as": "Invite as",
"Role": "Role",
- "Remove invite": "Remove invite",
+ "Email": "Email",
"Add another": "Add another",
"Inviting": "Inviting",
"Send Invites": "Send Invites",