Skip to content

Commit 1de6f09

Browse files
authored
feat(versions): added the ability to rename deployment versions (#1610)
1 parent b10b246 commit 1de6f09

File tree

8 files changed

+7155
-14
lines changed

8 files changed

+7155
-14
lines changed

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

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export async function GET(
1919
const { id, version } = await params
2020

2121
try {
22-
// Validate permissions and get workflow data
2322
const { error } = await validateWorkflowPermissions(id, requestId, 'read')
2423
if (error) {
2524
return createErrorResponse(error.message, error.status)
@@ -54,3 +53,66 @@ export async function GET(
5453
return createErrorResponse(error.message || 'Failed to fetch deployment version', 500)
5554
}
5655
}
56+
57+
export async function PATCH(
58+
request: NextRequest,
59+
{ params }: { params: Promise<{ id: string; version: string }> }
60+
) {
61+
const requestId = generateRequestId()
62+
const { id, version } = await params
63+
64+
try {
65+
const { error } = await validateWorkflowPermissions(id, requestId, 'write')
66+
if (error) {
67+
return createErrorResponse(error.message, error.status)
68+
}
69+
70+
const versionNum = Number(version)
71+
if (!Number.isFinite(versionNum)) {
72+
return createErrorResponse('Invalid version', 400)
73+
}
74+
75+
const body = await request.json()
76+
const { name } = body
77+
78+
if (typeof name !== 'string') {
79+
return createErrorResponse('Name must be a string', 400)
80+
}
81+
82+
const trimmedName = name.trim()
83+
if (trimmedName.length === 0) {
84+
return createErrorResponse('Name cannot be empty', 400)
85+
}
86+
87+
if (trimmedName.length > 100) {
88+
return createErrorResponse('Name must be 100 characters or less', 400)
89+
}
90+
91+
const [updated] = await db
92+
.update(workflowDeploymentVersion)
93+
.set({ name: trimmedName })
94+
.where(
95+
and(
96+
eq(workflowDeploymentVersion.workflowId, id),
97+
eq(workflowDeploymentVersion.version, versionNum)
98+
)
99+
)
100+
.returning({ id: workflowDeploymentVersion.id, name: workflowDeploymentVersion.name })
101+
102+
if (!updated) {
103+
return createErrorResponse('Deployment version not found', 404)
104+
}
105+
106+
logger.info(
107+
`[${requestId}] Renamed deployment version ${version} for workflow ${id} to "${trimmedName}"`
108+
)
109+
110+
return createSuccessResponse({ name: updated.name })
111+
} catch (error: any) {
112+
logger.error(
113+
`[${requestId}] Error renaming deployment version ${version} for workflow ${id}`,
114+
error
115+
)
116+
return createErrorResponse(error.message || 'Failed to rename deployment version', 500)
117+
}
118+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
2525
.select({
2626
id: workflowDeploymentVersion.id,
2727
version: workflowDeploymentVersion.version,
28+
name: workflowDeploymentVersion.name,
2829
isActive: workflowDeploymentVersion.isActive,
2930
createdAt: workflowDeploymentVersion.createdAt,
3031
createdBy: workflowDeploymentVersion.createdBy,

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx

Lines changed: 117 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useEffect, useState } from 'react'
3+
import { useEffect, useRef, useState } from 'react'
44
import { Loader2, MoreVertical, X } from 'lucide-react'
55
import {
66
Button,
@@ -102,6 +102,18 @@ export function DeployModal({
102102
const [previewDeployedState, setPreviewDeployedState] = useState<WorkflowState | null>(null)
103103
const [currentPage, setCurrentPage] = useState(1)
104104
const itemsPerPage = 5
105+
const [editingVersion, setEditingVersion] = useState<number | null>(null)
106+
const [editValue, setEditValue] = useState('')
107+
const [isRenaming, setIsRenaming] = useState(false)
108+
const [openDropdown, setOpenDropdown] = useState<number | null>(null)
109+
const inputRef = useRef<HTMLInputElement>(null)
110+
111+
useEffect(() => {
112+
if (editingVersion !== null && inputRef.current) {
113+
inputRef.current.focus()
114+
inputRef.current.select()
115+
}
116+
}, [editingVersion])
105117

106118
const getInputFormatExample = (includeStreaming = false) => {
107119
let inputFormatExample = ''
@@ -419,6 +431,52 @@ export function DeployModal({
419431
}
420432
}
421433

434+
const handleStartRename = (version: number, currentName: string | null | undefined) => {
435+
setOpenDropdown(null) // Close dropdown first
436+
setEditingVersion(version)
437+
setEditValue(currentName || `v${version}`)
438+
}
439+
440+
const handleSaveRename = async (version: number) => {
441+
if (!workflowId || !editValue.trim()) {
442+
setEditingVersion(null)
443+
return
444+
}
445+
446+
const currentVersion = versions.find((v) => v.version === version)
447+
const currentName = currentVersion?.name || `v${version}`
448+
449+
if (editValue.trim() === currentName) {
450+
setEditingVersion(null)
451+
return
452+
}
453+
454+
setIsRenaming(true)
455+
try {
456+
const res = await fetch(`/api/workflows/${workflowId}/deployments/${version}`, {
457+
method: 'PATCH',
458+
headers: { 'Content-Type': 'application/json' },
459+
body: JSON.stringify({ name: editValue.trim() }),
460+
})
461+
462+
if (res.ok) {
463+
await fetchVersions()
464+
setEditingVersion(null)
465+
} else {
466+
logger.error('Failed to rename version')
467+
}
468+
} catch (error) {
469+
logger.error('Error renaming version:', error)
470+
} finally {
471+
setIsRenaming(false)
472+
}
473+
}
474+
475+
const handleCancelRename = () => {
476+
setEditingVersion(null)
477+
setEditValue('')
478+
}
479+
422480
const handleUndeploy = async () => {
423481
try {
424482
setIsUndeploying(true)
@@ -539,7 +597,7 @@ export function DeployModal({
539597
return (
540598
<Dialog open={open} onOpenChange={handleCloseModal}>
541599
<DialogContent
542-
className='flex max-h-[90vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'
600+
className='flex max-h-[90vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[700px]'
543601
hideCloseButton
544602
>
545603
<DialogHeader className='flex-shrink-0 border-b px-6 py-4'>
@@ -650,13 +708,13 @@ export function DeployModal({
650708
<thead className='border-b bg-muted/50'>
651709
<tr>
652710
<th className='w-10' />
653-
<th className='px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
711+
<th className='w-[200px] whitespace-nowrap px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
654712
Version
655713
</th>
656-
<th className='px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
714+
<th className='whitespace-nowrap px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
657715
Deployed By
658716
</th>
659-
<th className='px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
717+
<th className='whitespace-nowrap px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
660718
Created
661719
</th>
662720
<th className='w-10' />
@@ -669,7 +727,11 @@ export function DeployModal({
669727
<tr
670728
key={v.id}
671729
className='cursor-pointer transition-colors hover:bg-muted/30'
672-
onClick={() => openVersionPreview(v.version)}
730+
onClick={() => {
731+
if (editingVersion !== v.version) {
732+
openVersionPreview(v.version)
733+
}
734+
}}
673735
>
674736
<td className='px-4 py-2.5'>
675737
<div
@@ -679,22 +741,54 @@ export function DeployModal({
679741
title={v.isActive ? 'Active' : 'Inactive'}
680742
/>
681743
</td>
682-
<td className='px-4 py-2.5'>
683-
<span className='font-medium text-sm'>v{v.version}</span>
744+
<td className='w-[220px] max-w-[220px] px-4 py-2.5'>
745+
{editingVersion === v.version ? (
746+
<input
747+
ref={inputRef}
748+
value={editValue}
749+
onChange={(e) => setEditValue(e.target.value)}
750+
onKeyDown={(e) => {
751+
if (e.key === 'Enter') {
752+
e.preventDefault()
753+
handleSaveRename(v.version)
754+
} else if (e.key === 'Escape') {
755+
e.preventDefault()
756+
handleCancelRename()
757+
}
758+
}}
759+
onBlur={() => handleSaveRename(v.version)}
760+
className='w-full border-0 bg-transparent p-0 font-medium text-sm leading-5 outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
761+
maxLength={100}
762+
disabled={isRenaming}
763+
autoComplete='off'
764+
autoCorrect='off'
765+
autoCapitalize='off'
766+
spellCheck='false'
767+
/>
768+
) : (
769+
<span className='block whitespace-pre-wrap break-words break-all font-medium text-sm leading-5'>
770+
{v.name || `v${v.version}`}
771+
</span>
772+
)}
684773
</td>
685-
<td className='px-4 py-2.5'>
774+
<td className='whitespace-nowrap px-4 py-2.5'>
686775
<span className='text-muted-foreground text-sm'>
687776
{v.deployedBy || 'Unknown'}
688777
</span>
689778
</td>
690-
<td className='px-4 py-2.5'>
779+
<td className='whitespace-nowrap px-4 py-2.5'>
691780
<span className='text-muted-foreground text-sm'>
692781
{new Date(v.createdAt).toLocaleDateString()}{' '}
693782
{new Date(v.createdAt).toLocaleTimeString()}
694783
</span>
695784
</td>
696785
<td className='px-4 py-2.5' onClick={(e) => e.stopPropagation()}>
697-
<DropdownMenu>
786+
<DropdownMenu
787+
open={openDropdown === v.version}
788+
onOpenChange={(open) =>
789+
setOpenDropdown(open ? v.version : null)
790+
}
791+
>
698792
<DropdownMenuTrigger asChild>
699793
<Button
700794
variant='ghost'
@@ -705,7 +799,10 @@ export function DeployModal({
705799
<MoreVertical className='h-4 w-4' />
706800
</Button>
707801
</DropdownMenuTrigger>
708-
<DropdownMenuContent align='end'>
802+
<DropdownMenuContent
803+
align='end'
804+
onCloseAutoFocus={(event) => event.preventDefault()}
805+
>
709806
<DropdownMenuItem
710807
onClick={() => activateVersion(v.version)}
711808
disabled={v.isActive || activatingVersion === v.version}
@@ -721,6 +818,11 @@ export function DeployModal({
721818
>
722819
Inspect
723820
</DropdownMenuItem>
821+
<DropdownMenuItem
822+
onClick={() => handleStartRename(v.version, v.name)}
823+
>
824+
Rename
825+
</DropdownMenuItem>
724826
</DropdownMenuContent>
725827
</DropdownMenu>
726828
</td>
@@ -889,7 +991,9 @@ export function DeployModal({
889991
selectedVersion={previewVersion}
890992
onActivateVersion={() => activateVersion(previewVersion)}
891993
isActivating={activatingVersion === previewVersion}
892-
selectedVersionLabel={`v${previewVersion}`}
994+
selectedVersionLabel={
995+
versions.find((v) => v.version === previewVersion)?.name || `v${previewVersion}`
996+
}
893997
workflowId={workflowId}
894998
isSelectedVersionActive={versions.find((v) => v.version === previewVersion)?.isActive}
895999
/>

apps/sim/lib/workflows/db-helpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type WorkflowDeploymentVersion = InferSelectModel<typeof workflowDeployme
2222
export interface WorkflowDeploymentVersionResponse {
2323
id: string
2424
version: number
25+
name?: string | null
2526
isActive: boolean
2627
createdAt: string
2728
createdBy?: string | null
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "workflow_deployment_version" ADD COLUMN "name" text;

0 commit comments

Comments
 (0)