Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
ack PR comments
  • Loading branch information
waleedlatif1 committed Feb 10, 2026
commit fe45ba5daece961a962b578b1395538c30c40409
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ export async function PUT(
.where(eq(workspaceInvitation.id, wsInvitation.id))

const existingPermission = await tx
.select({ id: permissions.id })
.select({ id: permissions.id, permissionType: permissions.permissionType })
.from(permissions)
.where(
and(
Expand All @@ -459,13 +459,22 @@ export async function PUT(
.then((rows) => rows[0])

if (existingPermission) {
await tx
.update(permissions)
.set({
permissionType: wsInvitation.permissions || 'read',
updatedAt: new Date(),
})
.where(eq(permissions.id, existingPermission.id))
const PERMISSION_RANK = { read: 0, write: 1, admin: 2 } as const
type PermissionLevel = keyof typeof PERMISSION_RANK
const existingRank =
PERMISSION_RANK[existingPermission.permissionType as PermissionLevel] ?? 0
const newPermission = (wsInvitation.permissions || 'read') as PermissionLevel
const newRank = PERMISSION_RANK[newPermission] ?? 0

if (newRank > existingRank) {
await tx
.update(permissions)
.set({
permissionType: newPermission,
updatedAt: new Date(),
})
.where(eq(permissions.id, existingPermission.id))
}
} else {
await tx.insert(permissions).values({
id: randomUUID(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export function McpDeploy({
})
const [parameterDescriptions, setParameterDescriptions] = useState<Record<string, string>>({})
const [pendingServerChanges, setPendingServerChanges] = useState<Set<string>>(new Set())
const [saveErrors, setSaveErrors] = useState<string[]>([])

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

onSubmittingChange?.(true)
setSaveErrors([])
try {
const nextServerToolsMap = { ...serverToolsMap }
const errors: string[] = []

for (const serverId of toAdd) {
setPendingServerChanges((prev) => new Set(prev).add(serverId))
Expand All @@ -303,6 +306,8 @@ export function McpDeploy({
onAddedToServer?.()
logger.info(`Added workflow ${workflowId} as tool to server ${serverId}`)
} catch (error) {
const serverName = servers.find((s) => s.id === serverId)?.name || serverId
errors.push(`Failed to add to ${serverName}`)
logger.error(`Failed to add tool to server ${serverId}:`, error)
} finally {
setPendingServerChanges((prev) => {
Expand All @@ -326,6 +331,8 @@ export function McpDeploy({
})
delete nextServerToolsMap[serverId]
} catch (error) {
const serverName = servers.find((s) => s.id === serverId)?.name || serverId
errors.push(`Failed to remove from ${serverName}`)
logger.error(`Failed to remove tool from server ${serverId}:`, error)
} finally {
setPendingServerChanges((prev) => {
Expand All @@ -352,19 +359,25 @@ export function McpDeploy({
parameterSchema,
})
} catch (error) {
const serverName = servers.find((s) => s.id === serverId)?.name || serverId
errors.push(`Failed to update on ${serverName}`)
logger.error(`Failed to update tool on server ${serverId}:`, error)
}
}
}
Comment thread
waleedlatif1 marked this conversation as resolved.

setServerToolsMap(nextServerToolsMap)
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
setDraftSelectedServerIds(null)
setSavedValues({
toolName,
toolDescription,
parameterDescriptions: { ...parameterDescriptions },
})
onCanSaveChange?.(false)
if (errors.length > 0) {
setSaveErrors(errors)
} else {
setDraftSelectedServerIds(null)
setSavedValues({
toolName,
toolDescription,
parameterDescriptions: { ...parameterDescriptions },
})
onCanSaveChange?.(false)
}
onSubmittingChange?.(false)
Comment thread
waleedlatif1 marked this conversation as resolved.
} catch (error) {
logger.error('Failed to save tool configuration:', error)
Expand All @@ -381,6 +394,7 @@ export function McpDeploy({
serverToolsMap,
workspaceId,
workflowId,
servers,
addToolMutation,
deleteToolMutation,
updateToolMutation,
Expand Down Expand Up @@ -571,10 +585,14 @@ export function McpDeploy({
)}
</div>

{addToolMutation.isError && (
<p className='mt-[6.5px] text-[12px] text-[var(--text-error)]'>
{addToolMutation.error?.message || 'Failed to add tool'}
</p>
{saveErrors.length > 0 && (
<div className='mt-[6.5px] flex flex-col gap-[2px]'>
{saveErrors.map((error) => (
<p key={error} className='text-[12px] text-[var(--text-error)]'>
{error}
</p>
))}
</div>
)}
</form>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
'use client'

import React, { useState } from 'react'
import React from 'react'
import { ChevronDown } from 'lucide-react'
import {
Button,
ButtonGroup,
ButtonGroupItem,
Checkbox,
Input,
Popover,
PopoverContent,
PopoverItem,
PopoverTrigger,
TagInput,
type TagItem,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
Expand Down Expand Up @@ -64,8 +65,8 @@ const PermissionSelector = React.memo<PermissionSelectorProps>(
PermissionSelector.displayName = 'PermissionSelector'

interface MemberInvitationCardProps {
inviteEmail: string
setInviteEmail: (email: string) => void
inviteEmails: TagItem[]
setInviteEmails: (emails: TagItem[]) => void
isInviting: boolean
showWorkspaceInvite: boolean
setShowWorkspaceInvite: (show: boolean) => void
Expand All @@ -82,8 +83,8 @@ interface MemberInvitationCardProps {
}

export function MemberInvitationCard({
inviteEmail,
setInviteEmail,
inviteEmails,
setInviteEmails,
isInviting,
showWorkspaceInvite,
setShowWorkspaceInvite,
Expand All @@ -100,45 +101,26 @@ export function MemberInvitationCard({
}: MemberInvitationCardProps) {
const selectedCount = selectedWorkspaces.length
const hasAvailableSeats = availableSeats > 0
const [emailError, setEmailError] = useState<string>('')
const hasValidEmails = inviteEmails.some((e) => e.isValid)

const validateEmailInput = (email: string) => {
if (!email.trim()) {
setEmailError('')
return
}
const handleAddEmail = (value: string) => {
const normalized = value.trim().toLowerCase()
if (!normalized) return false

const validation = quickValidateEmail(email.trim())
if (!validation.isValid) {
setEmailError(validation.reason || 'Please enter a valid email address')
} else {
setEmailError('')
}
}
const isDuplicate = inviteEmails.some((e) => e.value === normalized)
if (isDuplicate) return false

const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setInviteEmail(value)
if (emailError) {
setEmailError('')
}
const validation = quickValidateEmail(normalized)
setInviteEmails([...inviteEmails, { value: normalized, isValid: validation.isValid }])
return validation.isValid
}

const handleInviteClick = () => {
if (inviteEmail.trim()) {
validateEmailInput(inviteEmail)
const validation = quickValidateEmail(inviteEmail.trim())
if (!validation.isValid) {
return // Don't proceed if validation fails
}
}

onInviteMember()
const handleRemoveEmail = (_value: string, index: number) => {
setInviteEmails(inviteEmails.filter((_, i) => i !== index))
}

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

<div className='flex flex-col gap-[12px] border-[var(--border-1)] border-t bg-[var(--surface-4)] px-[14px] py-[12px]'>
{/* Main invitation input */}
<div className='flex items-start gap-[8px]'>
<div className='flex-1'>
{/* Hidden decoy fields to prevent browser autofill */}
<input
type='text'
name='fakeusernameremembered'
autoComplete='username'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<input
type='email'
name='fakeemailremembered'
autoComplete='email'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<Input
placeholder='Enter email address'
value={inviteEmail}
onChange={handleEmailChange}
<TagInput
items={inviteEmails}
onAdd={handleAddEmail}
onRemove={handleRemoveEmail}
placeholder='Enter email addresses'
placeholderWithTags='Add another email'
disabled={isInviting || !hasAvailableSeats}
className={cn(emailError && 'border-red-500 focus-visible:ring-red-500')}
name='member_invite_field'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck={false}
data-lpignore='true'
data-form-type='other'
aria-autocomplete='none'
triggerKeys={['Enter', ',', ' ']}
maxHeight='max-h-24'
/>
{emailError && (
<p className='mt-1 text-[12px] text-[var(--text-error)] leading-tight'>
{emailError}
</p>
)}
</div>
<Popover
open={showWorkspaceInvite}
Expand Down Expand Up @@ -287,14 +241,13 @@ export function MemberInvitationCard({
</Popover>
<Button
variant='tertiary'
onClick={handleInviteClick}
disabled={!inviteEmail || isInviting || !hasAvailableSeats}
onClick={() => onInviteMember()}
disabled={!hasValidEmails || isInviting || !hasAvailableSeats}
>
{isInviting ? 'Inviting...' : hasAvailableSeats ? 'Invite' : 'No Seats'}
</Button>
</div>

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

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