diff --git a/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts b/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts index 20642c90e..93e7b9ce6 100644 --- a/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts +++ b/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts @@ -9,13 +9,24 @@ import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/ const logger = createLogger('WorkflowDeploymentVersionAPI') -const patchBodySchema = z.object({ - name: z - .string() - .trim() - .min(1, 'Name cannot be empty') - .max(100, 'Name must be 100 characters or less'), -}) +const patchBodySchema = z + .object({ + name: z + .string() + .trim() + .min(1, 'Name cannot be empty') + .max(100, 'Name must be 100 characters or less') + .optional(), + description: z + .string() + .trim() + .max(500, 'Description must be 500 characters or less') + .nullable() + .optional(), + }) + .refine((data) => data.name !== undefined || data.description !== undefined, { + message: 'At least one of name or description must be provided', + }) export const dynamic = 'force-dynamic' export const runtime = 'nodejs' @@ -88,33 +99,46 @@ export async function PATCH( return createErrorResponse(validation.error.errors[0]?.message || 'Invalid request body', 400) } - const { name } = validation.data + const { name, description } = validation.data + + const updateData: { name?: string; description?: string | null } = {} + if (name !== undefined) { + updateData.name = name + } + if (description !== undefined) { + updateData.description = description + } const [updated] = await db .update(workflowDeploymentVersion) - .set({ name }) + .set(updateData) .where( and( eq(workflowDeploymentVersion.workflowId, id), eq(workflowDeploymentVersion.version, versionNum) ) ) - .returning({ id: workflowDeploymentVersion.id, name: workflowDeploymentVersion.name }) + .returning({ + id: workflowDeploymentVersion.id, + name: workflowDeploymentVersion.name, + description: workflowDeploymentVersion.description, + }) if (!updated) { return createErrorResponse('Deployment version not found', 404) } - logger.info( - `[${requestId}] Renamed deployment version ${version} for workflow ${id} to "${name}"` - ) + logger.info(`[${requestId}] Updated deployment version ${version} for workflow ${id}`, { + name: updateData.name, + description: updateData.description, + }) - return createSuccessResponse({ name: updated.name }) + return createSuccessResponse({ name: updated.name, description: updated.description }) } catch (error: any) { logger.error( `[${requestId}] Error renaming deployment version ${version} for workflow ${id}`, error ) - return createErrorResponse(error.message || 'Failed to rename deployment version', 500) + return createErrorResponse(error.message || 'Failed to update deployment version', 500) } } diff --git a/apps/sim/app/api/workflows/[id]/deployments/route.ts b/apps/sim/app/api/workflows/[id]/deployments/route.ts index 87ec11e70..ac2e7e101 100644 --- a/apps/sim/app/api/workflows/[id]/deployments/route.ts +++ b/apps/sim/app/api/workflows/[id]/deployments/route.ts @@ -26,6 +26,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ id: workflowDeploymentVersion.id, version: workflowDeploymentVersion.version, name: workflowDeploymentVersion.name, + description: workflowDeploymentVersion.description, isActive: workflowDeploymentVersion.isActive, createdAt: workflowDeploymentVersion.createdAt, createdBy: workflowDeploymentVersion.createdBy, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx new file mode 100644 index 000000000..866bd804e --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx @@ -0,0 +1,155 @@ +'use client' + +import { useCallback, useEffect, useRef, useState } from 'react' +import { createLogger } from '@sim/logger' +import { + Button, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Textarea, +} from '@/components/emcn' + +const logger = createLogger('VersionDescriptionModal') + +interface VersionDescriptionModalProps { + open: boolean + onOpenChange: (open: boolean) => void + workflowId: string + version: number + versionName: string + currentDescription: string | null | undefined + onSave: () => Promise +} + +export function VersionDescriptionModal({ + open, + onOpenChange, + workflowId, + version, + versionName, + currentDescription, + onSave, +}: VersionDescriptionModalProps) { + const [description, setDescription] = useState('') + const [isSaving, setIsSaving] = useState(false) + const [error, setError] = useState(null) + const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false) + + const initialDescriptionRef = useRef('') + + useEffect(() => { + if (open) { + const initialDescription = currentDescription || '' + setDescription(initialDescription) + initialDescriptionRef.current = initialDescription + setError(null) + } + }, [open, currentDescription]) + + const hasChanges = description.trim() !== initialDescriptionRef.current.trim() + + const handleCloseAttempt = useCallback(() => { + if (hasChanges && !isSaving) { + setShowUnsavedChangesAlert(true) + } else { + onOpenChange(false) + } + }, [hasChanges, isSaving, onOpenChange]) + + const handleDiscardChanges = useCallback(() => { + setShowUnsavedChangesAlert(false) + setDescription(initialDescriptionRef.current) + onOpenChange(false) + }, [onOpenChange]) + + const handleSave = useCallback(async () => { + if (!workflowId) return + + setIsSaving(true) + setError(null) + try { + const res = await fetch(`/api/workflows/${workflowId}/deployments/${version}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ description: description.trim() || null }), + }) + + if (res.ok) { + await onSave() + onOpenChange(false) + } else { + const data = await res.json().catch(() => ({})) + const message = data.error || 'Failed to save description' + setError(message) + logger.error('Failed to save description:', message) + } + } catch (err) { + const message = err instanceof Error ? err.message : 'An unexpected error occurred' + setError(message) + logger.error('Error saving description:', err) + } finally { + setIsSaving(false) + } + }, [workflowId, version, description, onSave, onOpenChange]) + + return ( + <> + !openState && handleCloseAttempt()}> + + + Version Description + + +

+ {currentDescription ? 'Edit' : 'Add'} a description for{' '} + {versionName} +

+