Files
resolutionflow/frontend/src/components/script-editor/ParameterSchemaBuilder.tsx
chihlasm d4dbf44781 feat: Script Generator Phase 1+2 — backend, engine, API, frontend, template editor, parameter detector
Complete Script Generator feature including:

Backend:
- ScriptCategory, ScriptTemplate, ScriptGeneration models
- ScriptTemplateEngine with substitution, filters, sanitization
- CRUD + share API endpoints with permission checks
- Integration tests for permissions and sharing
- Migration 057 with AD User Management seed templates

Frontend — Script Library:
- Browse templates with category tabs and search
- Configure pane with parameter form and script generation
- Script preview with live substitution and copy/download
- scriptGeneratorStore Zustand store

Frontend — Template Editor:
- Full CRUD form with metadata, script body (Monaco Editor), parameters
- ParameterSchemaBuilder with visual builder + JSON toggle
- ScriptManagePage with routing and nav link

Frontend — Parameter Detector:
- Client-side PowerShell parameter detection engine
- Detects script-level param() blocks and variable assignments
- Type inference from PS type annotations and value patterns
- ParameterDetectorStepper one-by-one review UI with accept/skip

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 20:18:59 -04:00

173 lines
5.3 KiB
TypeScript

import { useState } from 'react'
import { Plus, Code, List } from 'lucide-react'
import { cn } from '@/lib/utils'
import { ParameterCard } from './ParameterCard'
import type { ScriptParameter, ScriptParametersSchema } from '@/types'
interface Props {
schema: ScriptParametersSchema
onChange: (schema: ScriptParametersSchema) => void
disabled?: boolean
}
function newParameter(order: number): ScriptParameter {
return {
key: '',
label: '',
type: 'text',
required: true,
placeholder: null,
group: null,
order,
help_text: null,
options: null,
default: null,
validation: null,
sensitive: false,
}
}
export function ParameterSchemaBuilder({ schema, onChange, disabled }: Props) {
const [mode, setMode] = useState<'visual' | 'json'>('visual')
const [jsonText, setJsonText] = useState('')
const [jsonError, setJsonError] = useState<string | null>(null)
const parameters = schema.parameters ?? []
const updateParams = (params: ScriptParameter[]) => {
onChange({ parameters: params })
}
const handleParamChange = (index: number, updated: ScriptParameter) => {
const next = [...parameters]
next[index] = updated
updateParams(next)
}
const handleRemove = (index: number) => {
updateParams(parameters.filter((_, i) => i !== index))
}
const handleMoveUp = (index: number) => {
if (index === 0) return
const next = [...parameters]
;[next[index - 1], next[index]] = [next[index], next[index - 1]]
next.forEach((p, i) => { p.order = i + 1 })
updateParams(next)
}
const handleMoveDown = (index: number) => {
if (index === parameters.length - 1) return
const next = [...parameters]
;[next[index], next[index + 1]] = [next[index + 1], next[index]]
next.forEach((p, i) => { p.order = i + 1 })
updateParams(next)
}
const handleAdd = () => {
updateParams([...parameters, newParameter(parameters.length + 1)])
}
const switchToJson = () => {
setJsonText(JSON.stringify(schema, null, 2))
setJsonError(null)
setMode('json')
}
const switchToVisual = () => {
try {
const parsed = JSON.parse(jsonText)
if (!parsed.parameters || !Array.isArray(parsed.parameters)) {
setJsonError('JSON must have a "parameters" array')
return
}
onChange(parsed as ScriptParametersSchema)
setJsonError(null)
setMode('visual')
} catch (e) {
setJsonError(`Invalid JSON: ${(e as Error).message}`)
}
}
return (
<div className="flex flex-col gap-3">
{/* Mode toggle */}
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => mode === 'json' ? switchToVisual() : undefined}
className={cn(
'flex items-center gap-1.5 font-label text-xs px-3 py-1.5 rounded-full border transition-all',
mode === 'visual'
? 'bg-primary/10 border-primary/30 text-foreground'
: 'border-border text-muted-foreground hover:text-foreground'
)}
>
<List size={12} /> Visual
</button>
<button
type="button"
onClick={() => mode === 'visual' ? switchToJson() : undefined}
className={cn(
'flex items-center gap-1.5 font-label text-xs px-3 py-1.5 rounded-full border transition-all',
mode === 'json'
? 'bg-primary/10 border-primary/30 text-foreground'
: 'border-border text-muted-foreground hover:text-foreground'
)}
>
<Code size={12} /> JSON
</button>
</div>
{mode === 'visual' ? (
<>
{parameters.length === 0 ? (
<p className="text-sm text-muted-foreground py-4 text-center">
No parameters defined. Add one to create dynamic form fields.
</p>
) : (
<div className="flex flex-col gap-2">
{parameters.map((param, i) => (
<ParameterCard
key={i}
param={param}
index={i}
onChange={handleParamChange}
onRemove={handleRemove}
onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown}
isFirst={i === 0}
isLast={i === parameters.length - 1}
disabled={disabled}
/>
))}
</div>
)}
<button
type="button"
onClick={handleAdd}
disabled={disabled}
className="flex items-center gap-1.5 text-sm text-primary hover:underline self-start"
>
<Plus size={14} /> Add Parameter
</button>
</>
) : (
<>
<textarea
value={jsonText}
onChange={e => { setJsonText(e.target.value); setJsonError(null) }}
disabled={disabled}
spellCheck={false}
className="w-full min-h-[300px] resize-y font-label text-sm bg-card border border-border rounded-xl p-4 text-foreground focus:outline-none focus:border-[rgba(6,182,212,0.3)] disabled:cursor-not-allowed disabled:opacity-50"
placeholder='{ "parameters": [...] }'
/>
{jsonError && (
<p className="text-xs text-rose-500">{jsonError}</p>
)}
</>
)}
</div>
)
}