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