diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts index 85fa235c57..1506a89164 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/route.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts @@ -244,16 +244,40 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ const emailsToInvite = newEmails.filter((email: string) => !pendingEmails.includes(email)) if (emailsToInvite.length === 0) { + const isSingleEmail = processedEmails.length === 1 + const existingMembersEmails = processedEmails.filter((email: string) => + existingEmails.includes(email) + ) + const pendingInvitationEmails = processedEmails.filter((email: string) => + pendingEmails.includes(email) + ) + + if (isSingleEmail) { + if (existingMembersEmails.length > 0) { + return NextResponse.json( + { + error: 'Failed to send invitation. User is already a part of the organization.', + }, + { status: 400 } + ) + } + if (pendingInvitationEmails.length > 0) { + return NextResponse.json( + { + error: + 'Failed to send invitation. A pending invitation already exists for this email.', + }, + { status: 400 } + ) + } + } + return NextResponse.json( { - error: 'All emails are already members or have pending invitations', + error: 'All emails are already members or have pending invitations.', details: { - existingMembers: processedEmails.filter((email: string) => - existingEmails.includes(email) - ), - pendingInvitations: processedEmails.filter((email: string) => - pendingEmails.includes(email) - ), + existingMembers: existingMembersEmails, + pendingInvitations: pendingInvitationEmails, }, }, { status: 400 } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/account/account.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/account/account.tsx index 40ca2e22d2..d2747bb063 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/account/account.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/account/account.tsx @@ -232,7 +232,11 @@ export function Account(_props: AccountProps) {

{profile?.name || ''}

{profile?.email || ''}

- {uploadError &&

{uploadError}

} + {uploadError && ( +

+ {uploadError} +

+ )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/api-keys/api-keys.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/api-keys/api-keys.tsx index 5a9b28a485..0ace598589 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/api-keys/api-keys.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/api-keys/api-keys.tsx @@ -512,9 +512,9 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) { autoFocus /> {createError && ( -
+

{createError} -

+

)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/creator-profile/creator-profile.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/creator-profile/creator-profile.tsx index ffbda998ce..cb9cbd063c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/creator-profile/creator-profile.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/creator-profile/creator-profile.tsx @@ -291,7 +291,11 @@ export function CreatorProfile() { />
- {uploadError &&

{uploadError}

} + {uploadError && ( +

+ {uploadError} +

+ )}

PNG or JPEG (max 5MB)

@@ -411,9 +415,9 @@ export function CreatorProfile() { {/* Error Message */} {saveError && (
-
+

{saveError} -

+

)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/custom-tools/custom-tools.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/custom-tools/custom-tools.tsx index b26ac39813..c05f1b947b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/custom-tools/custom-tools.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/custom-tools/custom-tools.tsx @@ -1,7 +1,7 @@ 'use client' import { useState } from 'react' -import { AlertCircle, Plus, Search } from 'lucide-react' +import { Plus, Search } from 'lucide-react' import { useParams } from 'next/navigation' import { Button, @@ -13,7 +13,7 @@ import { ModalHeader, ModalTitle, } from '@/components/emcn' -import { Alert, AlertDescription, Input, Skeleton } from '@/components/ui' +import { Input, Skeleton } from '@/components/ui' import { createLogger } from '@/lib/logs/console/logger' import { CustomToolModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal' import { useCustomTools, useDeleteCustomTool } from '@/hooks/queries/custom-tools' @@ -84,7 +84,6 @@ export function CustomTools() { setShowDeleteDialog(false) try { - // Pass null workspaceId for user-scoped tools (legacy tools without workspaceId) await deleteToolMutation.mutateAsync({ workspaceId: tool.workspaceId ?? null, toolId: toolToDelete.id, @@ -105,7 +104,6 @@ export function CustomTools() { const handleToolSaved = () => { setShowAddForm(false) setEditingTool(null) - // React Query will automatically refetch via cache invalidation refetchTools() } @@ -113,16 +111,6 @@ export function CustomTools() {
{/* Fixed Header with Search */}
- {/* Error Alert - only show when modal is not open */} - {error && !showAddForm && !editingTool && ( - - - - {error instanceof Error ? error.message : 'An error occurred'} - - - )} - {/* Search Input */} {isLoading ? ( @@ -148,6 +136,12 @@ export function CustomTools() {
+ ) : error ? ( +
+

+ {error instanceof Error ? error.message : 'Failed to load tools'} +

+
) : tools.length === 0 && !showAddForm && !editingTool ? (
Click "Create Tool" below to get started diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/files/files.tsx index fd1acde0a9..9c8e8f1152 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/files/files.tsx @@ -273,9 +273,9 @@ export function Files() { {/* Error message */} {uploadError && (
-
+

{uploadError} -

+

)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/add-server-form.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/add-server-form.tsx index b670c1ead0..331c480ea1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/add-server-form.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/components/add-server-form.tsx @@ -253,9 +253,9 @@ export function AddServerForm({
{/* Error message above buttons */} {testResult && !testResult.success && ( -
+

{testResult.error || testResult.message} -

+

)} {/* Buttons row */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx index b0c10a5486..8b45d2050b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/mcp/mcp.tsx @@ -1,10 +1,10 @@ 'use client' import { useCallback, useRef, useState } from 'react' -import { AlertCircle, Plus, Search } from 'lucide-react' +import { Plus, Search } from 'lucide-react' import { useParams } from 'next/navigation' import { Button } from '@/components/emcn' -import { Alert, AlertDescription, Input, Skeleton } from '@/components/ui' +import { Input, Skeleton } from '@/components/ui' import { createLogger } from '@/lib/logs/console/logger' import { createMcpToolId } from '@/lib/mcp/utils' import { checkEnvVarTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' @@ -24,7 +24,6 @@ export function MCP() { const params = useParams() const workspaceId = params.workspaceId as string - // React Query hooks const { data: servers = [], isLoading: serversLoading, @@ -42,7 +41,7 @@ export function MCP() { transport: 'streamable-http', url: '', timeout: 30000, - headers: {}, // Start with no headers + headers: {}, }) const [showEnvVars, setShowEnvVars] = useState(false) @@ -207,7 +206,6 @@ export function MCP() { try { await deleteServerMutation.mutateAsync({ workspaceId, serverId }) - // TanStack Query mutations automatically invalidate and refetch tools logger.info(`Removed MCP server: ${serverId}`) } catch (error) { @@ -264,27 +262,23 @@ export function MCP() { />
)} - - {/* Error Alert */} - {(toolsError || serversError) && ( - - - - {toolsError instanceof Error - ? toolsError.message - : serversError instanceof Error - ? serversError.message - : 'An error occurred'} - - - )}
{/* Scrollable Content */}
{/* Server List */} - {serversLoading ? ( + {toolsError || serversError ? ( +
+

+ {toolsError instanceof Error + ? toolsError.message + : serversError instanceof Error + ? serversError.message + : 'Failed to load MCP servers'} +

+
+ ) : serversLoading ? (
@@ -342,7 +336,6 @@ export function MCP() { ) : (
{filteredServers.map((server: any) => { - // Add defensive checks for server properties if (!server || !server.id) { return null } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/sso/sso.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/sso/sso.tsx index 016bfda858..26f90dba4f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/sso/sso.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/sso/sso.tsx @@ -2,8 +2,8 @@ import { useState } from 'react' import { Check, ChevronDown, Copy, Eye, EyeOff } from 'lucide-react' -import { Button, Combobox } from '@/components/emcn' -import { Alert, AlertDescription, Input, Label } from '@/components/ui' +import { Button, Combobox, Input, Label } from '@/components/emcn' +import { Alert, AlertDescription } from '@/components/ui' import { Skeleton } from '@/components/ui/skeleton' import { useSession } from '@/lib/auth-client' import { isBillingEnabled } from '@/lib/environment' @@ -79,7 +79,6 @@ export function SSO() { const { data: subscriptionData } = useSubscriptionData() const activeOrganization = orgsData?.activeOrganization - // Determine if we should fetch SSO providers const userEmail = session?.user?.email const userId = session?.user?.id const userRole = getUserRole(activeOrganization, userEmail) @@ -89,14 +88,12 @@ export function SSO() { const subscriptionStatus = getSubscriptionStatus(subscriptionData?.data) const hasEnterprisePlan = subscriptionStatus.isEnterprise - // Use React Query to fetch SSO providers const { data: providersData, isLoading: isLoadingProviders } = useSSOProviders() const providers = providersData?.providers || [] const isSSOProviderOwner = !isBillingEnabled && userId ? providers.some((p: any) => p.userId === userId) : null - // Use mutation hook for configuring SSO const configureSSOMutation = useConfigureSSO() const [error, setError] = useState(null) @@ -331,12 +328,10 @@ export function SSO() { } } - // Use the mutation hook - this will automatically invalidate the cache await configureSSOMutation.mutateAsync(requestBody) logger.info('SSO provider configured', { providerId: formData.providerId }) - // Reset form setFormData({ providerType: 'oidc', providerId: '', @@ -408,7 +403,6 @@ export function SSO() { const handleReconfigure = (provider: SSOProvider) => { try { - // Parse config based on provider type let clientId = '' let clientSecret = '' let scopes = 'openid,profile,email' @@ -480,14 +474,7 @@ export function SSO() { />
- {error && ( - - {error} - - )} - {showStatus ? ( - // SSO Provider Status View
{providers.map((provider: SSOProvider) => (
@@ -558,7 +545,6 @@ export function SSO() { ))}
) : ( - // SSO Configuration Form <> {hasProviders && (
@@ -631,9 +617,9 @@ export function SSO() { )} /> {showErrors && errors.providerId.length > 0 && ( -
-

{errors.providerId.join(' ')}

-
+

+ {errors.providerId.join(' ')} +

)}

Select a pre-configured provider ID from the trusted providers list @@ -662,9 +648,9 @@ export function SSO() { )} /> {showErrors && errors.issuerUrl.length > 0 && ( -

-

{errors.issuerUrl.join(' ')}

-
+

+ {errors.issuerUrl.join(' ')} +

)}

@@ -691,9 +677,9 @@ export function SSO() { )} /> {showErrors && errors.domain.length > 0 && ( -
-

{errors.domain.join(' ')}

-
+

+ {errors.domain.join(' ')} +

)}
@@ -722,9 +708,9 @@ export function SSO() { )} /> {showErrors && errors.clientId.length > 0 && ( -
-

{errors.clientId.join(' ')}

-
+

+ {errors.clientId.join(' ')} +

)}
@@ -775,9 +761,9 @@ export function SSO() {
{showErrors && errors.clientSecret.length > 0 && ( -
-

{errors.clientSecret.join(' ')}

-
+

+ {errors.clientSecret.join(' ')} +

)}
@@ -800,9 +786,9 @@ export function SSO() { )} /> {showErrors && errors.scopes.length > 0 && ( -
-

{errors.scopes.join(' ')}

-
+

+ {errors.scopes.join(' ')} +

)}

Comma-separated list of OIDC scopes to request @@ -830,9 +816,9 @@ export function SSO() { )} /> {showErrors && errors.entryPoint.length > 0 && ( -

-

{errors.entryPoint.join(' ')}

-
+

+ {errors.entryPoint.join(' ')} +

)}

@@ -856,9 +842,9 @@ export function SSO() { rows={4} /> {showErrors && errors.cert.length > 0 && ( -
-

{errors.cert.join(' ')}

-
+

+ {errors.cert.join(' ')} +

)}

@@ -964,6 +950,12 @@ export function SSO() { )} + {error && ( +

+ {error} +

+ )} + + ))}
) @@ -71,12 +68,8 @@ interface MemberInvitationCardProps { inviteSuccess: boolean availableSeats?: number maxSeats?: number -} - -function ButtonSkeleton() { - return ( -
- ) + invitationError?: Error | null + isLoadingWorkspaces?: boolean } export function MemberInvitationCard({ @@ -93,12 +86,13 @@ export function MemberInvitationCard({ inviteSuccess, availableSeats = 0, maxSeats = 0, + invitationError = null, + isLoadingWorkspaces = false, }: MemberInvitationCardProps) { const selectedCount = selectedWorkspaces.length const hasAvailableSeats = availableSeats > 0 const [emailError, setEmailError] = useState('') - // Email validation function using existing lib const validateEmailInput = (email: string) => { if (!email.trim()) { setEmailError('') @@ -116,14 +110,12 @@ export function MemberInvitationCard({ const handleEmailChange = (e: React.ChangeEvent) => { const value = e.target.value setInviteEmail(value) - // Clear error when user starts typing again if (emailError) { setEmailError('') } } const handleInviteClick = () => { - // Validate email before proceeding if (inviteEmail.trim()) { validateEmailInput(inviteEmail) const validation = quickValidateEmail(inviteEmail.trim()) @@ -132,7 +124,6 @@ export function MemberInvitationCard({ } } - // If validation passes or email is empty, proceed with original invite onInviteMember() } @@ -163,114 +154,118 @@ export function MemberInvitationCard({

)}
- + + + {isLoadingWorkspaces ? ( +
+

Loading...

+
+ ) : userWorkspaces.length === 0 ? ( +
+

No workspaces available

+
+ ) : ( +
+ {userWorkspaces.map((workspace) => { + const isSelected = selectedWorkspaces.some( + (w) => w.workspaceId === workspace.id + ) + const selectedWorkspace = selectedWorkspaces.find( + (w) => w.workspaceId === workspace.id + ) + + return ( +
+ { + if (isSelected) { + onWorkspaceToggle(workspace.id, '') + } else { + onWorkspaceToggle(workspace.id, 'read') + } + }} + active={isSelected} + disabled={isInviting} + > + + {workspace.name} + + {isSelected && ( +
+ Access: + onWorkspaceToggle(workspace.id, permission)} + disabled={isInviting} + /> +
+ )} +
+ ) + })} +
)} - /> - Workspaces - +
+
- {/* Workspace selection - collapsible */} - {showWorkspaceInvite && ( -
-
-
-
Workspace Access
- (Optional) -
- {selectedCount > 0 && ( - {selectedCount} selected - )} -
- - {userWorkspaces.length === 0 ? ( -
-

No workspaces available

-
- ) : ( -
- {userWorkspaces.map((workspace) => { - const isSelected = selectedWorkspaces.some((w) => w.workspaceId === workspace.id) - const selectedWorkspace = selectedWorkspaces.find( - (w) => w.workspaceId === workspace.id - ) - - return ( -
-
-
- { - if (checked) { - onWorkspaceToggle(workspace.id, 'read') - } else { - onWorkspaceToggle(workspace.id, '') - } - }} - disabled={isInviting} - /> - - {workspace.isOwner && ( - - Owner - - )} -
-
- - {isSelected && ( - onWorkspaceToggle(workspace.id, permission)} - disabled={isInviting} - className='w-auto' - /> - )} -
- ) - })} -
- )} -
+ {/* Invitation error - inline */} + {invitationError && ( +

+ {invitationError instanceof Error && invitationError.message + ? invitationError.message + : String(invitationError)} +

)} {/* Success message */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/no-organization-view/no-organization-view.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/no-organization-view/no-organization-view.tsx index 0ce2c18f7f..cc8333a47e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/no-organization-view/no-organization-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/no-organization-view/no-organization-view.tsx @@ -8,7 +8,6 @@ import { ModalHeader, ModalTitle, } from '@/components/emcn' -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' @@ -87,22 +86,22 @@ export function NoOrganizationView({ - {error && ( - - Error - {error} - - )} - -
- +
+ {error && ( +

+ {error} +

+ )} +
+ +
@@ -117,13 +116,6 @@ export function NoOrganizationView({
- {error && ( - - Error - {error} - - )} -
+ {error && ( +

+ {error} +

+ )} +