Skip to content

Commit fe45ba5

Browse files
committed
ack PR comments
1 parent 35f3f31 commit fe45ba5

5 files changed

Lines changed: 89 additions & 110 deletions

File tree

apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ export async function PUT(
447447
.where(eq(workspaceInvitation.id, wsInvitation.id))
448448

449449
const existingPermission = await tx
450-
.select({ id: permissions.id })
450+
.select({ id: permissions.id, permissionType: permissions.permissionType })
451451
.from(permissions)
452452
.where(
453453
and(
@@ -459,13 +459,22 @@ export async function PUT(
459459
.then((rows) => rows[0])
460460

461461
if (existingPermission) {
462-
await tx
463-
.update(permissions)
464-
.set({
465-
permissionType: wsInvitation.permissions || 'read',
466-
updatedAt: new Date(),
467-
})
468-
.where(eq(permissions.id, existingPermission.id))
462+
const PERMISSION_RANK = { read: 0, write: 1, admin: 2 } as const
463+
type PermissionLevel = keyof typeof PERMISSION_RANK
464+
const existingRank =
465+
PERMISSION_RANK[existingPermission.permissionType as PermissionLevel] ?? 0
466+
const newPermission = (wsInvitation.permissions || 'read') as PermissionLevel
467+
const newRank = PERMISSION_RANK[newPermission] ?? 0
468+
469+
if (newRank > existingRank) {
470+
await tx
471+
.update(permissions)
472+
.set({
473+
permissionType: newPermission,
474+
updatedAt: new Date(),
475+
})
476+
.where(eq(permissions.id, existingPermission.id))
477+
}
469478
} else {
470479
await tx.insert(permissions).values({
471480
id: randomUUID(),

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export function McpDeploy({
149149
})
150150
const [parameterDescriptions, setParameterDescriptions] = useState<Record<string, string>>({})
151151
const [pendingServerChanges, setPendingServerChanges] = useState<Set<string>>(new Set())
152+
const [saveErrors, setSaveErrors] = useState<string[]>([])
152153

153154
const parameterSchema = useMemo(
154155
() => generateParameterSchema(inputFormat, parameterDescriptions),
@@ -285,8 +286,10 @@ export function McpDeploy({
285286
if (toAdd.size === 0 && toRemove.length === 0 && !shouldUpdateExisting) return
286287

287288
onSubmittingChange?.(true)
289+
setSaveErrors([])
288290
try {
289291
const nextServerToolsMap = { ...serverToolsMap }
292+
const errors: string[] = []
290293

291294
for (const serverId of toAdd) {
292295
setPendingServerChanges((prev) => new Set(prev).add(serverId))
@@ -303,6 +306,8 @@ export function McpDeploy({
303306
onAddedToServer?.()
304307
logger.info(`Added workflow ${workflowId} as tool to server ${serverId}`)
305308
} catch (error) {
309+
const serverName = servers.find((s) => s.id === serverId)?.name || serverId
310+
errors.push(`Failed to add to ${serverName}`)
306311
logger.error(`Failed to add tool to server ${serverId}:`, error)
307312
} finally {
308313
setPendingServerChanges((prev) => {
@@ -326,6 +331,8 @@ export function McpDeploy({
326331
})
327332
delete nextServerToolsMap[serverId]
328333
} catch (error) {
334+
const serverName = servers.find((s) => s.id === serverId)?.name || serverId
335+
errors.push(`Failed to remove from ${serverName}`)
329336
logger.error(`Failed to remove tool from server ${serverId}:`, error)
330337
} finally {
331338
setPendingServerChanges((prev) => {
@@ -352,19 +359,25 @@ export function McpDeploy({
352359
parameterSchema,
353360
})
354361
} catch (error) {
362+
const serverName = servers.find((s) => s.id === serverId)?.name || serverId
363+
errors.push(`Failed to update on ${serverName}`)
355364
logger.error(`Failed to update tool on server ${serverId}:`, error)
356365
}
357366
}
358367
}
359368

360369
setServerToolsMap(nextServerToolsMap)
361-
setDraftSelectedServerIds(null)
362-
setSavedValues({
363-
toolName,
364-
toolDescription,
365-
parameterDescriptions: { ...parameterDescriptions },
366-
})
367-
onCanSaveChange?.(false)
370+
if (errors.length > 0) {
371+
setSaveErrors(errors)
372+
} else {
373+
setDraftSelectedServerIds(null)
374+
setSavedValues({
375+
toolName,
376+
toolDescription,
377+
parameterDescriptions: { ...parameterDescriptions },
378+
})
379+
onCanSaveChange?.(false)
380+
}
368381
onSubmittingChange?.(false)
369382
} catch (error) {
370383
logger.error('Failed to save tool configuration:', error)
@@ -381,6 +394,7 @@ export function McpDeploy({
381394
serverToolsMap,
382395
workspaceId,
383396
workflowId,
397+
servers,
384398
addToolMutation,
385399
deleteToolMutation,
386400
updateToolMutation,
@@ -571,10 +585,14 @@ export function McpDeploy({
571585
)}
572586
</div>
573587

574-
{addToolMutation.isError && (
575-
<p className='mt-[6.5px] text-[12px] text-[var(--text-error)]'>
576-
{addToolMutation.error?.message || 'Failed to add tool'}
577-
</p>
588+
{saveErrors.length > 0 && (
589+
<div className='mt-[6.5px] flex flex-col gap-[2px]'>
590+
{saveErrors.map((error) => (
591+
<p key={error} className='text-[12px] text-[var(--text-error)]'>
592+
{error}
593+
</p>
594+
))}
595+
</div>
578596
)}
579597
</form>
580598
)

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/components/member-invitation-card/member-invitation-card.tsx

Lines changed: 28 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
'use client'
22

3-
import React, { useState } from 'react'
3+
import React from 'react'
44
import { ChevronDown } from 'lucide-react'
55
import {
66
Button,
77
ButtonGroup,
88
ButtonGroupItem,
99
Checkbox,
10-
Input,
1110
Popover,
1211
PopoverContent,
1312
PopoverItem,
1413
PopoverTrigger,
14+
TagInput,
15+
type TagItem,
1516
} from '@/components/emcn'
1617
import { cn } from '@/lib/core/utils/cn'
1718
import { quickValidateEmail } from '@/lib/messaging/email/validation'
@@ -64,8 +65,8 @@ const PermissionSelector = React.memo<PermissionSelectorProps>(
6465
PermissionSelector.displayName = 'PermissionSelector'
6566

6667
interface MemberInvitationCardProps {
67-
inviteEmail: string
68-
setInviteEmail: (email: string) => void
68+
inviteEmails: TagItem[]
69+
setInviteEmails: (emails: TagItem[]) => void
6970
isInviting: boolean
7071
showWorkspaceInvite: boolean
7172
setShowWorkspaceInvite: (show: boolean) => void
@@ -82,8 +83,8 @@ interface MemberInvitationCardProps {
8283
}
8384

8485
export function MemberInvitationCard({
85-
inviteEmail,
86-
setInviteEmail,
86+
inviteEmails,
87+
setInviteEmails,
8788
isInviting,
8889
showWorkspaceInvite,
8990
setShowWorkspaceInvite,
@@ -100,45 +101,26 @@ export function MemberInvitationCard({
100101
}: MemberInvitationCardProps) {
101102
const selectedCount = selectedWorkspaces.length
102103
const hasAvailableSeats = availableSeats > 0
103-
const [emailError, setEmailError] = useState<string>('')
104+
const hasValidEmails = inviteEmails.some((e) => e.isValid)
104105

105-
const validateEmailInput = (email: string) => {
106-
if (!email.trim()) {
107-
setEmailError('')
108-
return
109-
}
106+
const handleAddEmail = (value: string) => {
107+
const normalized = value.trim().toLowerCase()
108+
if (!normalized) return false
110109

111-
const validation = quickValidateEmail(email.trim())
112-
if (!validation.isValid) {
113-
setEmailError(validation.reason || 'Please enter a valid email address')
114-
} else {
115-
setEmailError('')
116-
}
117-
}
110+
const isDuplicate = inviteEmails.some((e) => e.value === normalized)
111+
if (isDuplicate) return false
118112

119-
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
120-
const value = e.target.value
121-
setInviteEmail(value)
122-
if (emailError) {
123-
setEmailError('')
124-
}
113+
const validation = quickValidateEmail(normalized)
114+
setInviteEmails([...inviteEmails, { value: normalized, isValid: validation.isValid }])
115+
return validation.isValid
125116
}
126117

127-
const handleInviteClick = () => {
128-
if (inviteEmail.trim()) {
129-
validateEmailInput(inviteEmail)
130-
const validation = quickValidateEmail(inviteEmail.trim())
131-
if (!validation.isValid) {
132-
return // Don't proceed if validation fails
133-
}
134-
}
135-
136-
onInviteMember()
118+
const handleRemoveEmail = (_value: string, index: number) => {
119+
setInviteEmails(inviteEmails.filter((_, i) => i !== index))
137120
}
138121

139122
return (
140123
<div className='overflow-hidden rounded-[6px] border border-[var(--border-1)] bg-[var(--surface-5)]'>
141-
{/* Header */}
142124
<div className='px-[14px] py-[10px]'>
143125
<h4 className='font-medium text-[14px] text-[var(--text-primary)]'>Invite Team Members</h4>
144126
<p className='text-[12px] text-[var(--text-muted)]'>
@@ -147,46 +129,18 @@ export function MemberInvitationCard({
147129
</div>
148130

149131
<div className='flex flex-col gap-[12px] border-[var(--border-1)] border-t bg-[var(--surface-4)] px-[14px] py-[12px]'>
150-
{/* Main invitation input */}
151132
<div className='flex items-start gap-[8px]'>
152133
<div className='flex-1'>
153-
{/* Hidden decoy fields to prevent browser autofill */}
154-
<input
155-
type='text'
156-
name='fakeusernameremembered'
157-
autoComplete='username'
158-
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
159-
tabIndex={-1}
160-
readOnly
161-
/>
162-
<input
163-
type='email'
164-
name='fakeemailremembered'
165-
autoComplete='email'
166-
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
167-
tabIndex={-1}
168-
readOnly
169-
/>
170-
<Input
171-
placeholder='Enter email address'
172-
value={inviteEmail}
173-
onChange={handleEmailChange}
134+
<TagInput
135+
items={inviteEmails}
136+
onAdd={handleAddEmail}
137+
onRemove={handleRemoveEmail}
138+
placeholder='Enter email addresses'
139+
placeholderWithTags='Add another email'
174140
disabled={isInviting || !hasAvailableSeats}
175-
className={cn(emailError && 'border-red-500 focus-visible:ring-red-500')}
176-
name='member_invite_field'
177-
autoComplete='off'
178-
autoCorrect='off'
179-
autoCapitalize='off'
180-
spellCheck={false}
181-
data-lpignore='true'
182-
data-form-type='other'
183-
aria-autocomplete='none'
141+
triggerKeys={['Enter', ',', ' ']}
142+
maxHeight='max-h-24'
184143
/>
185-
{emailError && (
186-
<p className='mt-1 text-[12px] text-[var(--text-error)] leading-tight'>
187-
{emailError}
188-
</p>
189-
)}
190144
</div>
191145
<Popover
192146
open={showWorkspaceInvite}
@@ -287,14 +241,13 @@ export function MemberInvitationCard({
287241
</Popover>
288242
<Button
289243
variant='tertiary'
290-
onClick={handleInviteClick}
291-
disabled={!inviteEmail || isInviting || !hasAvailableSeats}
244+
onClick={() => onInviteMember()}
245+
disabled={!hasValidEmails || isInviting || !hasAvailableSeats}
292246
>
293247
{isInviting ? 'Inviting...' : hasAvailableSeats ? 'Invite' : 'No Seats'}
294248
</Button>
295249
</div>
296250

297-
{/* Invitation error - inline */}
298251
{invitationError && (
299252
<p className='text-[12px] text-[var(--text-error)] leading-tight'>
300253
{invitationError instanceof Error && invitationError.message
@@ -303,7 +256,6 @@ export function MemberInvitationCard({
303256
</p>
304257
)}
305258

306-
{/* Success message */}
307259
{inviteSuccess && (
308260
<p className='text-[11px] text-[var(--text-success)] leading-tight'>
309261
Invitation sent successfully

0 commit comments

Comments
 (0)