Skip to content

Commit 44dfe92

Browse files
fix(admin): reflect immediate submission state
1 parent 45d8918 commit 44dfe92

3 files changed

Lines changed: 48 additions & 12 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/components/admin/add-user-modal.test.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,36 @@ const { addUserMutation, mockMutate, mockReset } = vi.hoisted(() => ({
2121
vi.mock('@sim/emcn', () => ({
2222
ChipModal: ({ open, children }: { open: boolean; children: ReactNode }) =>
2323
open ? <div role='dialog'>{children}</div> : null,
24-
ChipModalHeader: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
24+
ChipModalHeader: ({
25+
children,
26+
onClose,
27+
closeDisabled,
28+
}: {
29+
children: ReactNode
30+
onClose: () => void
31+
closeDisabled?: boolean
32+
}) => (
33+
<header>
34+
<h2>{children}</h2>
35+
<button type='button' onClick={onClose} disabled={closeDisabled}>
36+
Close
37+
</button>
38+
</header>
39+
),
2540
ChipModalBody: ({ children }: { children: ReactNode }) => <div>{children}</div>,
2641
ChipModalError: ({ children }: { children: ReactNode }) =>
2742
children ? <div role='alert'>{children}</div> : null,
2843
ChipModalFooter: ({
2944
onCancel,
45+
cancelDisabled,
3046
primaryAction,
3147
}: {
3248
onCancel: () => void
49+
cancelDisabled?: boolean
3350
primaryAction: { label: ReactNode; onClick: () => void; disabled?: boolean }
3451
}) => (
3552
<footer>
36-
<button type='button' onClick={onCancel}>
53+
<button type='button' onClick={onCancel} disabled={cancelDisabled}>
3754
Cancel
3855
</button>
3956
<button type='button' disabled={primaryAction.disabled} onClick={primaryAction.onClick}>
@@ -223,6 +240,8 @@ describe('AddUserModal', () => {
223240
})
224241

225242
expect(mockMutate).toHaveBeenCalledTimes(1)
243+
expect(buttonLabelled('Close').disabled).toBe(true)
244+
expect(buttonLabelled('Cancel').disabled).toBe(true)
226245
})
227246

228247
it('supports unverified accounts without exposing a platform-role control', async () => {

apps/sim/app/workspace/[workspaceId]/settings/components/admin/add-user-modal.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ interface AddUserModalProps {
2727
export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProps) {
2828
const addUser = useAddUser()
2929
const submissionInFlightRef = useRef(false)
30+
const [isSubmitting, setIsSubmitting] = useState(false)
3031
const [name, setName] = useState('')
3132
const [email, setEmail] = useState('')
3233
const [password, setPassword] = useState('')
@@ -41,11 +42,12 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
4142
password.length > 0 && password.length < 8
4243
? 'Password must be at least 8 characters'
4344
: undefined
45+
const isSubmissionPending = isSubmitting || addUser.isPending
4446
const canSubmit =
4547
normalizedName.length > 0 &&
4648
isValidEmailSyntax(normalizedEmail) &&
4749
password.length >= 8 &&
48-
!addUser.isPending
50+
!isSubmissionPending
4951

5052
const reset = () => {
5153
setName('')
@@ -56,14 +58,15 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
5658
}
5759

5860
const handleClose = () => {
59-
if (submissionInFlightRef.current || addUser.isPending) return
61+
if (submissionInFlightRef.current || isSubmissionPending) return
6062
reset()
6163
onOpenChange(false)
6264
}
6365

6466
const handleAddUser = () => {
6567
if (!canSubmit || submissionInFlightRef.current) return
6668
submissionInFlightRef.current = true
69+
setIsSubmitting(true)
6770
addUser.reset()
6871
addUser.mutate(
6972
{
@@ -80,6 +83,7 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
8083
},
8184
onSettled: () => {
8285
submissionInFlightRef.current = false
86+
setIsSubmitting(false)
8387
},
8488
}
8589
)
@@ -93,7 +97,9 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
9397
}}
9498
srTitle='Add user'
9599
>
96-
<ChipModalHeader onClose={handleClose}>Add user</ChipModalHeader>
100+
<ChipModalHeader onClose={handleClose} closeDisabled={isSubmissionPending}>
101+
Add user
102+
</ChipModalHeader>
97103
<ChipModalBody>
98104
<ChipModalField
99105
type='input'
@@ -107,7 +113,7 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
107113
placeholder='Canary Writer'
108114
maxLength={100}
109115
autoComplete='off'
110-
disabled={addUser.isPending}
116+
disabled={isSubmissionPending}
111117
required
112118
/>
113119
<ChipModalField
@@ -121,7 +127,7 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
121127
error={emailError}
122128
placeholder='writer@synthetics.example.com'
123129
autoComplete='off'
124-
disabled={addUser.isPending}
130+
disabled={isSubmissionPending}
125131
required
126132
/>
127133
<ChipModalField
@@ -137,7 +143,7 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
137143
hint='Better Auth creates a credential account with this password.'
138144
placeholder='At least 8 characters'
139145
autoComplete='new-password'
140-
disabled={addUser.isPending}
146+
disabled={isSubmissionPending}
141147
required
142148
/>
143149
<ChipModalField
@@ -151,7 +157,7 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
151157
options={EMAIL_STATUS_OPTIONS}
152158
align='start'
153159
hint='Verified users can sign in when email verification is required.'
154-
disabled={addUser.isPending}
160+
disabled={isSubmissionPending}
155161
required
156162
/>
157163
<ChipModalError>
@@ -160,9 +166,9 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
160166
</ChipModalBody>
161167
<ChipModalFooter
162168
onCancel={handleClose}
163-
cancelDisabled={addUser.isPending}
169+
cancelDisabled={isSubmissionPending}
164170
primaryAction={{
165-
label: addUser.isPending ? 'Adding...' : 'Add user',
171+
label: isSubmissionPending ? 'Adding...' : 'Add user',
166172
onClick: handleAddUser,
167173
disabled: !canSubmit,
168174
}}

packages/emcn/src/components/chip-modal/chip-modal.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ export interface ChipModalHeaderProps extends React.HTMLAttributes<HTMLDivElemen
154154
icon?: React.ComponentType<{ className?: string }> | null
155155
/** Invoked when the trailing close button is activated. Always rendered. */
156156
onClose: () => void
157+
/** Disables the trailing close button while an operation is in flight. */
158+
closeDisabled?: boolean
157159
/** Accessible label for the close button. */
158160
closeAriaLabel?: string
159161
}
@@ -164,7 +166,15 @@ export interface ChipModalHeaderProps extends React.HTMLAttributes<HTMLDivElemen
164166
*/
165167
const ChipModalHeader = React.forwardRef<HTMLDivElement, ChipModalHeaderProps>(
166168
(
167-
{ className, children, icon: Icon = null, onClose, closeAriaLabel = 'Close', ...props },
169+
{
170+
className,
171+
children,
172+
icon: Icon = null,
173+
onClose,
174+
closeDisabled = false,
175+
closeAriaLabel = 'Close',
176+
...props
177+
},
168178
ref
169179
) => (
170180
<div ref={ref} className={cn('flex flex-col', className)} {...props}>
@@ -177,6 +187,7 @@ const ChipModalHeader = React.forwardRef<HTMLDivElement, ChipModalHeaderProps>(
177187
type='button'
178188
variant='ghost'
179189
onClick={onClose}
190+
disabled={closeDisabled}
180191
className='relative size-[14px] flex-shrink-0 p-0 before:absolute before:inset-[-14px] before:content-[""]'
181192
>
182193
<X className='size-[14px] text-[var(--text-icon)]' />

0 commit comments

Comments
 (0)