Skip to content

Commit 8b24047

Browse files
authored
feat(description): add deployment version descriptions (#3048)
* feat(description): added version description for deployments table * feat(description): refactor to tanstack query and remove useEffect * add wand to generate diff * ack comments * removed redundant logic, kept single source of truth for diff * updated docs * use consolidated sse parsing util, add loops & parallels check * DRY
1 parent c00f05c commit 8b24047

File tree

18 files changed

+11562
-308
lines changed

18 files changed

+11562
-308
lines changed

apps/docs/content/docs/en/quick-reference/index.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,11 @@ A quick lookup for everyday actions in the Sim workflow editor. For keyboard sho
335335
<td>Access previous versions in Deploy tab → **Promote to live**</td>
336336
<td><ActionImage src="/static/quick-reference/promote-deployment.png" alt="Promote deployment to live" /></td>
337337
</tr>
338+
<tr>
339+
<td>Add version description</td>
340+
<td>Deploy tab → Click description icon → Add or generate description</td>
341+
<td><ActionVideo src="quick-reference/deployment-description.mp4" alt="Add deployment version description" /></td>
342+
</tr>
338343
<tr>
339344
<td>Copy API endpoint</td>
340345
<td>Deploy tab → API → Copy API cURL</td>

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

Lines changed: 40 additions & 16 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(
115-
`[${requestId}] Error renaming deployment version ${version} for workflow ${id}`,
139+
`[${requestId}] Error updating 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,170 @@
1+
'use client'
2+
3+
import { useCallback, useRef, useState } from 'react'
4+
import {
5+
Button,
6+
Modal,
7+
ModalBody,
8+
ModalContent,
9+
ModalFooter,
10+
ModalHeader,
11+
Textarea,
12+
} from '@/components/emcn'
13+
import {
14+
useGenerateVersionDescription,
15+
useUpdateDeploymentVersion,
16+
} from '@/hooks/queries/deployments'
17+
18+
interface VersionDescriptionModalProps {
19+
open: boolean
20+
onOpenChange: (open: boolean) => void
21+
workflowId: string
22+
version: number
23+
versionName: string
24+
currentDescription: string | null | undefined
25+
}
26+
27+
export function VersionDescriptionModal({
28+
open,
29+
onOpenChange,
30+
workflowId,
31+
version,
32+
versionName,
33+
currentDescription,
34+
}: VersionDescriptionModalProps) {
35+
const initialDescriptionRef = useRef(currentDescription || '')
36+
const [description, setDescription] = useState(initialDescriptionRef.current)
37+
const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false)
38+
39+
const updateMutation = useUpdateDeploymentVersion()
40+
const generateMutation = useGenerateVersionDescription()
41+
42+
const hasChanges = description.trim() !== initialDescriptionRef.current.trim()
43+
const isGenerating = generateMutation.isPending
44+
45+
const handleCloseAttempt = useCallback(() => {
46+
if (updateMutation.isPending || isGenerating) {
47+
return
48+
}
49+
if (hasChanges) {
50+
setShowUnsavedChangesAlert(true)
51+
} else {
52+
onOpenChange(false)
53+
}
54+
}, [hasChanges, updateMutation.isPending, isGenerating, onOpenChange])
55+
56+
const handleDiscardChanges = useCallback(() => {
57+
setShowUnsavedChangesAlert(false)
58+
setDescription(initialDescriptionRef.current)
59+
onOpenChange(false)
60+
}, [onOpenChange])
61+
62+
const handleGenerateDescription = useCallback(() => {
63+
generateMutation.mutate({
64+
workflowId,
65+
version,
66+
onStreamChunk: (accumulated) => {
67+
setDescription(accumulated)
68+
},
69+
})
70+
}, [workflowId, version, generateMutation])
71+
72+
const handleSave = useCallback(() => {
73+
if (!workflowId) return
74+
75+
updateMutation.mutate(
76+
{
77+
workflowId,
78+
version,
79+
description: description.trim() || null,
80+
},
81+
{
82+
onSuccess: () => {
83+
onOpenChange(false)
84+
},
85+
}
86+
)
87+
}, [workflowId, version, description, updateMutation, onOpenChange])
88+
89+
return (
90+
<>
91+
<Modal open={open} onOpenChange={(openState) => !openState && handleCloseAttempt()}>
92+
<ModalContent className='max-w-[480px]'>
93+
<ModalHeader>
94+
<span>Version Description</span>
95+
</ModalHeader>
96+
<ModalBody className='space-y-[10px]'>
97+
<div className='flex items-center justify-between'>
98+
<p className='text-[12px] text-[var(--text-secondary)]'>
99+
{currentDescription ? 'Edit the' : 'Add a'} description for{' '}
100+
<span className='font-medium text-[var(--text-primary)]'>{versionName}</span>
101+
</p>
102+
<Button
103+
variant='active'
104+
className='-my-1 h-5 px-2 py-0 text-[11px]'
105+
onClick={handleGenerateDescription}
106+
disabled={isGenerating || updateMutation.isPending}
107+
>
108+
{isGenerating ? 'Generating...' : 'Generate'}
109+
</Button>
110+
</div>
111+
<Textarea
112+
placeholder='Describe the changes in this deployment version...'
113+
className='min-h-[120px] resize-none'
114+
value={description}
115+
onChange={(e) => setDescription(e.target.value)}
116+
maxLength={500}
117+
disabled={isGenerating}
118+
/>
119+
<div className='flex items-center justify-between'>
120+
{(updateMutation.error || generateMutation.error) && (
121+
<p className='text-[12px] text-[var(--text-error)]'>
122+
{updateMutation.error?.message || generateMutation.error?.message}
123+
</p>
124+
)}
125+
{!updateMutation.error && !generateMutation.error && <div />}
126+
<p className='text-[11px] text-[var(--text-tertiary)]'>{description.length}/500</p>
127+
</div>
128+
</ModalBody>
129+
<ModalFooter>
130+
<Button
131+
variant='default'
132+
onClick={handleCloseAttempt}
133+
disabled={updateMutation.isPending || isGenerating}
134+
>
135+
Cancel
136+
</Button>
137+
<Button
138+
variant='tertiary'
139+
onClick={handleSave}
140+
disabled={updateMutation.isPending || isGenerating || !hasChanges}
141+
>
142+
{updateMutation.isPending ? 'Saving...' : 'Save'}
143+
</Button>
144+
</ModalFooter>
145+
</ModalContent>
146+
</Modal>
147+
148+
<Modal open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
149+
<ModalContent className='max-w-[400px]'>
150+
<ModalHeader>
151+
<span>Unsaved Changes</span>
152+
</ModalHeader>
153+
<ModalBody>
154+
<p className='text-[14px] text-[var(--text-secondary)]'>
155+
You have unsaved changes. Are you sure you want to discard them?
156+
</p>
157+
</ModalBody>
158+
<ModalFooter>
159+
<Button variant='default' onClick={() => setShowUnsavedChangesAlert(false)}>
160+
Keep Editing
161+
</Button>
162+
<Button variant='destructive' onClick={handleDiscardChanges}>
163+
Discard Changes
164+
</Button>
165+
</ModalFooter>
166+
</ModalContent>
167+
</Modal>
168+
</>
169+
)
170+
}

0 commit comments

Comments
 (0)