215 lines
7.4 KiB
TypeScript
215 lines
7.4 KiB
TypeScript
import { useEffect } from 'react'
|
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
|
import { Save, ArrowLeft, ListOrdered, Wrench } from 'lucide-react'
|
|
import { treesApi } from '@/api/trees'
|
|
import { useProceduralEditorStore } from '@/store/proceduralEditorStore'
|
|
import { IntakeFormBuilder } from '@/components/procedural-editor/IntakeFormBuilder'
|
|
import { StepList } from '@/components/procedural-editor/StepList'
|
|
import { TagInput } from '@/components/common/TagInput'
|
|
import { toast } from '@/lib/toast'
|
|
import type { TreeType } from '@/types'
|
|
|
|
export function ProceduralEditorPage() {
|
|
const { id } = useParams<{ id: string }>()
|
|
const [searchParams] = useSearchParams()
|
|
const navigate = useNavigate()
|
|
const isEditMode = !!id
|
|
|
|
const {
|
|
treeId,
|
|
treeType,
|
|
name,
|
|
description,
|
|
tags,
|
|
isPublic,
|
|
isDirty,
|
|
isSaving,
|
|
isLoading,
|
|
initNew,
|
|
loadTree,
|
|
reset,
|
|
setName,
|
|
setDescription,
|
|
setTags,
|
|
setIsPublic,
|
|
setIsSaving,
|
|
markSaved,
|
|
getTreeForSave,
|
|
} = useProceduralEditorStore()
|
|
|
|
const isMaintenance = treeType === 'maintenance'
|
|
const flowLabel = isMaintenance ? 'Maintenance Flow' : 'Procedure'
|
|
|
|
// Load tree or init new
|
|
useEffect(() => {
|
|
if (isEditMode && id) {
|
|
loadExistingTree(id)
|
|
} else {
|
|
const urlType = searchParams.get('type')
|
|
initNew((urlType === 'maintenance' ? 'maintenance' : 'procedural') as TreeType)
|
|
}
|
|
|
|
return () => { reset() }
|
|
}, [id])
|
|
|
|
const loadExistingTree = async (treeId: string) => {
|
|
try {
|
|
const tree = await treesApi.get(treeId)
|
|
if (tree.tree_type !== 'procedural' && tree.tree_type !== 'maintenance') {
|
|
toast.error('This flow is not a procedural or maintenance flow')
|
|
navigate('/my-trees')
|
|
return
|
|
}
|
|
loadTree(tree)
|
|
} catch {
|
|
toast.error('Failed to load flow')
|
|
navigate('/my-trees')
|
|
}
|
|
}
|
|
|
|
const handleSave = async (saveStatus?: 'draft' | 'published') => {
|
|
if (!name.trim()) {
|
|
toast.error(`Please enter a name for the ${flowLabel.toLowerCase()}`)
|
|
return
|
|
}
|
|
|
|
setIsSaving(true)
|
|
try {
|
|
const payload = getTreeForSave()
|
|
if (saveStatus) {
|
|
payload.status = saveStatus
|
|
}
|
|
|
|
if (isEditMode && treeId) {
|
|
await treesApi.update(treeId, payload)
|
|
markSaved()
|
|
toast.success(`${flowLabel} saved`)
|
|
} else {
|
|
const created = await treesApi.create(payload)
|
|
markSaved()
|
|
toast.success(`${flowLabel} created`)
|
|
navigate(`/flows/${created.id}/edit`, { replace: true })
|
|
}
|
|
} catch (err: unknown) {
|
|
const message = err && typeof err === 'object' && 'response' in err
|
|
? (err as { response?: { data?: { detail?: string | { message?: string } } } }).response?.data?.detail
|
|
: null
|
|
const errorText = typeof message === 'string' ? message : typeof message === 'object' && message?.message ? message.message : `Failed to save ${flowLabel.toLowerCase()}`
|
|
toast.error(errorText)
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex min-h-[50vh] items-center justify-center">
|
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
|
{/* Header */}
|
|
<div className="mb-6 flex items-center justify-between sm:mb-8">
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => navigate('/my-trees')}
|
|
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<ArrowLeft className="h-5 w-5" />
|
|
</button>
|
|
<div className="flex items-center gap-2">
|
|
{isMaintenance
|
|
? <Wrench className="h-5 w-5 text-amber-400" />
|
|
: <ListOrdered className="h-5 w-5 text-muted-foreground" />}
|
|
<h1 className="text-xl font-bold text-foreground sm:text-2xl">
|
|
{isEditMode ? `Edit ${flowLabel}` : `New ${flowLabel}`}
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{isDirty && (
|
|
<span className="text-xs text-muted-foreground">Unsaved changes</span>
|
|
)}
|
|
<button
|
|
onClick={() => handleSave('draft')}
|
|
disabled={isSaving}
|
|
className="flex items-center gap-1.5 rounded-md border border-border px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50"
|
|
>
|
|
Save Draft
|
|
</button>
|
|
<button
|
|
onClick={() => handleSave('published')}
|
|
disabled={isSaving}
|
|
className="flex items-center gap-1.5 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50"
|
|
>
|
|
<Save className="h-4 w-4" />
|
|
{isSaving ? 'Saving...' : 'Publish'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="space-y-6">
|
|
{/* Metadata */}
|
|
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
|
|
<h2 className="mb-4 text-lg font-semibold text-foreground">Details</h2>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="mb-1 block text-sm font-medium text-muted-foreground">Name</label>
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="e.g. Domain Controller Build"
|
|
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1 block text-sm font-medium text-muted-foreground">Description</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="Brief description of this procedure..."
|
|
rows={2}
|
|
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="mb-1 block text-sm font-medium text-muted-foreground">Tags</label>
|
|
<TagInput tags={tags} onChange={setTags} />
|
|
</div>
|
|
|
|
<div className="flex items-end pb-1">
|
|
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<input
|
|
type="checkbox"
|
|
checked={isPublic}
|
|
onChange={(e) => setIsPublic(e.target.checked)}
|
|
className="rounded border-border"
|
|
/>
|
|
Public (visible to all users)
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Intake Form Builder */}
|
|
<IntakeFormBuilder />
|
|
|
|
{/* Step List */}
|
|
<StepList />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ProceduralEditorPage
|