Skip to content

Commit d4af644

Browse files
committed
feat(description): added version description for deployments table
1 parent 304cf71 commit d4af644

File tree

9 files changed

+10632
-38
lines changed

9 files changed

+10632
-38
lines changed

apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,24 @@ import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/
99

1010
const logger = createLogger('WorkflowDeploymentVersionAPI')
1111

12-
const patchBodySchema = z.object({
13-
name: z
14-
.string()
15-
.trim()
16-
.min(1, 'Name cannot be empty')
17-
.max(100, 'Name must be 100 characters or less'),
18-
})
12+
const patchBodySchema = z
13+
.object({
14+
name: z
15+
.string()
16+
.trim()
17+
.min(1, 'Name cannot be empty')
18+
.max(100, 'Name must be 100 characters or less')
19+
.optional(),
20+
description: z
21+
.string()
22+
.trim()
23+
.max(500, 'Description must be 500 characters or less')
24+
.nullable()
25+
.optional(),
26+
})
27+
.refine((data) => data.name !== undefined || data.description !== undefined, {
28+
message: 'At least one of name or description must be provided',
29+
})
1930

2031
export const dynamic = 'force-dynamic'
2132
export const runtime = 'nodejs'
@@ -88,33 +99,46 @@ export async function PATCH(
8899
return createErrorResponse(validation.error.errors[0]?.message || 'Invalid request body', 400)
89100
}
90101

91-
const { name } = validation.data
102+
const { name, description } = validation.data
103+
104+
const updateData: { name?: string; description?: string | null } = {}
105+
if (name !== undefined) {
106+
updateData.name = name
107+
}
108+
if (description !== undefined) {
109+
updateData.description = description
110+
}
92111

93112
const [updated] = await db
94113
.update(workflowDeploymentVersion)
95-
.set({ name })
114+
.set(updateData)
96115
.where(
97116
and(
98117
eq(workflowDeploymentVersion.workflowId, id),
99118
eq(workflowDeploymentVersion.version, versionNum)
100119
)
101120
)
102-
.returning({ id: workflowDeploymentVersion.id, name: workflowDeploymentVersion.name })
121+
.returning({
122+
id: workflowDeploymentVersion.id,
123+
name: workflowDeploymentVersion.name,
124+
description: workflowDeploymentVersion.description,
125+
})
103126

104127
if (!updated) {
105128
return createErrorResponse('Deployment version not found', 404)
106129
}
107130

108-
logger.info(
109-
`[${requestId}] Renamed deployment version ${version} for workflow ${id} to "${name}"`
110-
)
131+
logger.info(`[${requestId}] Updated deployment version ${version} for workflow ${id}`, {
132+
name: updateData.name,
133+
description: updateData.description,
134+
})
111135

112-
return createSuccessResponse({ name: updated.name })
136+
return createSuccessResponse({ name: updated.name, description: updated.description })
113137
} catch (error: any) {
114138
logger.error(
115139
`[${requestId}] Error renaming deployment version ${version} for workflow ${id}`,
116140
error
117141
)
118-
return createErrorResponse(error.message || 'Failed to rename deployment version', 500)
142+
return createErrorResponse(error.message || 'Failed to update deployment version', 500)
119143
}
120144
}

apps/sim/app/api/workflows/[id]/deployments/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
2626
id: workflowDeploymentVersion.id,
2727
version: workflowDeploymentVersion.version,
2828
name: workflowDeploymentVersion.name,
29+
description: workflowDeploymentVersion.description,
2930
isActive: workflowDeploymentVersion.isActive,
3031
createdAt: workflowDeploymentVersion.createdAt,
3132
createdBy: workflowDeploymentVersion.createdBy,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
'use client'
2+
3+
import { useCallback, useEffect, useRef, useState } from 'react'
4+
import { createLogger } from '@sim/logger'
5+
import {
6+
Button,
7+
Modal,
8+
ModalBody,
9+
ModalContent,
10+
ModalFooter,
11+
ModalHeader,
12+
Textarea,
13+
} from '@/components/emcn'
14+
15+
const logger = createLogger('VersionDescriptionModal')
16+
17+
interface VersionDescriptionModalProps {
18+
open: boolean
19+
onOpenChange: (open: boolean) => void
20+
workflowId: string
21+
version: number
22+
versionName: string
23+
currentDescription: string | null | undefined
24+
onSave: () => Promise<void>
25+
}
26+
27+
export function VersionDescriptionModal({
28+
open,
29+
onOpenChange,
30+
workflowId,
31+
version,
32+
versionName,
33+
currentDescription,
34+
onSave,
35+
}: VersionDescriptionModalProps) {
36+
const [description, setDescription] = useState('')
37+
const [isSaving, setIsSaving] = useState(false)
38+
const [error, setError] = useState<string | null>(null)
39+
const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false)
40+
41+
const initialDescriptionRef = useRef('')
42+
43+
useEffect(() => {
44+
if (open) {
45+
const initialDescription = currentDescription || ''
46+
setDescription(initialDescription)
47+
initialDescriptionRef.current = initialDescription
48+
setError(null)
49+
}
50+
}, [open, currentDescription])
51+
52+
const hasChanges = description.trim() !== initialDescriptionRef.current.trim()
53+
54+
const handleCloseAttempt = useCallback(() => {
55+
if (hasChanges && !isSaving) {
56+
setShowUnsavedChangesAlert(true)
57+
} else {
58+
onOpenChange(false)
59+
}
60+
}, [hasChanges, isSaving, onOpenChange])
61+
62+
const handleDiscardChanges = useCallback(() => {
63+
setShowUnsavedChangesAlert(false)
64+
setDescription(initialDescriptionRef.current)
65+
onOpenChange(false)
66+
}, [onOpenChange])
67+
68+
const handleSave = useCallback(async () => {
69+
if (!workflowId) return
70+
71+
setIsSaving(true)
72+
setError(null)
73+
try {
74+
const res = await fetch(`/api/workflows/${workflowId}/deployments/${version}`, {
75+
method: 'PATCH',
76+
headers: { 'Content-Type': 'application/json' },
77+
body: JSON.stringify({ description: description.trim() || null }),
78+
})
79+
80+
if (res.ok) {
81+
await onSave()
82+
onOpenChange(false)
83+
} else {
84+
const data = await res.json().catch(() => ({}))
85+
const message = data.error || 'Failed to save description'
86+
setError(message)
87+
logger.error('Failed to save description:', message)
88+
}
89+
} catch (err) {
90+
const message = err instanceof Error ? err.message : 'An unexpected error occurred'
91+
setError(message)
92+
logger.error('Error saving description:', err)
93+
} finally {
94+
setIsSaving(false)
95+
}
96+
}, [workflowId, version, description, onSave, onOpenChange])
97+
98+
return (
99+
<>
100+
<Modal open={open} onOpenChange={(openState) => !openState && handleCloseAttempt()}>
101+
<ModalContent className='max-w-[480px]'>
102+
<ModalHeader>
103+
<span>Version Description</span>
104+
</ModalHeader>
105+
<ModalBody className='space-y-[12px]'>
106+
<p className='text-[12px] text-[var(--text-secondary)]'>
107+
{currentDescription ? 'Edit' : 'Add'} a description for{' '}
108+
<span className='font-medium text-[var(--text-primary)]'>{versionName}</span>
109+
</p>
110+
<Textarea
111+
placeholder='Describe the changes in this deployment version...'
112+
className='min-h-[120px] resize-none'
113+
value={description}
114+
onChange={(e) => setDescription(e.target.value)}
115+
maxLength={500}
116+
/>
117+
<div className='flex items-center justify-between'>
118+
{error ? <p className='text-[12px] text-[var(--text-error)]'>{error}</p> : <div />}
119+
<p className='text-[11px] text-[var(--text-tertiary)]'>{description.length}/500</p>
120+
</div>
121+
</ModalBody>
122+
<ModalFooter>
123+
<Button variant='default' onClick={handleCloseAttempt} disabled={isSaving}>
124+
Cancel
125+
</Button>
126+
<Button variant='tertiary' onClick={handleSave} disabled={isSaving || !hasChanges}>
127+
{isSaving ? 'Saving...' : 'Save'}
128+
</Button>
129+
</ModalFooter>
130+
</ModalContent>
131+
</Modal>
132+
133+
<Modal open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
134+
<ModalContent className='max-w-[400px]'>
135+
<ModalHeader>
136+
<span>Unsaved Changes</span>
137+
</ModalHeader>
138+
<ModalBody>
139+
<p className='text-[14px] text-[var(--text-secondary)]'>
140+
You have unsaved changes. Are you sure you want to discard them?
141+
</p>
142+
</ModalBody>
143+
<ModalFooter>
144+
<Button variant='default' onClick={() => setShowUnsavedChangesAlert(false)}>
145+
Keep Editing
146+
</Button>
147+
<Button variant='destructive' onClick={handleDiscardChanges}>
148+
Discard Changes
149+
</Button>
150+
</ModalFooter>
151+
</ModalContent>
152+
</Modal>
153+
</>
154+
)
155+
}

0 commit comments

Comments
 (0)