mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-28 16:27:55 -05:00
feat(description): refactor to tanstack query and remove useEffect
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user