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. */}
-
- {[...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 (
+ {shouldShowInviteButton && (
+
+ )}
{shouldShowShareButton && (