feat: Add custom step creation and backend support (Phase 3A: B.8-B.10, B.13)
Implements custom step creation forms and backend persistence: Task B.8 - StepForm Component: - Comprehensive form for creating custom steps - Step type selection (decision/action/solution) with descriptions - Required fields: title, instructions (markdown supported) - Optional fields: help text, commands (dynamic array), category, tags - Visibility control (private/team/public) - Save to library checkbox - Full validation with error display - Dynamic command management (add/remove, label + command) - Tag input with Enter key support Task B.9 - CustomStepModal: - Tabbed modal interface - Tab 1: "Type My Own" - embeds StepForm - Tab 2: "Browse Library" - embeds StepLibraryBrowser - Handles both saved steps (API) and drafts (no save) - Loading states during step creation - Error handling with user feedback - Returns Step or CustomStepDraft to parent Task B.10 - Backend Custom Steps Support: - Database migration: add custom_steps JSONB column to sessions - Updated Session model with custom_steps field - Updated SessionResponse schema with custom_steps - Updated SessionUpdate schema to accept custom_steps - Migration ready to run: 4cdb5cba1aff Task B.13 - Session Types Updates: - Added CustomStep and CustomStepDraft interfaces - Updated Session interface with custom_steps field - Updated SessionUpdate interface - Exported step types from types/index.ts - Full TypeScript support for custom step integration Remaining tasks: B.11 (TreeNavigationPage integration), B.12 (Export) Build tested successfully. Related: Issues #8, #9, #10 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
148
frontend/src/components/step-library/CustomStepModal.tsx
Normal file
148
frontend/src/components/step-library/CustomStepModal.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { useState } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { stepsApi } from '@/api'
|
||||
import { StepForm } from './StepForm'
|
||||
import { StepLibraryBrowser } from './StepLibraryBrowser'
|
||||
import type { Step, StepCreate } from '@/types/step'
|
||||
|
||||
export interface CustomStepDraft {
|
||||
title: string
|
||||
step_type: 'decision' | 'action' | 'solution'
|
||||
content: {
|
||||
instructions: string
|
||||
help_text?: string
|
||||
commands?: Array<{
|
||||
label: string
|
||||
command: string
|
||||
command_type?: string
|
||||
}>
|
||||
}
|
||||
category_id?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
interface CustomStepModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onInsertStep: (step: Step | CustomStepDraft) => void
|
||||
}
|
||||
|
||||
type Tab = 'create' | 'browse'
|
||||
|
||||
export function CustomStepModal({ isOpen, onClose, onInsertStep }: CustomStepModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('create')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const handleFormSubmit = async (data: StepCreate, saveToLibrary: boolean) => {
|
||||
setIsSubmitting(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
if (saveToLibrary) {
|
||||
// Save to library first, then return the saved step
|
||||
const savedStep = await stepsApi.create(data)
|
||||
onInsertStep(savedStep)
|
||||
} else {
|
||||
// Return as draft (not saved to library)
|
||||
const draft: CustomStepDraft = {
|
||||
title: data.title,
|
||||
step_type: data.step_type,
|
||||
content: data.content,
|
||||
category_id: data.category_id,
|
||||
tags: data.tags
|
||||
}
|
||||
onInsertStep(draft)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create step:', err)
|
||||
setError('Failed to create step. Please try again.')
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBrowserInsert = (step: Step) => {
|
||||
onInsertStep(step)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="relative flex h-[90vh] w-full max-w-4xl flex-col rounded-lg border border-border bg-card shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border p-4">
|
||||
<h2 className="text-lg font-semibold">Add Custom Step</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-md p-1 hover:bg-accent"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-border">
|
||||
<button
|
||||
onClick={() => setActiveTab('create')}
|
||||
className={cn(
|
||||
'flex-1 px-4 py-3 text-sm font-medium transition-colors',
|
||||
activeTab === 'create'
|
||||
? 'border-b-2 border-primary bg-primary/5 text-primary'
|
||||
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
Type My Own
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('browse')}
|
||||
className={cn(
|
||||
'flex-1 px-4 py-3 text-sm font-medium transition-colors',
|
||||
activeTab === 'browse'
|
||||
? 'border-b-2 border-primary bg-primary/5 text-primary'
|
||||
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
Browse Library
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mx-4 mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{activeTab === 'create' ? (
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
<StepForm
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<StepLibraryBrowser
|
||||
onInsert={handleBrowserInsert}
|
||||
showCreateButton={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Loading Overlay */}
|
||||
{isSubmitting && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<p className="text-sm text-muted-foreground">Creating step...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user