feat(description): refactor to tanstack query and remove useEffect

This commit is contained in:
waleed
2026-01-28 11:23:19 -08:00
parent d4af644159
commit 43f7f3bc00
6 changed files with 130 additions and 111 deletions

View File

@@ -136,7 +136,7 @@ export async function PATCH(
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 update deployment version', 500)

View File

@@ -1,7 +1,6 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useCallback, useState } from 'react'
import {
Button,
Modal,
@@ -11,8 +10,7 @@ import {
ModalHeader,
Textarea,
} from '@/components/emcn'
const logger = createLogger('VersionDescriptionModal')
import { useUpdateDeploymentVersion } from '@/hooks/queries/deployments'
interface VersionDescriptionModalProps {
open: boolean
@@ -21,7 +19,6 @@ interface VersionDescriptionModalProps {
version: number
versionName: string
currentDescription: string | null | undefined
onSave: () => Promise<void>
}
export function VersionDescriptionModal({
@@ -31,69 +28,46 @@ export function VersionDescriptionModal({
version,
versionName,
currentDescription,
onSave,
}: VersionDescriptionModalProps) {
const [description, setDescription] = useState('')
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
// 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 initialDescriptionRef = useRef('')
const updateMutation = useUpdateDeploymentVersion()
useEffect(() => {
if (open) {
const initialDescription = currentDescription || ''
setDescription(initialDescription)
initialDescriptionRef.current = initialDescription
setError(null)
}
}, [open, currentDescription])
const hasChanges = description.trim() !== initialDescriptionRef.current.trim()
const hasChanges = description.trim() !== initialDescription.trim()
const handleCloseAttempt = useCallback(() => {
if (hasChanges && !isSaving) {
if (hasChanges && !updateMutation.isPending) {
setShowUnsavedChangesAlert(true)
} else {
onOpenChange(false)
}
}, [hasChanges, isSaving, onOpenChange])
}, [hasChanges, updateMutation.isPending, onOpenChange])
const handleDiscardChanges = useCallback(() => {
setShowUnsavedChangesAlert(false)
setDescription(initialDescriptionRef.current)
setDescription(initialDescription)
onOpenChange(false)
}, [onOpenChange])
}, [initialDescription, 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)
updateMutation.mutate(
{
workflowId,
version,
description: description.trim() || null,
},
{
onSuccess: () => {
onOpenChange(false)
},
}
} 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])
)
}, [workflowId, version, description, updateMutation, onOpenChange])
return (
<>
@@ -104,7 +78,7 @@ export function VersionDescriptionModal({
</ModalHeader>
<ModalBody className='space-y-[12px]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
{currentDescription ? 'Edit' : 'Add'} a description for{' '}
{currentDescription ? 'Edit the' : 'Add a'} description for{' '}
<span className='font-medium text-[var(--text-primary)]'>{versionName}</span>
</p>
<Textarea
@@ -115,16 +89,30 @@ export function VersionDescriptionModal({
maxLength={500}
/>
<div className='flex items-center justify-between'>
{error ? <p className='text-[12px] text-[var(--text-error)]'>{error}</p> : <div />}
{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={isSaving}>
<Button
variant='default'
onClick={handleCloseAttempt}
disabled={updateMutation.isPending}
>
Cancel
</Button>
<Button variant='tertiary' onClick={handleSave} disabled={isSaving || !hasChanges}>
{isSaving ? 'Saving...' : 'Save'}
<Button
variant='tertiary'
onClick={handleSave}
disabled={updateMutation.isPending || !hasChanges}
>
{updateMutation.isPending ? 'Saving...' : 'Save'}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -1,7 +1,6 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import clsx from 'clsx'
import { FileText, MoreVertical, Pencil, RotateCcw, SendToBack } from 'lucide-react'
import {
@@ -15,16 +14,13 @@ import {
import { Skeleton } from '@/components/ui'
import { formatDateTime } from '@/lib/core/utils/formatting'
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils'
import { useUpdateDeploymentVersion } from '@/hooks/queries/deployments'
import { VersionDescriptionModal } from './version-description-modal'
const logger = createLogger('Versions')
/** Shared styling constants aligned with terminal component */
const HEADER_TEXT_CLASS = 'font-medium text-[var(--text-tertiary)] text-[12px]'
const ROW_TEXT_CLASS = 'font-medium text-[var(--text-primary)] text-[12px]'
const COLUMN_BASE_CLASS = 'flex-shrink-0'
/** Column width configuration */
const COLUMN_WIDTHS = {
VERSION: 'w-[180px]',
DEPLOYED_BY: 'w-[140px]',
@@ -40,20 +36,6 @@ interface VersionsProps {
onSelectVersion: (version: number | null) => void
onPromoteToLive: (version: number) => void
onLoadDeployment: (version: number) => void
fetchVersions: () => Promise<void>
}
/**
* Formats a timestamp into a readable string.
* @param value - The date string or Date object to format
* @returns Formatted string like "Jan 28, 2026, 10:43 AM"
*/
const formatTimestamp = (value: string | Date): string => {
const date = value instanceof Date ? value : new Date(value)
if (Number.isNaN(date.getTime())) {
return '-'
}
return formatDateTime(date)
}
/**
@@ -68,15 +50,15 @@ export function Versions({
onSelectVersion,
onPromoteToLive,
onLoadDeployment,
fetchVersions,
}: VersionsProps) {
const [editingVersion, setEditingVersion] = useState<number | null>(null)
const [editValue, setEditValue] = useState('')
const [isRenaming, setIsRenaming] = useState(false)
const [openDropdown, setOpenDropdown] = useState<number | null>(null)
const [descriptionModalVersion, setDescriptionModalVersion] = useState<number | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const renameMutation = useUpdateDeploymentVersion()
useEffect(() => {
if (editingVersion !== null && inputRef.current) {
inputRef.current.focus()
@@ -90,7 +72,7 @@ export function Versions({
setEditValue(currentName || `v${version}`)
}
const handleSaveRename = async (version: number) => {
const handleSaveRename = (version: number) => {
if (!workflowId || !editValue.trim()) {
setEditingVersion(null)
return
@@ -104,25 +86,21 @@ export function Versions({
return
}
setIsRenaming(true)
try {
const res = await fetch(`/api/workflows/${workflowId}/deployments/${version}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: editValue.trim() }),
})
if (res.ok) {
await fetchVersions()
setEditingVersion(null)
} else {
logger.error('Failed to rename version')
renameMutation.mutate(
{
workflowId,
version,
name: editValue.trim(),
},
{
onSuccess: () => {
setEditingVersion(null)
},
onError: () => {
// Keep editing state open on error so user can retry
},
}
} catch (error) {
logger.error('Error renaming version:', error)
} finally {
setIsRenaming(false)
}
)
}
const handleCancelRename = () => {
@@ -270,7 +248,7 @@ export function Versions({
'text-[var(--text-primary)] focus:outline-none focus:ring-0'
)}
maxLength={100}
disabled={isRenaming}
disabled={renameMutation.isPending}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
@@ -302,7 +280,7 @@ export function Versions({
<span
className={clsx('block truncate text-[var(--text-tertiary)]', ROW_TEXT_CLASS)}
>
{formatTimestamp(v.createdAt)}
{formatDateTime(new Date(v.createdAt))}
</span>
</div>
@@ -374,6 +352,7 @@ export function Versions({
{workflowId && descriptionModalVersionData && (
<VersionDescriptionModal
key={descriptionModalVersionData.version}
open={descriptionModalVersion !== null}
onOpenChange={(open) => !open && setDescriptionModalVersion(null)}
workflowId={workflowId}
@@ -382,7 +361,6 @@ export function Versions({
descriptionModalVersionData.name || `v${descriptionModalVersionData.version}`
}
currentDescription={descriptionModalVersionData.description}
onSave={fetchVersions}
/>
)}
</div>

View File

@@ -32,7 +32,6 @@ interface GeneralDeployProps {
versionsLoading: boolean
onPromoteToLive: (version: number) => Promise<void>
onLoadDeploymentComplete: () => void
fetchVersions: () => Promise<void>
}
type PreviewMode = 'active' | 'selected'
@@ -48,7 +47,6 @@ export function GeneralDeploy({
versionsLoading,
onPromoteToLive,
onLoadDeploymentComplete,
fetchVersions,
}: GeneralDeployProps) {
const [selectedVersion, setSelectedVersion] = useState<number | null>(null)
const [previewMode, setPreviewMode] = useState<PreviewMode>('active')
@@ -229,7 +227,6 @@ export function GeneralDeploy({
onSelectVersion={handleSelectVersion}
onPromoteToLive={handlePromoteToLive}
onLoadDeployment={handleLoadDeployment}
fetchVersions={fetchVersions}
/>
</div>
</div>

View File

@@ -135,11 +135,9 @@ export function DeployModal({
refetch: refetchDeploymentInfo,
} = useDeploymentInfo(workflowId, { enabled: open && isDeployed })
const {
data: versionsData,
isLoading: versionsLoading,
refetch: refetchVersions,
} = useDeploymentVersions(workflowId, { enabled: open })
const { data: versionsData, isLoading: versionsLoading } = useDeploymentVersions(workflowId, {
enabled: open,
})
const {
isLoading: isLoadingChat,
@@ -450,10 +448,6 @@ export function DeployModal({
deleteTrigger?.click()
}, [])
const handleFetchVersions = useCallback(async () => {
await refetchVersions()
}, [refetchVersions])
const isSubmitting = deployMutation.isPending
const isUndeploying = undeployMutation.isPending
@@ -512,7 +506,6 @@ export function DeployModal({
versionsLoading={versionsLoading}
onPromoteToLive={handlePromoteToLive}
onLoadDeploymentComplete={handleCloseModal}
fetchVersions={handleFetchVersions}
/>
</ModalTabsContent>

View File

@@ -348,6 +348,69 @@ export function useUndeployWorkflow() {
})
}
/**
* Variables for update deployment version mutation
*/
interface UpdateDeploymentVersionVariables {
workflowId: string
version: number
name?: string
description?: string | null
}
/**
* Response from update deployment version mutation
*/
interface UpdateDeploymentVersionResult {
name: string | null
description: string | null
}
/**
* Mutation hook for updating a deployment version's name or description.
* Invalidates versions query on success.
*/
export function useUpdateDeploymentVersion() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
workflowId,
version,
name,
description,
}: UpdateDeploymentVersionVariables): Promise<UpdateDeploymentVersionResult> => {
const response = await fetch(`/api/workflows/${workflowId}/deployments/${version}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, description }),
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to update deployment version')
}
return response.json()
},
onSuccess: (_, variables) => {
logger.info('Deployment version updated', {
workflowId: variables.workflowId,
version: variables.version,
})
queryClient.invalidateQueries({
queryKey: deploymentKeys.versions(variables.workflowId),
})
},
onError: (error) => {
logger.error('Failed to update deployment version', { error })
},
})
}
/**
* Variables for activate version mutation
*/