; onSettled: () => void }
+) {
+ options.onSuccess(CREATED_USER)
+ options.onSettled()
+}
+
function buttonLabelled(text: string): HTMLButtonElement {
const button = [...container.querySelectorAll('button')].find(
(candidate) => candidate.textContent === text
@@ -179,6 +256,11 @@ describe('AddUserModal', () => {
onCreated = vi.fn()
onOpenChange = vi.fn()
addUserMutation.current = { isPending: false, error: null }
+ resetPasswordMutation.current = { isPending: false }
+ mockResetPassword.mockResolvedValue({ success: true })
+ // vi.clearAllMocks() does not drop implementations, so re-arm the default (a
+ // request that never settles) rather than inheriting the previous test's.
+ mockMutate.mockImplementation(() => {})
})
afterEach(() => {
@@ -202,11 +284,7 @@ describe('AddUserModal', () => {
})
it('creates a verified credential user and returns it to the admin view', async () => {
- mockMutate.mockImplementation(
- (_input: AddUserInput, options: { onSuccess: (user: AdminUser) => void }) => {
- options.onSuccess(CREATED_USER)
- }
- )
+ mockMutate.mockImplementation(succeedWithCreatedUser)
await renderModal()
await fillRequiredFields()
@@ -227,6 +305,57 @@ describe('AddUserModal', () => {
)
expect(onOpenChange).toHaveBeenCalledWith(false)
expect(onCreated).toHaveBeenCalledWith(CREATED_USER)
+ expect(mockResetPassword).not.toHaveBeenCalled()
+ })
+
+ it('sends a password reset email when the toggle is on', async () => {
+ mockMutate.mockImplementation(succeedWithCreatedUser)
+ await renderModal()
+ await fillRequiredFields()
+ await toggleField('Send password reset email')
+
+ await act(async () => {
+ buttonLabelled('Add user').dispatchEvent(new MouseEvent('click', { bubbles: true }))
+ await Promise.resolve()
+ await Promise.resolve()
+ await Promise.resolve()
+ })
+
+ expect(mockResetPassword).toHaveBeenCalledWith({
+ email: 'writer@synthetics.example.com',
+ redirectTo: 'https://sim.test/reset-password',
+ })
+ expect(mockToast.success).toHaveBeenCalled()
+ expect(onCreated).toHaveBeenCalledWith(CREATED_USER)
+ })
+
+ it('keeps the submit path locked while the reset email is still in flight', async () => {
+ resetPasswordMutation.current = { isPending: true }
+ await renderModal()
+ await fillRequiredFields()
+
+ expect(buttonLabelled('Adding...').disabled).toBe(true)
+ expect(buttonLabelled('Close').disabled).toBe(true)
+ expect(buttonLabelled('Cancel').disabled).toBe(true)
+ })
+
+ it('still reports the created user when the reset email fails', async () => {
+ mockResetPassword.mockRejectedValue(new Error('SMTP unavailable'))
+ mockMutate.mockImplementation(succeedWithCreatedUser)
+ await renderModal()
+ await fillRequiredFields()
+ await toggleField('Send password reset email')
+
+ await act(async () => {
+ buttonLabelled('Add user').dispatchEvent(new MouseEvent('click', { bubbles: true }))
+ await Promise.resolve()
+ await Promise.resolve()
+ await Promise.resolve()
+ })
+
+ expect(mockToast.error).toHaveBeenCalledWith(expect.stringContaining('SMTP unavailable'))
+ expect(onOpenChange).toHaveBeenCalledWith(false)
+ expect(onCreated).toHaveBeenCalledWith(CREATED_USER)
})
it('ignores repeated submissions before the pending state renders', async () => {
@@ -245,11 +374,7 @@ describe('AddUserModal', () => {
})
it('supports unverified accounts without exposing a platform-role control', async () => {
- mockMutate.mockImplementation(
- (_input: AddUserInput, options: { onSuccess: (user: AdminUser) => void }) => {
- options.onSuccess(CREATED_USER)
- }
- )
+ mockMutate.mockImplementation(succeedWithCreatedUser)
await renderModal()
await fillRequiredFields()
await changeField('Email status', 'unverified')
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/add-user-modal.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/add-user-modal.tsx
index 89ebb362a34..0056d0c99b5 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/add-user-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/add-user-modal.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useRef, useState } from 'react'
+import { useId, useRef, useState } from 'react'
import {
ChipModal,
ChipModalBody,
@@ -8,10 +8,15 @@ import {
ChipModalField,
ChipModalFooter,
ChipModalHeader,
+ Label,
+ Switch,
+ toast,
} from '@sim/emcn'
import { getErrorMessage } from '@sim/utils/errors'
import { isValidEmailSyntax } from '@sim/utils/string'
+import { getBaseUrl } from '@/lib/core/utils/urls'
import { type AdminUser, useAddUser } from '@/hooks/queries/admin-users'
+import { useResetPassword } from '@/hooks/queries/user-profile'
const EMAIL_STATUS_OPTIONS = [
{ value: 'verified', label: 'Verified' },
@@ -26,12 +31,15 @@ interface AddUserModalProps {
export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProps) {
const addUser = useAddUser()
+ const resetPassword = useResetPassword()
+ const resetEmailToggleId = useId()
const submissionInFlightRef = useRef(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [emailVerified, setEmailVerified] = useState(true)
+ const [sendResetEmail, setSendResetEmail] = useState(false)
const normalizedName = name.trim()
const normalizedEmail = email.trim().toLowerCase()
@@ -42,7 +50,7 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
password.length > 0 && password.length < 8
? 'Password must be at least 8 characters'
: undefined
- const isSubmissionPending = isSubmitting || addUser.isPending
+ const isSubmissionPending = isSubmitting || addUser.isPending || resetPassword.isPending
const canSubmit =
normalizedName.length > 0 &&
isValidEmailSyntax(normalizedEmail) &&
@@ -54,7 +62,9 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
setEmail('')
setPassword('')
setEmailVerified(true)
+ setSendResetEmail(false)
addUser.reset()
+ resetPassword.reset()
}
const handleClose = () => {
@@ -76,7 +86,20 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
emailVerified,
},
{
- onSuccess: (user) => {
+ onSuccess: async (user) => {
+ if (sendResetEmail) {
+ try {
+ await resetPassword.mutateAsync({
+ email: normalizedEmail,
+ redirectTo: `${getBaseUrl()}/reset-password`,
+ })
+ toast.success(`Password reset email sent to ${normalizedEmail}`)
+ } catch (error) {
+ toast.error(
+ `User created, but the password reset email failed to send: ${getErrorMessage(error, 'Unknown error')}`
+ )
+ }
+ }
reset()
onOpenChange(false)
onCreated(user)
@@ -160,6 +183,18 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
disabled={isSubmissionPending}
required
/>
+
+
+ {
+ setSendResetEmail(checked)
+ addUser.reset()
+ }}
+ />
+
{addUser.error ? getErrorMessage(addUser.error, 'Failed to add user') : null}