feat(readme): added deepwiki to readme, consolidated utils (#2856)

* feat(readme): added deepwiki to readme, consolidated utils

* standardized all modals

* updated modal copy

* standardized modals

* streamlined all error msg patterns
This commit is contained in:
Waleed
2026-01-16 16:07:31 -08:00
committed by GitHub
parent aa80116b99
commit 3768c6379c
49 changed files with 307 additions and 196 deletions

View File

@@ -10,7 +10,7 @@
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/sim.ai-6F3DFA" alt="Sim.ai"></a>
<a href="https://discord.gg/Hr4UWYEcTT" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
<a href="https://x.com/simdotai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/twitter/follow/simstudioai?style=social" alt="Twitter"></a>
<a href="https://docs.sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Docs-6F3DFA.svg" alt="Documentation"></a>
<a href="https://docs.sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Docs-6F3DFA.svg" alt="Documentation"></a> <a href="https://deepwiki.com/simstudioai/sim" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/DeepWiki-1E90FF.svg" alt="DeepWiki"></a>
</p>
<p align="center">

View File

@@ -11,6 +11,7 @@ import { preprocessExecution } from '@/lib/execution/preprocessing'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
import { createStreamingResponse } from '@/lib/workflows/streaming/streaming'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import { setFormAuthCookie, validateFormAuth } from '@/app/api/form/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -35,10 +36,7 @@ async function getWorkflowInputSchema(workflowId: string): Promise<any[]> {
.from(workflowBlocks)
.where(eq(workflowBlocks.workflowId, workflowId))
const startBlock = blocks.find(
(block) =>
block.type === 'starter' || block.type === 'start_trigger' || block.type === 'input_trigger'
)
const startBlock = blocks.find((block) => isValidStartBlockType(block.type))
if (!startBlock) {
return []

View File

@@ -77,7 +77,7 @@ export function DeleteChunkModal({
</p>
</ModalBody>
<ModalFooter>
<Button variant='active' disabled={isDeleting} onClick={onClose}>
<Button variant='default' disabled={isDeleting} onClick={onClose}>
Cancel
</Button>
<Button variant='destructive' onClick={handleDeleteChunk} disabled={isDeleting}>

View File

@@ -392,7 +392,7 @@ export function DocumentTagsModal({
return (
<Modal open={open} onOpenChange={handleClose}>
<ModalContent>
<ModalContent size='sm'>
<ModalHeader>
<div className='flex items-center justify-between'>
<span>Document Tags</span>
@@ -486,7 +486,7 @@ export function DocumentTagsModal({
/>
)}
{tagNameConflict && (
<span className='text-[11px] text-[var(--text-error)]'>
<span className='text-[12px] text-[var(--text-error)]'>
A tag with this name already exists
</span>
)}
@@ -639,7 +639,7 @@ export function DocumentTagsModal({
/>
)}
{tagNameConflict && (
<span className='text-[11px] text-[var(--text-error)]'>
<span className='text-[12px] text-[var(--text-error)]'>
A tag with this name already exists
</span>
)}

View File

@@ -1161,15 +1161,19 @@ export function Document({
<ModalHeader>Delete Document</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete "{effectiveDocumentName}"? This will permanently
delete the document and all {documentData?.chunkCount ?? 0} chunk
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{effectiveDocumentName}
</span>
? This will permanently delete the document and all {documentData?.chunkCount ?? 0}{' '}
chunk
{documentData?.chunkCount === 1 ? '' : 's'} within it.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
</ModalBody>
<ModalFooter>
<Button
variant='active'
variant='default'
onClick={() => setShowDeleteDocumentDialog(false)}
disabled={isDeletingDocument}
>

View File

@@ -1523,15 +1523,16 @@ export function KnowledgeBase({
<ModalHeader>Delete Knowledge Base</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete "{knowledgeBaseName}"? This will permanently delete
the knowledge base and all {pagination.total} document
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{knowledgeBaseName}</span>?
This will permanently delete the knowledge base and all {pagination.total} document
{pagination.total === 1 ? '' : 's'} within it.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
</ModalBody>
<ModalFooter>
<Button
variant='active'
variant='default'
onClick={() => setShowDeleteDialog(false)}
disabled={isDeleting}
>
@@ -1549,14 +1550,16 @@ export function KnowledgeBase({
<ModalHeader>Delete Document</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete "
{documents.find((doc) => doc.id === documentToDelete)?.filename ?? 'this document'}"?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{documents.find((doc) => doc.id === documentToDelete)?.filename ?? 'this document'}
</span>
? <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
</ModalBody>
<ModalFooter>
<Button
variant='active'
variant='default'
onClick={() => {
setShowDeleteDocumentModal(false)
setDocumentToDelete(null)
@@ -1582,7 +1585,7 @@ export function KnowledgeBase({
</p>
</ModalBody>
<ModalFooter>
<Button variant='active' onClick={() => setShowBulkDeleteModal(false)}>
<Button variant='default' onClick={() => setShowBulkDeleteModal(false)}>
Cancel
</Button>
<Button variant='destructive' onClick={confirmBulkDelete} disabled={isBulkOperating}>

View File

@@ -221,14 +221,14 @@ export function AddDocumentsModal({
return (
<Modal open={open} onOpenChange={handleClose}>
<ModalContent>
<ModalContent size='md'>
<ModalHeader>Add Documents</ModalHeader>
<ModalBody>
<div className='min-h-0 flex-1 overflow-y-auto'>
<div className='space-y-[12px]'>
{fileError && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>{fileError}</p>
<p className='text-[12px] text-[var(--text-error)] leading-tight'>{fileError}</p>
)}
<div className='flex flex-col gap-[8px]'>
@@ -336,7 +336,7 @@ export function AddDocumentsModal({
<ModalFooter>
<div className='flex w-full items-center justify-between gap-[12px]'>
{uploadError ? (
<p className='min-w-0 flex-1 truncate text-[11px] text-[var(--text-error)] leading-tight'>
<p className='min-w-0 flex-1 truncate text-[12px] text-[var(--text-error)] leading-tight'>
{uploadError.message}
</p>
) : (

View File

@@ -306,7 +306,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
return (
<>
<Modal open={open} onOpenChange={handleClose}>
<ModalContent>
<ModalContent size='sm'>
<ModalHeader>
<div className='flex items-center justify-between'>
<span>Tags</span>
@@ -400,7 +400,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
}}
/>
{tagNameConflict && (
<span className='text-[11px] text-[var(--text-error)]'>
<span className='text-[12px] text-[var(--text-error)]'>
A tag with this name already exists
</span>
)}
@@ -417,7 +417,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
placeholder='Select type'
/>
{!hasAvailableSlots(createTagForm.fieldType) && (
<span className='text-[11px] text-[var(--text-error)]'>
<span className='text-[12px] text-[var(--text-error)]'>
No available slots for this type. Choose a different type.
</span>
)}

View File

@@ -77,7 +77,7 @@ export function RenameDocumentModal({
return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent>
<ModalContent size='sm'>
<ModalHeader>Rename Document</ModalHeader>
<form onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>
<ModalBody className='!pb-[16px]'>
@@ -108,7 +108,7 @@ export function RenameDocumentModal({
<ModalFooter>
<div className='flex w-full items-center justify-between gap-[12px]'>
{error ? (
<p className='min-w-0 flex-1 truncate text-[11px] text-[var(--text-error)] leading-tight'>
<p className='min-w-0 flex-1 truncate text-[12px] text-[var(--text-error)] leading-tight'>
{error}
</p>
) : (

View File

@@ -332,7 +332,7 @@ export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) {
return (
<Modal open={open} onOpenChange={handleClose}>
<ModalContent>
<ModalContent size='lg'>
<ModalHeader>Create Knowledge Base</ModalHeader>
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
@@ -528,7 +528,7 @@ export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) {
)}
{fileError && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>{fileError}</p>
<p className='text-[12px] text-[var(--text-error)] leading-tight'>{fileError}</p>
)}
</div>
</div>
@@ -537,7 +537,7 @@ export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) {
<ModalFooter>
<div className='flex w-full items-center justify-between gap-[12px]'>
{submitStatus?.type === 'error' || uploadError ? (
<p className='min-w-0 flex-1 truncate text-[11px] text-[var(--text-error)] leading-tight'>
<p className='min-w-0 flex-1 truncate text-[12px] text-[var(--text-error)] leading-tight'>
{uploadError?.message || submitStatus?.message}
</p>
) : (

View File

@@ -38,7 +38,7 @@ export function DeleteKnowledgeBaseModal({
}: DeleteKnowledgeBaseModalProps) {
return (
<Modal open={isOpen} onOpenChange={onClose}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete Knowledge Base</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
@@ -55,7 +55,7 @@ export function DeleteKnowledgeBaseModal({
</p>
</ModalBody>
<ModalFooter>
<Button variant='active' onClick={onClose} disabled={isDeleting}>
<Button variant='default' onClick={onClose} disabled={isDeleting}>
Cancel
</Button>
<Button variant='destructive' onClick={onConfirm} disabled={isDeleting}>

View File

@@ -98,7 +98,7 @@ export function EditKnowledgeBaseModal({
return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent>
<ModalContent size='sm'>
<ModalHeader>Edit Knowledge Base</ModalHeader>
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
@@ -118,7 +118,7 @@ export function EditKnowledgeBaseModal({
data-form-type='other'
/>
{errors.name && (
<p className='text-[11px] text-[var(--text-error)]'>{errors.name.message}</p>
<p className='text-[12px] text-[var(--text-error)]'>{errors.name.message}</p>
)}
</div>
@@ -132,7 +132,7 @@ export function EditKnowledgeBaseModal({
className={cn(errors.description && 'border-[var(--text-error)]')}
/>
{errors.description && (
<p className='text-[11px] text-[var(--text-error)]'>
<p className='text-[12px] text-[var(--text-error)]'>
{errors.description.message}
</p>
)}
@@ -143,7 +143,7 @@ export function EditKnowledgeBaseModal({
<ModalFooter>
<div className='flex w-full items-center justify-between gap-[12px]'>
{error ? (
<p className='min-w-0 flex-1 truncate text-[11px] text-[var(--text-error)] leading-tight'>
<p className='min-w-0 flex-1 truncate text-[12px] text-[var(--text-error)] leading-tight'>
{error}
</p>
) : (

View File

@@ -112,7 +112,7 @@ export function SlackChannelSelector({
{selectedChannel.isPrivate ? 'Private' : 'Public'} channel: #{selectedChannel.name}
</p>
)}
{error && <p className='text-[11px] text-[var(--text-error)]'>{error}</p>}
{error && <p className='text-[12px] text-[var(--text-error)]'>{error}</p>}
</div>
)
}

View File

@@ -634,7 +634,7 @@ export function NotificationSettings({
}}
/>
{formErrors.webhookUrl && (
<p className='text-[11px] text-[var(--text-error)]'>{formErrors.webhookUrl}</p>
<p className='text-[12px] text-[var(--text-error)]'>{formErrors.webhookUrl}</p>
)}
</div>
<div className='flex flex-col gap-[8px]'>
@@ -660,7 +660,7 @@ export function NotificationSettings({
placeholderWithTags='Add email'
/>
{formErrors.emailRecipients && (
<p className='text-[11px] text-[var(--text-error)]'>{formErrors.emailRecipients}</p>
<p className='text-[12px] text-[var(--text-error)]'>{formErrors.emailRecipients}</p>
)}
</div>
)}
@@ -707,7 +707,7 @@ export function NotificationSettings({
/>
)}
{formErrors.slackAccountId && (
<p className='text-[11px] text-[var(--text-error)]'>
<p className='text-[12px] text-[var(--text-error)]'>
{formErrors.slackAccountId}
</p>
)}
@@ -776,7 +776,7 @@ export function NotificationSettings({
allOptionLabel='All levels'
/>
{formErrors.levelFilter && (
<p className='text-[11px] text-[var(--text-error)]'>{formErrors.levelFilter}</p>
<p className='text-[12px] text-[var(--text-error)]'>{formErrors.levelFilter}</p>
)}
</div>
@@ -822,7 +822,7 @@ export function NotificationSettings({
allOptionLabel='All triggers'
/>
{formErrors.triggerFilter && (
<p className='text-[11px] text-[var(--text-error)]'>{formErrors.triggerFilter}</p>
<p className='text-[12px] text-[var(--text-error)]'>{formErrors.triggerFilter}</p>
)}
</div>
@@ -938,7 +938,7 @@ export function NotificationSettings({
}
/>
{formErrors.consecutiveFailures && (
<p className='text-[11px] text-[var(--text-error)]'>
<p className='text-[12px] text-[var(--text-error)]'>
{formErrors.consecutiveFailures}
</p>
)}
@@ -962,7 +962,7 @@ export function NotificationSettings({
}
/>
{formErrors.failureRatePercent && (
<p className='text-[11px] text-[var(--text-error)]'>
<p className='text-[12px] text-[var(--text-error)]'>
{formErrors.failureRatePercent}
</p>
)}
@@ -982,7 +982,7 @@ export function NotificationSettings({
}
/>
{formErrors.windowHours && (
<p className='text-[11px] text-[var(--text-error)]'>{formErrors.windowHours}</p>
<p className='text-[12px] text-[var(--text-error)]'>{formErrors.windowHours}</p>
)}
</div>
</div>
@@ -1004,7 +1004,7 @@ export function NotificationSettings({
}
/>
{formErrors.durationThresholdMs && (
<p className='text-[11px] text-[var(--text-error)]'>
<p className='text-[12px] text-[var(--text-error)]'>
{formErrors.durationThresholdMs}
</p>
)}
@@ -1028,7 +1028,7 @@ export function NotificationSettings({
}
/>
{formErrors.latencySpikePercent && (
<p className='text-[11px] text-[var(--text-error)]'>
<p className='text-[12px] text-[var(--text-error)]'>
{formErrors.latencySpikePercent}
</p>
)}
@@ -1048,7 +1048,7 @@ export function NotificationSettings({
}
/>
{formErrors.windowHours && (
<p className='text-[11px] text-[var(--text-error)]'>{formErrors.windowHours}</p>
<p className='text-[12px] text-[var(--text-error)]'>{formErrors.windowHours}</p>
)}
</div>
</div>
@@ -1071,7 +1071,7 @@ export function NotificationSettings({
}
/>
{formErrors.costThresholdDollars && (
<p className='text-[11px] text-[var(--text-error)]'>
<p className='text-[12px] text-[var(--text-error)]'>
{formErrors.costThresholdDollars}
</p>
)}
@@ -1094,7 +1094,7 @@ export function NotificationSettings({
}
/>
{formErrors.inactivityHours && (
<p className='text-[11px] text-[var(--text-error)]'>{formErrors.inactivityHours}</p>
<p className='text-[12px] text-[var(--text-error)]'>{formErrors.inactivityHours}</p>
)}
</div>
)}
@@ -1116,7 +1116,7 @@ export function NotificationSettings({
}
/>
{formErrors.errorCountThreshold && (
<p className='text-[11px] text-[var(--text-error)]'>
<p className='text-[12px] text-[var(--text-error)]'>
{formErrors.errorCountThreshold}
</p>
)}
@@ -1136,7 +1136,7 @@ export function NotificationSettings({
}
/>
{formErrors.windowHours && (
<p className='text-[11px] text-[var(--text-error)]'>{formErrors.windowHours}</p>
<p className='text-[12px] text-[var(--text-error)]'>{formErrors.windowHours}</p>
)}
</div>
</div>
@@ -1261,7 +1261,7 @@ export function NotificationSettings({
</Modal>
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete Notification</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>

View File

@@ -2,6 +2,7 @@ import { memo, useCallback } from 'react'
import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, LogOut } from 'lucide-react'
import { Button, Copy, Tooltip, Trash2 } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -97,7 +98,7 @@ export const ActionBar = memo(
const userPermissions = useUserPermissionsContext()
const isStartBlock = blockType === 'starter' || blockType === 'start_trigger'
const isStartBlock = isValidStartBlockType(blockType)
const isResponseBlock = blockType === 'response'
const isNoteBlock = blockType === 'note'
const isSubflowBlock = blockType === 'loop' || blockType === 'parallel'

View File

@@ -8,6 +8,7 @@ import {
PopoverDivider,
PopoverItem,
} from '@/components/emcn'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
/**
* Block information for context menu actions
@@ -73,9 +74,7 @@ export function BlockMenu({
const allEnabled = selectedBlocks.every((b) => b.enabled)
const allDisabled = selectedBlocks.every((b) => !b.enabled)
const hasStarterBlock = selectedBlocks.some(
(b) => b.type === 'starter' || b.type === 'start_trigger'
)
const hasStarterBlock = selectedBlocks.some((b) => isValidStartBlockType(b.type))
const allNoteBlocks = selectedBlocks.every((b) => b.type === 'note')
const isSubflow =
isSingleBlock && (selectedBlocks[0]?.type === 'loop' || selectedBlocks[0]?.type === 'parallel')

View File

@@ -995,7 +995,7 @@ export function Chat() {
<div className='flex items-start gap-2'>
<AlertCircle className='mt-0.5 h-3 w-3 shrink-0 text-[var(--text-error)]' />
<div className='flex-1'>
<div className='mb-1 font-medium text-[11px] text-[var(--text-error)]'>
<div className='mb-1 font-medium text-[12px] text-[var(--text-error)]'>
File upload error
</div>
<div className='space-y-1'>

View File

@@ -557,7 +557,7 @@ function IdentifierInput({
)}
</div>
</div>
{error && <p className='mt-[6.5px] text-[11px] text-[var(--text-error)]'>{error}</p>}
{error && <p className='mt-[6.5px] text-[12px] text-[var(--text-error)]'>{error}</p>}
<p className='mt-[6.5px] truncate text-[11px] text-[var(--text-secondary)]'>
{isEditingExisting && value ? (
<>
@@ -777,7 +777,7 @@ function AuthSelector({
disabled={disabled}
/>
{emailError && (
<p className='mt-[6.5px] text-[11px] text-[var(--text-error)]'>{emailError}</p>
<p className='mt-[6.5px] text-[12px] text-[var(--text-error)]'>{emailError}</p>
)}
<p className='mt-[6.5px] text-[11px] text-[var(--text-secondary)]'>
{authType === 'email'
@@ -787,7 +787,7 @@ function AuthSelector({
</div>
)}
{error && <p className='mt-[6.5px] text-[11px] text-[var(--text-error)]'>{error}</p>}
{error && <p className='mt-[6.5px] text-[12px] text-[var(--text-error)]'>{error}</p>}
</div>
)
}

View File

@@ -216,7 +216,7 @@ export function FormBuilder({
)}
</div>
{titleError && (
<p className='mt-[4px] text-[11px] text-[var(--text-error)]'>{titleError}</p>
<p className='mt-[4px] text-[12px] text-[var(--text-error)]'>{titleError}</p>
)}
<div className='mt-[4px] flex items-center gap-[6px]'>
<input

View File

@@ -17,6 +17,7 @@ import { Skeleton } from '@/components/ui'
import { isDev } from '@/lib/core/config/feature-flags'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl, getEmailDomain } from '@/lib/core/utils/urls'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import {
type FieldConfig,
useCreateForm,
@@ -146,7 +147,7 @@ export function FormDeploy({
useEffect(() => {
const blocks = Object.values(useWorkflowStore.getState().blocks)
const startBlock = blocks.find((b) => b.type === 'starter' || b.type === 'start_trigger')
const startBlock = blocks.find((b) => isValidStartBlockType(b.type))
if (startBlock) {
const inputFormat = useSubBlockStore.getState().getValue(startBlock.id, 'inputFormat')
@@ -398,7 +399,7 @@ export function FormDeploy({
</div>
</div>
{(identifierError || errors.identifier) && (
<p className='mt-[6.5px] text-[11px] text-[var(--text-error)]'>
<p className='mt-[6.5px] text-[12px] text-[var(--text-error)]'>
{identifierError || errors.identifier}
</p>
)}
@@ -482,7 +483,7 @@ export function FormDeploy({
</button>
</div>
{errors.password && (
<p className='mt-[6.5px] text-[11px] text-[var(--text-error)]'>{errors.password}</p>
<p className='mt-[6.5px] text-[12px] text-[var(--text-error)]'>{errors.password}</p>
)}
<p className='mt-[6.5px] text-[11px] text-[var(--text-secondary)]'>
{existingForm?.hasPassword
@@ -519,7 +520,7 @@ export function FormDeploy({
placeholderWithTags='Add another'
/>
{errors.emails && (
<p className='mt-[6.5px] text-[11px] text-[var(--text-error)]'>{errors.emails}</p>
<p className='mt-[6.5px] text-[12px] text-[var(--text-error)]'>{errors.emails}</p>
)}
<p className='mt-[6.5px] text-[11px] text-[var(--text-secondary)]'>
Add specific emails or entire domains (@example.com)
@@ -550,7 +551,7 @@ export function FormDeploy({
)}
{errors.general && (
<p className='mt-[6.5px] text-[11px] text-[var(--text-error)]'>{errors.general}</p>
<p className='mt-[6.5px] text-[12px] text-[var(--text-error)]'>{errors.general}</p>
)}
<button type='button' data-delete-trigger onClick={handleDelete} className='hidden' />

View File

@@ -15,7 +15,7 @@ import {
import { Skeleton } from '@/components/ui'
import { generateToolInputSchema, sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
import { isValidStartBlockType } from '@/lib/workflows/triggers/trigger-utils'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import type { InputFormatField } from '@/lib/workflows/types'
import {
useAddWorkflowMcpTool,

View File

@@ -1,6 +1,6 @@
'use client'
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Check } from 'lucide-react'
import {
@@ -308,6 +308,7 @@ export function OAuthRequiredModal({
serviceId,
newScopes = [],
}: OAuthRequiredModalProps) {
const [error, setError] = useState<string | null>(null)
const { baseProvider } = parseProvider(provider)
const baseProviderConfig = OAUTH_PROVIDERS[baseProvider]
@@ -348,23 +349,24 @@ export function OAuthRequiredModal({
}, [requiredScopes, newScopesSet])
const handleConnectDirectly = async () => {
setError(null)
try {
const providerId = getProviderIdFromServiceId(serviceId)
onClose()
logger.info('Linking OAuth2:', {
providerId,
requiredScopes,
})
if (providerId === 'trello') {
onClose()
window.location.href = '/api/auth/trello/authorize'
return
}
if (providerId === 'shopify') {
// Pass the current URL so we can redirect back after OAuth
onClose()
const returnUrl = encodeURIComponent(window.location.href)
window.location.href = `/api/auth/shopify/authorize?returnUrl=${returnUrl}`
return
@@ -374,8 +376,10 @@ export function OAuthRequiredModal({
providerId,
callbackURL: window.location.href,
})
} catch (error) {
logger.error('Error initiating OAuth flow:', { error })
onClose()
} catch (err) {
logger.error('Error initiating OAuth flow:', { error: err })
setError('Failed to connect. Please try again.')
}
}
@@ -425,10 +429,12 @@ export function OAuthRequiredModal({
</ul>
</div>
)}
{error && <p className='text-[12px] text-[var(--text-error)]'>{error}</p>}
</div>
</ModalBody>
<ModalFooter>
<Button variant='active' onClick={onClose}>
<Button variant='default' onClick={onClose}>
Cancel
</Button>
<Button variant='tertiary' type='button' onClick={handleConnectDirectly}>

View File

@@ -1,10 +1,11 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { AlertCircle, Wand2 } from 'lucide-react'
import { AlertCircle, ArrowUp } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Badge,
Button,
Input,
Modal,
ModalBody,
ModalContent,
@@ -878,35 +879,53 @@ try {
JSON Schema
</Label>
{schemaError && (
<div className='ml-2 flex min-w-0 items-center gap-1 text-[var(--text-error)] text-xs'>
<div className='ml-2 flex min-w-0 items-center gap-1 text-[12px] text-[var(--text-error)]'>
<AlertCircle className='h-3 w-3 flex-shrink-0' />
<span className='truncate'>{schemaError}</span>
</div>
)}
</div>
<div className='flex min-w-0 flex-1 items-center justify-end gap-1 pr-[4px]'>
<div className='flex min-w-0 items-center justify-end gap-[4px]'>
{!isSchemaPromptActive ? (
<button
type='button'
<Button
variant='active'
className='-my-1 h-5 px-2 py-0 text-[11px]'
onClick={handleSchemaWandClick}
disabled={schemaGeneration.isLoading || schemaGeneration.isStreaming}
className='inline-flex h-[16px] w-[16px] items-center justify-center rounded-full hover:bg-transparent disabled:opacity-50'
aria-label='Generate schema with AI'
>
<Wand2 className='!h-[12px] !w-[12px] text-[var(--text-secondary)]' />
</button>
Generate
</Button>
) : (
<input
ref={schemaPromptInputRef}
type='text'
value={schemaGeneration.isStreaming ? 'Generating...' : schemaPromptInput}
onChange={(e) => handleSchemaPromptChange(e.target.value)}
onBlur={handleSchemaPromptBlur}
onKeyDown={handleSchemaPromptKeyDown}
disabled={schemaGeneration.isStreaming}
className='h-[16px] w-full border-none bg-transparent py-0 pr-[2px] text-right font-medium text-[12px] text-[var(--text-primary)] leading-[14px] placeholder:text-[var(--text-muted)] focus:outline-none'
placeholder='Describe schema...'
/>
<div className='-my-1 flex items-center gap-[4px]'>
<Input
ref={schemaPromptInputRef}
value={schemaGeneration.isStreaming ? 'Generating...' : schemaPromptInput}
onChange={(e) => handleSchemaPromptChange(e.target.value)}
onBlur={handleSchemaPromptBlur}
onKeyDown={handleSchemaPromptKeyDown}
disabled={schemaGeneration.isStreaming}
className={cn(
'h-5 max-w-[200px] flex-1 text-[11px]',
schemaGeneration.isStreaming && 'text-muted-foreground'
)}
placeholder='Generate...'
/>
<Button
variant='tertiary'
disabled={!schemaPromptInput.trim() || schemaGeneration.isStreaming}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={(e) => {
e.stopPropagation()
handleSchemaPromptSubmit()
}}
className='h-[20px] w-[20px] flex-shrink-0 p-0'
>
<ArrowUp className='h-[12px] w-[12px]' />
</Button>
</div>
)}
</div>
</div>
@@ -952,35 +971,53 @@ try {
Code
</Label>
{codeError && !codeGeneration.isStreaming && (
<div className='ml-2 flex min-w-0 items-center gap-1 text-[var(--text-error)] text-xs'>
<div className='ml-2 flex min-w-0 items-center gap-1 text-[12px] text-[var(--text-error)]'>
<AlertCircle className='h-3 w-3 flex-shrink-0' />
<span className='truncate'>{codeError}</span>
</div>
)}
</div>
<div className='flex min-w-0 flex-1 items-center justify-end gap-1 pr-[4px]'>
<div className='flex min-w-0 items-center justify-end gap-[4px]'>
{!isCodePromptActive ? (
<button
type='button'
<Button
variant='active'
className='-my-1 h-5 px-2 py-0 text-[11px]'
onClick={handleCodeWandClick}
disabled={codeGeneration.isLoading || codeGeneration.isStreaming}
className='inline-flex h-[16px] w-[16px] items-center justify-center rounded-full hover:bg-transparent disabled:opacity-50'
aria-label='Generate code with AI'
>
<Wand2 className='!h-[12px] !w-[12px] text-[var(--text-secondary)]' />
</button>
Generate
</Button>
) : (
<input
ref={codePromptInputRef}
type='text'
value={codeGeneration.isStreaming ? 'Generating...' : codePromptInput}
onChange={(e) => handleCodePromptChange(e.target.value)}
onBlur={handleCodePromptBlur}
onKeyDown={handleCodePromptKeyDown}
disabled={codeGeneration.isStreaming}
className='h-[16px] w-full border-none bg-transparent py-0 pr-[2px] text-right font-medium text-[12px] text-[var(--text-primary)] leading-[14px] placeholder:text-[var(--text-muted)] focus:outline-none'
placeholder='Describe code...'
/>
<div className='-my-1 flex items-center gap-[4px]'>
<Input
ref={codePromptInputRef}
value={codeGeneration.isStreaming ? 'Generating...' : codePromptInput}
onChange={(e) => handleCodePromptChange(e.target.value)}
onBlur={handleCodePromptBlur}
onKeyDown={handleCodePromptKeyDown}
disabled={codeGeneration.isStreaming}
className={cn(
'h-5 max-w-[200px] flex-1 text-[11px]',
codeGeneration.isStreaming && 'text-muted-foreground'
)}
placeholder='Generate...'
/>
<Button
variant='tertiary'
disabled={!codePromptInput.trim() || codeGeneration.isStreaming}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={(e) => {
e.stopPropagation()
handleCodePromptSubmit()
}}
className='h-[20px] w-[20px] flex-shrink-0 p-0'
>
<ArrowUp className='h-[12px] w-[12px]' />
</Button>
</div>
)}
</div>
</div>

View File

@@ -556,14 +556,17 @@ export function Panel() {
<ModalHeader>Delete Workflow</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
Deleting this workflow will permanently remove all associated blocks, executions, and
configuration.{' '}
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>
{currentWorkflow?.name ?? 'this workflow'}
</span>
? This will permanently remove all associated blocks, executions, and configuration.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
</ModalBody>
<ModalFooter>
<Button
variant='active'
variant='default'
onClick={() => setIsDeleteModalOpen(false)}
disabled={isDeleting}
>

View File

@@ -2,6 +2,7 @@ import { useMemo } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
import { SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/sanitization/references'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import { normalizeName } from '@/executor/constants'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { Loop, Parallel } from '@/stores/workflows/workflow/types'
@@ -26,9 +27,7 @@ export function useAccessibleReferencePrefixes(blockId?: string | null): Set<str
const accessibleIds = new Set<string>(ancestorIds)
accessibleIds.add(blockId)
const starterBlock = Object.values(blocks).find(
(block) => block.type === 'starter' || block.type === 'start_trigger'
)
const starterBlock = Object.values(blocks).find((block) => isValidStartBlockType(block.type))
if (starterBlock && ancestorIds.includes(starterBlock.id)) {
accessibleIds.add(starterBlock.id)
}

View File

@@ -420,7 +420,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent>
<ModalContent size='md'>
<ModalHeader>Help &amp; Support</ModalHeader>
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>

View File

@@ -1069,7 +1069,7 @@ export function AccessControl() {
</Modal>
<Modal open={showUnsavedChanges} onOpenChange={setShowUnsavedChanges}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Unsaved Changes</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
@@ -1185,7 +1185,7 @@ export function AccessControl() {
</div>
<Modal open={showCreateModal} onOpenChange={handleCloseCreateModal}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Create Permission Group</ModalHeader>
<ModalBody>
<div className='flex flex-col gap-[12px]'>
@@ -1237,7 +1237,7 @@ export function AccessControl() {
</Modal>
<Modal open={!!deletingGroup} onOpenChange={() => setDeletingGroup(null)}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete Permission Group</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>

View File

@@ -392,7 +392,7 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
{/* Delete Confirmation Dialog */}
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete API key</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>

View File

@@ -112,7 +112,7 @@ export function CreateApiKeyModal({
<>
{/* Create API Key Dialog */}
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Create new API key</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
@@ -176,7 +176,7 @@ export function CreateApiKeyModal({
data-form-type='other'
/>
{createError && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>
<p className='text-[12px] text-[var(--text-error)] leading-tight'>
{createError}
</p>
)}
@@ -215,7 +215,7 @@ export function CreateApiKeyModal({
}
}}
>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Your API key has been created</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>

View File

@@ -276,7 +276,7 @@ export function BYOK() {
</Button>
</div>
{error && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>{error}</p>
<p className='text-[12px] text-[var(--text-error)] leading-tight'>{error}</p>
)}
</div>
</ModalBody>
@@ -306,7 +306,7 @@ export function BYOK() {
</Modal>
<Modal open={!!deleteConfirmProvider} onOpenChange={() => setDeleteConfirmProvider(null)}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete API Key</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>

View File

@@ -211,7 +211,7 @@ export function Copilot() {
{/* Create API Key Dialog */}
<Modal open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Create new API key</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
@@ -234,7 +234,7 @@ export function Copilot() {
autoFocus
/>
{createError && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>{createError}</p>
<p className='text-[12px] text-[var(--text-error)] leading-tight'>{createError}</p>
)}
</div>
</ModalBody>
@@ -273,7 +273,7 @@ export function Copilot() {
}
}}
>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Your API key has been created</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
@@ -310,7 +310,7 @@ export function Copilot() {
{/* Delete Confirmation Dialog */}
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete API key</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>

View File

@@ -824,7 +824,7 @@ export function CredentialSets() {
{/* Create Polling Group Modal */}
<Modal open={showCreateModal} onOpenChange={handleCloseCreateModal}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Create Polling Group</ModalHeader>
<ModalBody>
<div className='flex flex-col gap-[12px]'>
@@ -897,7 +897,7 @@ export function CredentialSets() {
{/* Leave Confirmation Modal */}
<Modal open={!!leavingMembership} onOpenChange={() => setLeavingMembership(null)}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Leave Polling Group</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
@@ -925,7 +925,7 @@ export function CredentialSets() {
{/* Delete Confirmation Modal */}
<Modal open={!!deletingSet} onOpenChange={() => setDeletingSet(null)}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete Polling Group</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>

View File

@@ -206,7 +206,7 @@ export function CustomTools() {
/>
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete Custom Tool</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>

View File

@@ -821,7 +821,7 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
</div>
<Modal open={showUnsavedChanges} onOpenChange={setShowUnsavedChanges}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Unsaved Changes</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>

View File

@@ -390,7 +390,7 @@ export function Integrations({ onOpenChange, registerCloseHandler }: Integration
</div>
<Modal open={showDisconnectDialog} onOpenChange={setShowDisconnectDialog}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Disconnect Service</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>

View File

@@ -1170,7 +1170,7 @@ export function MCP({ initialServerId }: MCPProps) {
</div>
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete MCP Server</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>

View File

@@ -245,10 +245,7 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
? 'Your subscription is set to cancel at the end of the billing period. Would you like to keep your subscription active?'
: `You'll be redirected to Stripe to manage your subscription. You'll keep access until ${formatDate(
periodEndDate
)}, then downgrade to free plan.`}{' '}
{!isCancelAtPeriodEnd && (
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
)}
)}, then downgrade to free plan. You can restore your subscription at any time.`}
</p>
{!isCancelAtPeriodEnd && (
@@ -266,7 +263,7 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
</ModalBody>
<ModalFooter>
<Button
variant='active'
variant='default'
onClick={isCancelAtPeriodEnd ? () => setIsDialogOpen(false) : handleKeep}
disabled={isLoading}
>

View File

@@ -183,7 +183,7 @@ export function MemberInvitationCard({
aria-autocomplete='none'
/>
{emailError && (
<p className='mt-1 text-[11px] text-[var(--text-error)] leading-tight'>
<p className='mt-1 text-[12px] text-[var(--text-error)] leading-tight'>
{emailError}
</p>
)}
@@ -295,7 +295,7 @@ export function MemberInvitationCard({
{/* Invitation error - inline */}
{invitationError && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>
<p className='text-[12px] text-[var(--text-error)] leading-tight'>
{invitationError instanceof Error && invitationError.message
? invitationError.message
: String(invitationError)}

View File

@@ -104,7 +104,7 @@ export function NoOrganizationView({
<div className='flex flex-col gap-[8px]'>
{error && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>{error}</p>
<p className='text-[12px] text-[var(--text-error)] leading-tight'>{error}</p>
)}
<div className='flex justify-end'>
<Button
@@ -179,7 +179,7 @@ export function NoOrganizationView({
</div>
</div>
{error && <p className='text-[11px] text-[var(--text-error)] leading-tight'>{error}</p>}
{error && <p className='text-[12px] text-[var(--text-error)] leading-tight'>{error}</p>}
<ModalFooter>
<Button

View File

@@ -33,13 +33,19 @@ export function RemoveMemberDialog({
}: RemoveMemberDialogProps) {
return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>{isSelfRemoval ? 'Leave Organization' : 'Remove Team Member'}</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
{isSelfRemoval
? 'Are you sure you want to leave this organization? You will lose access to all team resources.'
: `Are you sure you want to remove ${memberName} from the team?`}{' '}
{isSelfRemoval ? (
'Are you sure you want to leave this organization? You will lose access to all team resources.'
) : (
<>
Are you sure you want to remove{' '}
<span className='font-medium text-[var(--text-primary)]'>{memberName}</span> from
the team?
</>
)}{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
@@ -64,14 +70,14 @@ export function RemoveMemberDialog({
{error && (
<div className='mt-[8px]'>
<p className='text-[11px] text-[var(--text-error)] leading-tight'>
<p className='text-[12px] text-[var(--text-error)] leading-tight'>
{error instanceof Error && error.message ? error.message : String(error)}
</p>
</div>
)}
</ModalBody>
<ModalFooter>
<Button variant='active' onClick={onCancel}>
<Button variant='default' onClick={onCancel}>
Cancel
</Button>
<Button variant='destructive' onClick={() => onConfirmRemove(shouldReduceSeats)}>

View File

@@ -532,7 +532,7 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
</div>
<Modal open={!!toolToDelete} onOpenChange={(open) => !open && setToolToDelete(null)}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Remove Workflow</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
@@ -745,7 +745,7 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
}
/>
{addToolMutation.isError && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>
<p className='text-[12px] text-[var(--text-error)] leading-tight'>
{addToolMutation.error?.message || 'Failed to add workflow'}
</p>
)}
@@ -1109,7 +1109,7 @@ export function WorkflowMcpServers() {
</div>
<Modal open={!!serverToDelete} onOpenChange={(open) => !open && setServerToDelete(null)}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete MCP Server</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>

View File

@@ -102,7 +102,7 @@ export function DeleteModal({
return (
<Modal open={isOpen} onOpenChange={onClose}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>{title}</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
@@ -111,7 +111,7 @@ export function DeleteModal({
</p>
</ModalBody>
<ModalFooter>
<Button variant='active' onClick={onClose} disabled={isDeleting}>
<Button variant='default' onClick={onClose} disabled={isDeleting}>
Cancel
</Button>
<Button variant='destructive' onClick={onConfirm} disabled={isDeleting}>

View File

@@ -607,7 +607,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
onOpenChange(newOpen)
}}
>
<ModalContent className='w-[500px]'>
<ModalContent size='md'>
<ModalHeader>Invite members to {workspaceName || 'Workspace'}</ModalHeader>
<form
@@ -740,7 +740,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
{/* Remove Member Confirmation Dialog */}
<Modal open={!!memberToRemove} onOpenChange={handleRemoveMemberCancel}>
<ModalContent>
<ModalContent size='sm'>
<ModalHeader>Remove Member</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
@@ -773,7 +773,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
{/* Remove Invitation Confirmation Dialog */}
<Modal open={!!invitationToRemove} onOpenChange={handleRemoveInvitationCancel}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Cancel Invitation</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>

View File

@@ -7,6 +7,11 @@ import {
Badge,
Button,
ChevronDown,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
PanelLeft,
Popover,
PopoverContent,
@@ -143,6 +148,9 @@ export function WorkspaceHeader({
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [deleteTarget, setDeleteTarget] = useState<Workspace | null>(null)
const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false)
const [isLeaving, setIsLeaving] = useState(false)
const [leaveTarget, setLeaveTarget] = useState<Workspace | null>(null)
const [editingWorkspaceId, setEditingWorkspaceId] = useState<string | null>(null)
const [editingName, setEditingName] = useState('')
const [isListRenaming, setIsListRenaming] = useState(false)
@@ -278,13 +286,35 @@ export function WorkspaceHeader({
}
/**
* Handles leave action from context menu
* Handles leave action from context menu - shows confirmation modal
*/
const handleLeaveAction = async () => {
if (!capturedWorkspaceRef.current || !onLeaveWorkspace) return
const handleLeaveAction = () => {
if (!capturedWorkspaceRef.current) return
await onLeaveWorkspace(capturedWorkspaceRef.current.id)
setIsWorkspaceMenuOpen(false)
const workspace = workspaces.find((w) => w.id === capturedWorkspaceRef.current?.id)
if (workspace) {
setLeaveTarget(workspace)
setIsLeaveModalOpen(true)
setIsWorkspaceMenuOpen(false)
}
}
/**
* Handle leave workspace after confirmation
*/
const handleLeaveWorkspace = async () => {
if (!leaveTarget || !onLeaveWorkspace) return
setIsLeaving(true)
try {
await onLeaveWorkspace(leaveTarget.id)
setIsLeaveModalOpen(false)
setLeaveTarget(null)
} catch (error) {
logger.error('Error leaving workspace:', error)
} finally {
setIsLeaving(false)
}
}
/**
@@ -573,6 +603,32 @@ export function WorkspaceHeader({
itemType='workspace'
itemName={deleteTarget?.name || activeWorkspaceFull?.name || activeWorkspace?.name}
/>
{/* Leave Confirmation Modal */}
<Modal open={isLeaveModalOpen} onOpenChange={() => setIsLeaveModalOpen(false)}>
<ModalContent size='sm'>
<ModalHeader>Leave Workspace</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to leave{' '}
<span className='font-medium text-[var(--text-primary)]'>{leaveTarget?.name}</span>?
You will lose access to all workflows and data in this workspace.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
</ModalBody>
<ModalFooter>
<Button
variant='default'
onClick={() => setIsLeaveModalOpen(false)}
disabled={isLeaving}
>
Cancel
</Button>
<Button variant='destructive' onClick={handleLeaveWorkspace} disabled={isLeaving}>
{isLeaving ? 'Leaving...' : 'Leave Workspace'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
)
}

View File

@@ -17,6 +17,7 @@ import {
type GetBlockUpstreamReferencesResultType,
} from '@/lib/copilot/tools/shared/schemas'
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { Loop, Parallel } from '@/stores/workflows/workflow/types'
@@ -140,9 +141,7 @@ export class GetBlockUpstreamReferencesClientTool extends BaseClientTool {
const accessibleIds = new Set<string>(ancestorIds)
accessibleIds.add(blockId)
const starterBlock = Object.values(blocks).find(
(b) => b.type === 'starter' || b.type === 'start_trigger'
)
const starterBlock = Object.values(blocks).find((b) => isValidStartBlockType(b.type))
if (starterBlock && ancestorIds.includes(starterBlock.id)) {
accessibleIds.add(starterBlock.id)
}

View File

@@ -1,6 +1,6 @@
import { z } from 'zod'
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
import { isValidStartBlockType } from '@/lib/workflows/triggers/trigger-utils'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import type { InputFormatField } from '@/lib/workflows/types'
import type { McpToolSchema } from './types'

View File

@@ -1,3 +1,4 @@
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import type { InputFormatField } from '@/lib/workflows/types'
/**
@@ -23,7 +24,7 @@ export function extractInputFieldsFromBlocks(
// Find trigger block
const triggerEntry = Object.entries(blocks).find(([, block]) => {
const b = block as Record<string, unknown>
return b.type === 'start_trigger' || b.type === 'input_trigger' || b.type === 'starter'
return typeof b.type === 'string' && isValidStartBlockType(b.type)
})
if (!triggerEntry) return []

View File

@@ -0,0 +1,21 @@
/**
* Valid start block types that can trigger a workflow
* This module is kept lightweight with no dependencies to avoid circular imports
*/
export const VALID_START_BLOCK_TYPES = [
'starter',
'start',
'start_trigger',
'api',
'api_trigger',
'input_trigger',
] as const
export type ValidStartBlockType = (typeof VALID_START_BLOCK_TYPES)[number]
/**
* Check if a block type is a valid start block type
*/
export function isValidStartBlockType(blockType: string): blockType is ValidStartBlockType {
return VALID_START_BLOCK_TYPES.includes(blockType as ValidStartBlockType)
}

View File

@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import {
type StartBlockCandidate,
StartBlockPath,
@@ -11,27 +12,6 @@ import { getTrigger } from '@/triggers'
const logger = createLogger('TriggerUtils')
/**
* Valid start block types that can trigger a workflow
*/
export const VALID_START_BLOCK_TYPES = [
'starter',
'start',
'start_trigger',
'api',
'api_trigger',
'input_trigger',
] as const
export type ValidStartBlockType = (typeof VALID_START_BLOCK_TYPES)[number]
/**
* Check if a block type is a valid start block type
*/
export function isValidStartBlockType(blockType: string): blockType is ValidStartBlockType {
return VALID_START_BLOCK_TYPES.includes(blockType as ValidStartBlockType)
}
/**
* Check if a workflow state has a valid start block
*/