diff --git a/app/components/Sharing/SharePopover.tsx b/app/components/Sharing/SharePopover.tsx
index ae93ab9ab..2f503503c 100644
--- a/app/components/Sharing/SharePopover.tsx
+++ b/app/components/Sharing/SharePopover.tsx
@@ -1,3 +1,4 @@
+import { isEmail } from "class-validator";
import { AnimatePresence, m } from "framer-motion";
import { observer } from "mobx-react";
import { BackIcon, LinkIcon } from "outline-icons";
@@ -150,27 +151,46 @@ function SharePopover({
name: t("Invite"),
section: UserSection,
perform: async () => {
- await Promise.all(
- pendingIds.map((userId) => {
- const user = users.get(userId);
+ const usersInvited = await Promise.all(
+ pendingIds.map(async (idOrEmail) => {
+ let user;
- return userMemberships.create({
+ // convert email to user
+ if (isEmail(idOrEmail)) {
+ const response = await users.invite([
+ {
+ email: idOrEmail,
+ name: idOrEmail,
+ role: team.defaultUserRole,
+ },
+ ]);
+ user = response.users[0];
+ } else {
+ user = users.get(idOrEmail);
+ }
+
+ if (!user) {
+ return;
+ }
+
+ await userMemberships.create({
documentId: document.id,
- userId,
+ userId: user.id,
permission:
user?.role === UserRole.Viewer ||
user?.role === UserRole.Guest
? DocumentPermission.Read
: DocumentPermission.ReadWrite,
});
+
+ return user;
})
);
- if (pendingIds.length === 1) {
- const user = users.get(pendingIds[0]);
+ if (usersInvited.length === 1) {
toast.message(
t("{{ userName }} was invited to the document", {
- userName: user!.name,
+ userName: usersInvited[0].name,
})
);
} else {
@@ -186,7 +206,15 @@ function SharePopover({
hidePicker();
},
}),
- [document.id, hidePicker, pendingIds, t, users, userMemberships]
+ [
+ t,
+ pendingIds,
+ hidePicker,
+ userMemberships,
+ document.id,
+ users,
+ team.defaultUserRole,
+ ]
);
const handleQuery = React.useCallback(
diff --git a/app/components/Sharing/UserSuggestions.tsx b/app/components/Sharing/UserSuggestions.tsx
index cc96220de..73a274c08 100644
--- a/app/components/Sharing/UserSuggestions.tsx
+++ b/app/components/Sharing/UserSuggestions.tsx
@@ -1,9 +1,11 @@
+import { isEmail } from "class-validator";
import { observer } from "mobx-react";
import { CheckmarkIcon, CloseIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
+import { stringToColor } from "@shared/utils/color";
import Document from "~/models/Document";
import User from "~/models/User";
import useCurrentUser from "~/hooks/useCurrentUser";
@@ -11,10 +13,14 @@ import useStores from "~/hooks/useStores";
import useThrottledCallback from "~/hooks/useThrottledCallback";
import { hover } from "~/styles";
import Avatar from "../Avatar";
-import { AvatarSize } from "../Avatar/Avatar";
+import { AvatarSize, IAvatar } from "../Avatar/Avatar";
import Empty from "../Empty";
import { InviteIcon, StyledListItem } from "./MemberListItem";
+type Suggestion = IAvatar & {
+ id: string;
+};
+
type Props = {
/** The document being shared. */
document: Document;
@@ -35,21 +41,51 @@ export const UserSuggestions = observer(
const user = useCurrentUser();
const fetchUsersByQuery = useThrottledCallback(
- (query) => users.fetchPage({ query }),
+ (params) => users.fetchPage({ query: params.query }),
250
);
- const suggestions = React.useMemo(
- () =>
- users
- .notInDocument(document.id, query)
- .filter((u) => u.id !== user.id && !u.isSuspended),
- [users, users.orderedData, document.id, document.members, user.id, query]
+ const getSuggestionForEmail = React.useCallback(
+ (email: string) => ({
+ id: email,
+ name: email,
+ avatarUrl: "",
+ color: stringToColor(email),
+ initial: email[0].toUpperCase(),
+ email: t("Invite to workspace"),
+ }),
+ [t]
);
+ const suggestions = React.useMemo(() => {
+ const filtered: Suggestion[] = users
+ .notInDocument(document.id, query)
+ .filter((u) => u.id !== user.id && !u.isSuspended);
+
+ if (isEmail(query)) {
+ filtered.push(getSuggestionForEmail(query));
+ }
+
+ return filtered;
+ }, [
+ getSuggestionForEmail,
+ users,
+ users.orderedData,
+ document.id,
+ document.members,
+ user.id,
+ query,
+ t,
+ ]);
+
const pending = React.useMemo(
- () => pendingIds.map((id) => users.get(id)).filter(Boolean) as User[],
- [users, pendingIds]
+ () =>
+ pendingIds
+ .map((id) =>
+ isEmail(id) ? getSuggestionForEmail(id) : users.get(id)
+ )
+ .filter(Boolean) as User[],
+ [users, getSuggestionForEmail, pendingIds]
);
React.useEffect(() => {
@@ -100,7 +136,7 @@ export const UserSuggestions = observer(
(suggestionsWithPending.length > 0 || isEmpty) && }
{suggestionsWithPending.map((suggestion) => (
addPendingId(suggestion.id)}
actions={}
diff --git a/app/models/Team.ts b/app/models/Team.ts
index 529a2ebf0..be9ea1d22 100644
--- a/app/models/Team.ts
+++ b/app/models/Team.ts
@@ -1,6 +1,6 @@
import { computed, observable } from "mobx";
import { TeamPreferenceDefaults } from "@shared/constants";
-import { TeamPreference, TeamPreferences } from "@shared/types";
+import { TeamPreference, TeamPreferences, UserRole } from "@shared/types";
import { stringToColor } from "@shared/utils/color";
import Model from "./base/Model";
import Field from "./decorators/Field";
@@ -58,7 +58,7 @@ class Team extends Model {
@Field
@observable
- defaultUserRole: string;
+ defaultUserRole: UserRole;
@Field
@observable