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>
173 lines
5.3 KiB
TypeScript
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>
|
|
)
|
|
}
|