Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
56 changes: 40 additions & 16 deletions apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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}`,
`[${requestId}] Error updating 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)
}
}
1 change: 1 addition & 0 deletions apps/sim/app/api/workflows/[id]/deployments/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use client'

import { useCallback, useState } from 'react'
import {
Button,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Textarea,
} from '@/components/emcn'
import { useUpdateDeploymentVersion } from '@/hooks/queries/deployments'

interface VersionDescriptionModalProps {
open: boolean
onOpenChange: (open: boolean) => void
workflowId: string
version: number
versionName: string
currentDescription: string | null | undefined
}

export function VersionDescriptionModal({
open,
onOpenChange,
workflowId,
version,
versionName,
currentDescription,
}: VersionDescriptionModalProps) {
// Initialize state from props - component remounts via key prop when version changes
const initialDescription = currentDescription || ''
const [description, setDescription] = useState(initialDescription)
const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false)

const updateMutation = useUpdateDeploymentVersion()

const hasChanges = description.trim() !== initialDescription.trim()
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated

const handleCloseAttempt = useCallback(() => {
if (hasChanges && !updateMutation.isPending) {
setShowUnsavedChangesAlert(true)
} else {
onOpenChange(false)
}
}, [hasChanges, updateMutation.isPending, onOpenChange])
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated

const handleDiscardChanges = useCallback(() => {
setShowUnsavedChangesAlert(false)
setDescription(initialDescription)
onOpenChange(false)
}, [initialDescription, onOpenChange])

const handleSave = useCallback(async () => {
if (!workflowId) return

updateMutation.mutate(
{
workflowId,
version,
description: description.trim() || null,
},
{
onSuccess: () => {
onOpenChange(false)
},
}
)
}, [workflowId, version, description, updateMutation, onOpenChange])
Comment thread
waleedlatif1 marked this conversation as resolved.

return (
<>
<Modal open={open} onOpenChange={(openState) => !openState && handleCloseAttempt()}>
<ModalContent className='max-w-[480px]'>
<ModalHeader>
<span>Version Description</span>
</ModalHeader>
<ModalBody className='space-y-[12px]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
{currentDescription ? 'Edit the' : 'Add a'} description for{' '}
<span className='font-medium text-[var(--text-primary)]'>{versionName}</span>
</p>
<Textarea
placeholder='Describe the changes in this deployment version...'
className='min-h-[120px] resize-none'
value={description}
onChange={(e) => setDescription(e.target.value)}
maxLength={500}
/>
<div className='flex items-center justify-between'>
{updateMutation.error ? (
<p className='text-[12px] text-[var(--text-error)]'>
{updateMutation.error.message}
</p>
) : (
<div />
)}
<p className='text-[11px] text-[var(--text-tertiary)]'>{description.length}/500</p>
</div>
</ModalBody>
<ModalFooter>
<Button
variant='default'
onClick={handleCloseAttempt}
disabled={updateMutation.isPending}
>
Cancel
</Button>
<Button
variant='tertiary'
onClick={handleSave}
disabled={updateMutation.isPending || !hasChanges}
>
{updateMutation.isPending ? 'Saving...' : 'Save'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>

<Modal open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
<ModalContent className='max-w-[400px]'>
<ModalHeader>
<span>Unsaved Changes</span>
</ModalHeader>
<ModalBody>
<p className='text-[14px] text-[var(--text-secondary)]'>
You have unsaved changes. Are you sure you want to discard them?
</p>
</ModalBody>
<ModalFooter>
<Button variant='default' onClick={() => setShowUnsavedChangesAlert(false)}>
Keep Editing
</Button>
<Button variant='destructive' onClick={handleDiscardChanges}>
Discard Changes
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
)
}
Loading