import { useState, useEffect, useRef } from 'react' import { ArrowLeft, Loader2, Save, Scan, Trash2 } from 'lucide-react' import { Input } from '@/components/ui/Input' import { Textarea } from '@/components/ui/Textarea' import { usePermissions } from '@/hooks/usePermissions' import { scriptsApi } from '@/api' import { ScriptBodyEditor } from './ScriptBodyEditor' import { ParameterSchemaBuilder } from './ParameterSchemaBuilder' import { detectParameterCandidates } from '@/lib/scriptParameterDetector' import { ParameterDetectorStepper } from './ParameterDetectorStepper' import type { ScriptTemplateDetail, ScriptCategoryResponse, ScriptParametersSchema, ScriptTemplateCreateRequest, ScriptTemplateUpdateRequest, ParameterCandidate, ScriptParameter, } from '@/types' interface Props { templateId: string | null // null = create mode onBack: () => void onSaved: () => void } interface FormState { name: string description: string use_case: string category_id: string complexity: 'beginner' | 'intermediate' | 'advanced' tags: string estimated_runtime: string requires_elevation: boolean requires_modules: string script_body: string parameters_schema: ScriptParametersSchema } const EMPTY_FORM: FormState = { name: '', description: '', use_case: '', category_id: '', complexity: 'beginner', tags: '', estimated_runtime: '', requires_elevation: false, requires_modules: '', script_body: '', parameters_schema: { parameters: [] }, } export function ScriptTemplateEditor({ templateId, onBack, onSaved }: Props) { const [form, setForm] = useState(EMPTY_FORM) const [categories, setCategories] = useState([]) const [isLoading, setIsLoading] = useState(!!templateId) const [isSaving, setIsSaving] = useState(false) const [saveError, setSaveError] = useState(null) const [isDirty, setIsDirty] = useState(false) const [deleteConfirm, setDeleteConfirm] = useState(false) const [template, setTemplate] = useState(null) const [detectedCandidates, setDetectedCandidates] = useState([]) const [showStepper, setShowStepper] = useState(false) const [detectionSummary, setDetectionSummary] = useState(null) const acceptingCandidateRef = useRef(false) const { canShareScriptTemplate } = usePermissions() // Dismiss stepper if user manually edits the script body during detection // (but NOT when handleAcceptCandidate programmatically updates script_body) const scriptBodyRef = form.script_body useEffect(() => { if (showStepper && !acceptingCandidateRef.current) { setShowStepper(false) setDetectedCandidates([]) } acceptingCandidateRef.current = false // eslint-disable-next-line react-hooks/exhaustive-deps }, [scriptBodyRef]) // Load categories + template detail (if editing) useEffect(() => { const load = async () => { try { const cats = await scriptsApi.getCategories() setCategories(cats) if (templateId) { const detail = await scriptsApi.getTemplateDetail(templateId) setTemplate(detail) const schema = detail.parameters_schema as ScriptParametersSchema setForm({ name: detail.name, description: detail.description ?? '', use_case: detail.use_case ?? '', category_id: detail.category_id, complexity: detail.complexity, tags: detail.tags.join(', '), estimated_runtime: detail.estimated_runtime ?? '', requires_elevation: detail.requires_elevation, requires_modules: detail.requires_modules.join(', '), script_body: detail.script_body, parameters_schema: schema ?? { parameters: [] }, }) } else if (cats.length > 0) { setForm(f => ({ ...f, category_id: cats[0].id })) } } catch { setSaveError('Failed to load data') } finally { setIsLoading(false) } } load() }, [templateId]) const updateField = (key: K, value: FormState[K]) => { setForm(f => ({ ...f, [key]: value })) setIsDirty(true) } const handleSave = async () => { if (!form.name.trim()) { setSaveError('Name is required') return } if (!form.script_body.trim()) { setSaveError('Script body is required') return } if (!form.category_id) { setSaveError('Category is required') return } setIsSaving(true) setSaveError(null) const tags = form.tags.split(',').map(t => t.trim()).filter(Boolean) const requires_modules = form.requires_modules.split(',').map(m => m.trim()).filter(Boolean) try { if (templateId) { const data: ScriptTemplateUpdateRequest = { name: form.name, description: form.description || null, use_case: form.use_case || null, script_body: form.script_body, parameters_schema: form.parameters_schema as unknown as ScriptParametersSchema, tags, complexity: form.complexity, estimated_runtime: form.estimated_runtime || null, requires_elevation: form.requires_elevation, requires_modules, } await scriptsApi.updateTemplate(templateId, data) } else { const data: ScriptTemplateCreateRequest = { category_id: form.category_id, name: form.name, description: form.description || null, use_case: form.use_case || null, script_body: form.script_body, parameters_schema: form.parameters_schema as unknown as ScriptParametersSchema, tags, complexity: form.complexity, estimated_runtime: form.estimated_runtime || null, requires_elevation: form.requires_elevation, requires_modules, } await scriptsApi.createTemplate(data) } setIsDirty(false) onSaved() } catch (err: unknown) { const axiosErr = err as { response?: { data?: { detail?: string } } } setSaveError(axiosErr.response?.data?.detail ?? 'Failed to save template') } finally { setIsSaving(false) } } const handleDelete = async () => { if (!templateId) return try { await scriptsApi.deleteTemplate(templateId) onSaved() } catch { setSaveError('Failed to delete template') } } const handleShare = async (shared: boolean) => { if (!templateId) return try { const updated = await scriptsApi.shareTemplate(templateId, shared) setTemplate(updated) } catch { setSaveError('Failed to update sharing') } } const handleBack = () => { if (isDirty && !confirm('You have unsaved changes. Leave anyway?')) return onBack() } const handleDetectParameters = () => { const candidates = detectParameterCandidates(form.script_body) if (candidates.length === 0) { setDetectionSummary('No parameter candidates detected in the script body.') setShowStepper(false) setTimeout(() => setDetectionSummary(null), 4000) return } setDetectedCandidates(candidates) setDetectionSummary(null) setShowStepper(true) } const handleAcceptCandidate = ( candidate: ParameterCandidate, overrides: { key: string label: string type: ScriptParameter['type'] sensitive: boolean required: boolean defaultValue: string | boolean | number | null } ) => { let updatedScript = form.script_body if (candidate.source === 'param_block') { const defaultMatch = candidate.matchedLine.match(/=\s*(.+?)(?:\s*,?\s*$)/) if (defaultMatch) { updatedScript = updatedScript.replace( candidate.matchedLine, candidate.matchedLine.replace(defaultMatch[1], `'{{${overrides.key}}}'`) ) } } else { const assignMatch = candidate.matchedLine.match(/=\s*(.+)$/) if (assignMatch) { updatedScript = updatedScript.replace( candidate.matchedLine, candidate.matchedLine.replace(assignMatch[1], `'{{${overrides.key}}}'`) ) } } const existingParams = form.parameters_schema.parameters const newParam: ScriptParameter = { key: overrides.key, label: overrides.label, type: overrides.type, required: overrides.required, placeholder: null, group: null, order: existingParams.length + 1, help_text: null, options: null, default: overrides.defaultValue, validation: null, sensitive: overrides.sensitive, } acceptingCandidateRef.current = true setForm(f => ({ ...f, script_body: updatedScript, parameters_schema: { parameters: [...f.parameters_schema.parameters, newParam], }, })) setIsDirty(true) } const handleSkipCandidate = () => { // Nothing to do — stepper advances internally } const handleDetectionFinish = (acceptedCount: number, totalCount: number) => { setShowStepper(false) setDetectedCandidates([]) setDetectionSummary( acceptedCount === 0 ? 'No parameters were added.' : `Added ${acceptedCount} of ${totalCount} detected parameter${totalCount !== 1 ? 's' : ''}.` ) setTimeout(() => setDetectionSummary(null), 5000) } if (isLoading) { return (
) } return (
{/* Back link */}

{templateId ? 'Edit Template' : 'New Template'}

{/* ── Metadata ──────────────────────────────────────────────── */}

Metadata

updateField('name', e.target.value)} placeholder="e.g. Create AD User" />