fix: wire up maintenance flow creation, editing, and navigation in frontend
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate, Link } from 'react-router-dom'
|
import { useNavigate, Link } from 'react-router-dom'
|
||||||
import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus, ListOrdered, ChevronDown } from 'lucide-react'
|
import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus, ListOrdered, ChevronDown, Wrench } from 'lucide-react'
|
||||||
import { treesApi } from '@/api/trees'
|
import { treesApi } from '@/api/trees'
|
||||||
import { sessionsApi } from '@/api/sessions'
|
import { sessionsApi } from '@/api/sessions'
|
||||||
import type { TreeListItem } from '@/types'
|
import type { TreeListItem } from '@/types'
|
||||||
@@ -71,7 +71,9 @@ export function MyTreesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleStartSession = (tree: TreeWithStats) => {
|
const handleStartSession = (tree: TreeWithStats) => {
|
||||||
if (tree.tree_type === 'procedural') {
|
if (tree.tree_type === 'maintenance') {
|
||||||
|
navigate(`/flows/${tree.id}/maintenance`)
|
||||||
|
} else if (tree.tree_type === 'procedural') {
|
||||||
navigate(`/flows/${tree.id}/navigate`)
|
navigate(`/flows/${tree.id}/navigate`)
|
||||||
} else {
|
} else {
|
||||||
navigate(`/trees/${tree.id}/navigate`)
|
navigate(`/trees/${tree.id}/navigate`)
|
||||||
@@ -79,7 +81,8 @@ export function MyTreesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getEditPath = (tree: TreeWithStats) => {
|
const getEditPath = (tree: TreeWithStats) => {
|
||||||
return tree.tree_type === 'procedural' ? `/flows/${tree.id}/edit` : `/trees/${tree.id}/edit`
|
return tree.tree_type === 'procedural' || tree.tree_type === 'maintenance'
|
||||||
|
? `/flows/${tree.id}/edit` : `/trees/${tree.id}/edit`
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteTree = async () => {
|
const handleDeleteTree = async () => {
|
||||||
@@ -153,6 +156,17 @@ export function MyTreesPage() {
|
|||||||
<div className="text-xs text-muted-foreground">Step-by-step procedure</div>
|
<div className="text-xs text-muted-foreground">Step-by-step procedure</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/flows/new?type=maintenance"
|
||||||
|
onClick={() => setShowCreateMenu(false)}
|
||||||
|
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
|
||||||
|
>
|
||||||
|
<Wrench className="h-4 w-4 text-amber-400" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">Maintenance Flow</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Scheduled multi-target tasks</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -209,6 +223,9 @@ export function MyTreesPage() {
|
|||||||
{tree.tree_type === 'procedural' && (
|
{tree.tree_type === 'procedural' && (
|
||||||
<ListOrdered className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<ListOrdered className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
|
{tree.tree_type === 'maintenance' && (
|
||||||
|
<Wrench className="h-4 w-4 shrink-0 text-amber-400" />
|
||||||
|
)}
|
||||||
<h3 className="font-semibold text-foreground">{tree.name}</h3>
|
<h3 className="font-semibold text-foreground">{tree.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
@@ -217,6 +234,11 @@ export function MyTreesPage() {
|
|||||||
Procedure
|
Procedure
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{tree.tree_type === 'maintenance' && (
|
||||||
|
<span className="rounded-full bg-amber-400/10 px-2 py-0.5 text-[10px] font-medium text-amber-400">
|
||||||
|
Maintenance
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{tree.category_info && (
|
{tree.category_info && (
|
||||||
<span className="rounded-full bg-accent px-2 py-0.5 text-xs text-muted-foreground">
|
<span className="rounded-full bg-accent px-2 py-0.5 text-xs text-muted-foreground">
|
||||||
{tree.category_info.name}
|
{tree.category_info.name}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { Save, ArrowLeft, ListOrdered } from 'lucide-react'
|
import { Save, ArrowLeft, ListOrdered, Wrench } from 'lucide-react'
|
||||||
import { treesApi } from '@/api/trees'
|
import { treesApi } from '@/api/trees'
|
||||||
import { useProceduralEditorStore } from '@/store/proceduralEditorStore'
|
import { useProceduralEditorStore } from '@/store/proceduralEditorStore'
|
||||||
import { IntakeFormBuilder } from '@/components/procedural-editor/IntakeFormBuilder'
|
import { IntakeFormBuilder } from '@/components/procedural-editor/IntakeFormBuilder'
|
||||||
import { StepList } from '@/components/procedural-editor/StepList'
|
import { StepList } from '@/components/procedural-editor/StepList'
|
||||||
import { TagInput } from '@/components/common/TagInput'
|
import { TagInput } from '@/components/common/TagInput'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
|
import type { TreeType } from '@/types'
|
||||||
|
|
||||||
export function ProceduralEditorPage() {
|
export function ProceduralEditorPage() {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const isEditMode = !!id
|
const isEditMode = !!id
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treeId,
|
treeId,
|
||||||
|
treeType,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
tags,
|
tags,
|
||||||
@@ -34,12 +37,16 @@ export function ProceduralEditorPage() {
|
|||||||
getTreeForSave,
|
getTreeForSave,
|
||||||
} = useProceduralEditorStore()
|
} = useProceduralEditorStore()
|
||||||
|
|
||||||
|
const isMaintenance = treeType === 'maintenance'
|
||||||
|
const flowLabel = isMaintenance ? 'Maintenance Flow' : 'Procedure'
|
||||||
|
|
||||||
// Load tree or init new
|
// Load tree or init new
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditMode && id) {
|
if (isEditMode && id) {
|
||||||
loadExistingTree(id)
|
loadExistingTree(id)
|
||||||
} else {
|
} else {
|
||||||
initNew()
|
const urlType = searchParams.get('type')
|
||||||
|
initNew((urlType === 'maintenance' ? 'maintenance' : 'procedural') as TreeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => { reset() }
|
return () => { reset() }
|
||||||
@@ -48,21 +55,21 @@ export function ProceduralEditorPage() {
|
|||||||
const loadExistingTree = async (treeId: string) => {
|
const loadExistingTree = async (treeId: string) => {
|
||||||
try {
|
try {
|
||||||
const tree = await treesApi.get(treeId)
|
const tree = await treesApi.get(treeId)
|
||||||
if (tree.tree_type !== 'procedural') {
|
if (tree.tree_type !== 'procedural' && tree.tree_type !== 'maintenance') {
|
||||||
toast.error('This tree is not a procedural flow')
|
toast.error('This flow is not a procedural or maintenance flow')
|
||||||
navigate('/my-trees')
|
navigate('/my-trees')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadTree(tree)
|
loadTree(tree)
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to load procedure')
|
toast.error('Failed to load flow')
|
||||||
navigate('/my-trees')
|
navigate('/my-trees')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = async (saveStatus?: 'draft' | 'published') => {
|
const handleSave = async (saveStatus?: 'draft' | 'published') => {
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
toast.error('Please enter a name for the procedure')
|
toast.error(`Please enter a name for the ${flowLabel.toLowerCase()}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,18 +83,18 @@ export function ProceduralEditorPage() {
|
|||||||
if (isEditMode && treeId) {
|
if (isEditMode && treeId) {
|
||||||
await treesApi.update(treeId, payload)
|
await treesApi.update(treeId, payload)
|
||||||
markSaved()
|
markSaved()
|
||||||
toast.success('Procedure saved')
|
toast.success(`${flowLabel} saved`)
|
||||||
} else {
|
} else {
|
||||||
const created = await treesApi.create(payload)
|
const created = await treesApi.create(payload)
|
||||||
markSaved()
|
markSaved()
|
||||||
toast.success('Procedure created')
|
toast.success(`${flowLabel} created`)
|
||||||
navigate(`/flows/${created.id}/edit`, { replace: true })
|
navigate(`/flows/${created.id}/edit`, { replace: true })
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const message = err && typeof err === 'object' && 'response' in err
|
const message = err && typeof err === 'object' && 'response' in err
|
||||||
? (err as { response?: { data?: { detail?: string | { message?: string } } } }).response?.data?.detail
|
? (err as { response?: { data?: { detail?: string | { message?: string } } } }).response?.data?.detail
|
||||||
: null
|
: null
|
||||||
const errorText = typeof message === 'string' ? message : typeof message === 'object' && message?.message ? message.message : 'Failed to save procedure'
|
const errorText = typeof message === 'string' ? message : typeof message === 'object' && message?.message ? message.message : `Failed to save ${flowLabel.toLowerCase()}`
|
||||||
toast.error(errorText)
|
toast.error(errorText)
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false)
|
setIsSaving(false)
|
||||||
@@ -114,9 +121,11 @@ export function ProceduralEditorPage() {
|
|||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ListOrdered className="h-5 w-5 text-muted-foreground" />
|
{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">
|
<h1 className="text-xl font-bold text-foreground sm:text-2xl">
|
||||||
{isEditMode ? 'Edit Procedure' : 'New Procedure'}
|
{isEditMode ? `Edit ${flowLabel}` : `New ${flowLabel}`}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -196,7 +196,9 @@ export function TreeLibraryPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleStartSession = (treeId: string, treeType?: string) => {
|
const handleStartSession = (treeId: string, treeType?: string) => {
|
||||||
if (treeType === 'procedural') {
|
if (treeType === 'maintenance') {
|
||||||
|
navigate(`/flows/${treeId}/maintenance`)
|
||||||
|
} else if (treeType === 'procedural') {
|
||||||
navigate(`/flows/${treeId}/navigate`)
|
navigate(`/flows/${treeId}/navigate`)
|
||||||
} else {
|
} else {
|
||||||
navigate(`/trees/${treeId}/navigate`)
|
navigate(`/trees/${treeId}/navigate`)
|
||||||
@@ -267,14 +269,14 @@ export function TreeLibraryPage() {
|
|||||||
</div>
|
</div>
|
||||||
{canCreateTrees && (
|
{canCreateTrees && (
|
||||||
<Link
|
<Link
|
||||||
to={typeFilter === 'procedural' ? '/flows/new' : '/trees/new'}
|
to={typeFilter === 'procedural' ? '/flows/new' : typeFilter === 'maintenance' ? '/flows/new?type=maintenance' : '/trees/new'}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
|
'flex items-center gap-2 rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
|
||||||
'hover:opacity-90'
|
'hover:opacity-90'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
{typeFilter === 'procedural' ? 'New Project' : 'Create Flow'}
|
{typeFilter === 'procedural' ? 'New Project' : typeFilter === 'maintenance' ? 'New Maintenance Flow' : 'Create Flow'}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ function createDefaultField(index: number, existingFields: IntakeFormField[]): I
|
|||||||
interface ProceduralEditorState {
|
interface ProceduralEditorState {
|
||||||
// Tree metadata
|
// Tree metadata
|
||||||
treeId: string | null
|
treeId: string | null
|
||||||
|
treeType: TreeType
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
categoryId: string | null
|
categoryId: string | null
|
||||||
@@ -83,11 +84,12 @@ interface ProceduralEditorState {
|
|||||||
isSaving: boolean
|
isSaving: boolean
|
||||||
|
|
||||||
// Actions - Init
|
// Actions - Init
|
||||||
initNew: () => void
|
initNew: (type?: TreeType) => void
|
||||||
loadTree: (tree: Tree) => void
|
loadTree: (tree: Tree) => void
|
||||||
reset: () => void
|
reset: () => void
|
||||||
|
|
||||||
// Actions - Metadata
|
// Actions - Metadata
|
||||||
|
setTreeType: (treeType: TreeType) => void
|
||||||
setName: (name: string) => void
|
setName: (name: string) => void
|
||||||
setDescription: (description: string) => void
|
setDescription: (description: string) => void
|
||||||
setCategoryId: (categoryId: string | null) => void
|
setCategoryId: (categoryId: string | null) => void
|
||||||
@@ -131,6 +133,7 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
|
|||||||
immer((set, get) => ({
|
immer((set, get) => ({
|
||||||
// Initial state
|
// Initial state
|
||||||
treeId: null,
|
treeId: null,
|
||||||
|
treeType: 'procedural' as TreeType,
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
categoryId: null,
|
categoryId: null,
|
||||||
@@ -146,9 +149,10 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
|
|||||||
isSaving: false,
|
isSaving: false,
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
initNew: () => {
|
initNew: (type?: TreeType) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.treeId = null
|
state.treeId = null
|
||||||
|
state.treeType = type || 'procedural'
|
||||||
state.name = ''
|
state.name = ''
|
||||||
state.description = ''
|
state.description = ''
|
||||||
state.categoryId = null
|
state.categoryId = null
|
||||||
@@ -169,6 +173,7 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
|
|||||||
const structure = tree.tree_structure as unknown as ProceduralTreeStructure
|
const structure = tree.tree_structure as unknown as ProceduralTreeStructure
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.treeId = tree.id
|
state.treeId = tree.id
|
||||||
|
state.treeType = tree.tree_type
|
||||||
state.name = tree.name
|
state.name = tree.name
|
||||||
state.description = tree.description || ''
|
state.description = tree.description || ''
|
||||||
state.categoryId = tree.category_id
|
state.categoryId = tree.category_id
|
||||||
@@ -188,6 +193,7 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
|
|||||||
reset: () => {
|
reset: () => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.treeId = null
|
state.treeId = null
|
||||||
|
state.treeType = 'procedural'
|
||||||
state.name = ''
|
state.name = ''
|
||||||
state.description = ''
|
state.description = ''
|
||||||
state.categoryId = null
|
state.categoryId = null
|
||||||
@@ -205,6 +211,7 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Metadata
|
// Metadata
|
||||||
|
setTreeType: (treeType) => set((state) => { state.treeType = treeType }),
|
||||||
setName: (name) => set((state) => { state.name = name; state.isDirty = true }),
|
setName: (name) => set((state) => { state.name = name; state.isDirty = true }),
|
||||||
setDescription: (description) => set((state) => { state.description = description; state.isDirty = true }),
|
setDescription: (description) => set((state) => { state.description = description; state.isDirty = true }),
|
||||||
setCategoryId: (categoryId) => set((state) => { state.categoryId = categoryId; state.isDirty = true }),
|
setCategoryId: (categoryId) => set((state) => { state.categoryId = categoryId; state.isDirty = true }),
|
||||||
@@ -343,7 +350,7 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
|
|||||||
return {
|
return {
|
||||||
name: state.name,
|
name: state.name,
|
||||||
description: state.description,
|
description: state.description,
|
||||||
tree_type: 'procedural' as TreeType,
|
tree_type: state.treeType,
|
||||||
tree_structure: { steps: state.steps },
|
tree_structure: { steps: state.steps },
|
||||||
intake_form: state.intakeForm.length > 0 ? state.intakeForm : undefined,
|
intake_form: state.intakeForm.length > 0 ? state.intakeForm : undefined,
|
||||||
category_id: state.categoryId,
|
category_id: state.categoryId,
|
||||||
|
|||||||
Reference in New Issue
Block a user