diff --git a/app/ide-desktop/lib/dashboard/src/layouts/dashboard/InviteUsersModal.tsx b/app/ide-desktop/lib/dashboard/src/layouts/dashboard/InviteUsersModal.tsx index 95262e075247..2c78ca55baf8 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/dashboard/InviteUsersModal.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/dashboard/InviteUsersModal.tsx @@ -3,6 +3,8 @@ import * as React from 'react' import isEmail from 'validator/es/lib/isEmail' +import CrossIcon from 'enso-assets/cross.svg' + import * as asyncEffectHooks from '#/hooks/asyncEffectHooks' import * as toastAndLogHooks from '#/hooks/toastAndLogHooks' @@ -14,9 +16,46 @@ import Modal from '#/components/Modal' import * as backendModule from '#/services/Backend' -// ============================== -// === ManagePermissionsModal === -// ============================== +// ================= +// === Constants === +// ================= + +/** The minimum width of the input for adding a new email. */ +const MIN_EMAIL_INPUT_WIDTH = 120 + +// ============= +// === Email === +// ============= + +/** Props for an {@link Email}. */ +interface InternalEmailProps { + readonly email: string + readonly isValid: boolean + readonly doDelete: () => void +} + +/** A self-validating email display. */ +function Email(props: InternalEmailProps) { + const { email, isValid, doDelete } = props + return ( +
+ {email}{' '} + +
+ ) +} + +// ======================== +// === InviteUsersModal === +// ======================== /** Props for an {@link InviteUsersModal}. */ export interface InviteUsersModalProps { @@ -31,7 +70,7 @@ export default function InviteUsersModal(props: InviteUsersModalProps) { const { backend } = backendProvider.useBackend() const { unsetModal } = modalProvider.useSetModal() const toastAndLog = toastAndLogHooks.useToastAndLog() - const [newEmails, setNewEmails] = React.useState(new Set()) + const [newEmails, setNewEmails] = React.useState([]) const [email, setEmail] = React.useState('') const position = React.useMemo(() => eventTarget?.getBoundingClientRect(), [eventTarget]) const members = asyncEffectHooks.useAsyncEffect([], () => backend.listUsers(), [backend]) @@ -39,29 +78,15 @@ export default function InviteUsersModal(props: InviteUsersModalProps) { () => new Set(members.map(member => member.email)), [members] ) - const invalidEmailError = React.useMemo( + const canSubmit = React.useMemo( () => - email === '' - ? 'Email is blank' - : !isEmail(email) - ? `'${email}' is not a valid email` - : existingEmails.has(email) - ? `'${email}' is already in the organization` - : newEmails.has(email) - ? `You are already adding '${email}'` - : null, - [email, existingEmails, newEmails] + newEmails.length > 0 && + newEmails.every( + (newEmail, i) => + isEmail(newEmail) && !existingEmails.has(newEmail) && newEmails.indexOf(newEmail) === i + ), + [existingEmails, newEmails] ) - const isEmailValid = invalidEmailError == null - - const doAddEmail = () => { - if (!isEmailValid) { - toastAndLog(invalidEmailError) - } else { - setNewEmails(oldNewEmails => new Set([...oldNewEmails, email])) - setEmail('') - } - } const doSubmit = () => { unsetModal() @@ -90,10 +115,7 @@ export default function InviteUsersModal(props: InviteUsersModalProps) { tabIndex={-1} style={ position != null - ? { - left: position.left + window.scrollX, - top: position.top + window.scrollY, - } + ? { left: position.left + window.scrollX, top: position.top + window.scrollY } : {} } className="sticky w-115.25 rounded-2xl before:absolute before:bg-frame-selected before:backdrop-blur-3xl before:rounded-2xl before:w-full before:h-full" @@ -116,50 +138,75 @@ export default function InviteUsersModal(props: InviteUsersModalProps) { {/* Space reserved for other tabs. */}
{ event.preventDefault() - doAddEmail() + if (email !== '') { + setNewEmails([...newEmails, email]) + setEmail('') + } else if (canSubmit) { + doSubmit() + } }} > -
+
- +
-
    - {[...newEmails].map(newEmail => ( -
  • - {newEmail} -
  • - ))} -
diff --git a/app/ide-desktop/lib/dashboard/src/layouts/dashboard/UserBar.tsx b/app/ide-desktop/lib/dashboard/src/layouts/dashboard/UserBar.tsx index ce26483e4639..e59a140da9ef 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/dashboard/UserBar.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/dashboard/UserBar.tsx @@ -8,6 +8,7 @@ import * as authProvider from '#/providers/AuthProvider' import * as backendProvider from '#/providers/BackendProvider' import * as modalProvider from '#/providers/ModalProvider' +import InviteUsersModal from '#/layouts/dashboard/InviteUsersModal' import ManagePermissionsModal from '#/layouts/dashboard/ManagePermissionsModal' import * as pageSwitcher from '#/layouts/dashboard/PageSwitcher' import UserMenu from '#/layouts/dashboard/UserMenu' @@ -37,7 +38,7 @@ export interface UserBarProps { export default function UserBar(props: UserBarProps) { const { supportsLocalBackend, page, setPage, isHelpChatOpen, setIsHelpChatOpen } = props const { projectAsset, setProjectAsset, doRemoveSelf, onSignOut } = props - const { user } = authProvider.useNonPartialUserSession() + const { type: sessionType, user } = authProvider.useNonPartialUserSession() const { setModal, updateModal } = modalProvider.useSetModal() const { backend } = backendProvider.useBackend() const self = @@ -52,6 +53,8 @@ export default function UserBar(props: UserBarProps) { projectAsset != null && setProjectAsset != null && self != null + const shouldShowInviteButton = + sessionType === authProvider.UserSessionType.full && !shouldShowShareButton return (
+ )} {shouldShowShareButton && (