docs: refine Phase 2 script generator spec (rounds 5-8 fixes)
- Fix canGenerate/disabled prop propagation: ScriptParameterForm passes
disabled={!canGenerate} to ScriptParameterField (was inconsistent)
- Clarify validate() only checks required fields; client-side pattern/
length/range validation deferred to backend
- Fix generate/download button disabled logic: OR of isGenerating and
!canGenerate
- Clarify manual vs shared error rendering split for select/boolean
- Add disabled prop to select and checkbox element examples
- Add parameters_schema cast note in ScriptParameterForm description
- Add loadCategories() prerequisite note on loadTemplates()
- Add exactly four asterisks clarification for sensitive param masking
- Document default_values and validation_rules as unused Phase 2 fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -73,14 +73,14 @@ interface ScriptGeneratorState {
|
||||
|
||||
- `setCategory` updates `activeCategoryId`, then calls `loadTemplates()`
|
||||
- `setSearch` updates `searchQuery`, then calls `loadTemplates()`. The component (not the store) debounces its call to `setSearch` — `setSearch` always calls `loadTemplates()` immediately on invocation
|
||||
- `loadTemplates()` resolves the slug from the `categories` array (`categories.find(c => c.id === activeCategoryId)?.slug`) before sending `{ category_slug, search }` to the API. When `activeCategoryId` is `null` ("All"), `category_slug` is omitted from the request
|
||||
- `loadTemplates()` resolves the slug from the `categories` array (`categories.find(c => c.id === activeCategoryId)?.slug`) before sending `{ category_slug, search }` to the API. When `activeCategoryId` is `null` ("All"), `category_slug` is omitted from the request. Prerequisite: `loadCategories()` must complete before `loadTemplates()` or `setCategory()` can resolve slugs — the page bootstrap calls them in this order
|
||||
- `selectTemplate(id)` workflow:
|
||||
1. Sets `isLoadingDetail: true` (does NOT touch `isLoadingTemplates`)
|
||||
2. Fetches `ScriptTemplateDetail` from the API
|
||||
3. In a single `set()` call: sets `selectedTemplate`, populates `paramValues` by converting each `parameter.default` to string (`null` → `''`, `true` → `'true'`, `false` → `'false'`, numbers → `String(n)`), clears `formErrors`, `generatedScript`, `generationId`, `generationWarnings`, `generateError`, sets `isLoadingDetail: false`
|
||||
4. The template list remains fully visible and interactive throughout
|
||||
- `reset()` clears exactly: `paramValues`, `formErrors`, `generatedScript`, `generationId`, `generationWarnings`, `generateError`. Does NOT clear `selectedTemplate`, `categories`, `templates`, or any browsing state. Not called by `selectTemplate()` — that action handles its own inline clear. Exposed as a public store action for Phase 3 callers
|
||||
- `validate()`: if `selectedTemplate` is `null`, returns `true` immediately (nothing to validate). Otherwise iterates `selectedTemplate.parameters_schema.parameters`, checks `required && !paramValues[key]` for each, writes errors to `formErrors` via `set()`, returns `false` if any required param is missing, `true` otherwise
|
||||
- `validate()`: if `selectedTemplate` is `null`, returns `true` immediately (nothing to validate). Otherwise iterates `(selectedTemplate.parameters_schema as ScriptParametersSchema).parameters` (cast required — backend types `parameters_schema` as `dict`; see `ScriptTemplateDetail` type comment), checks `required && !paramValues[key]` for each, writes errors to `formErrors` via `set()`, returns `false` if any required param is missing, `true` otherwise. Client-side validation of `ScriptParameterValidation` fields (`pattern`, `min_length`, `max_length`, `min_value`, `max_value`) is NOT implemented in Phase 2 — only required-field presence is checked. The backend validates these constraints on `POST /scripts/generate` and returns errors in `detail`. Phase 2 does not render per-field errors from the backend — all backend validation errors surface as a single `generateError` below the action bar
|
||||
- `generate(sessionId?)`: if `selectedTemplate` is `null`, returns immediately (no-op). Otherwise calls `validate()` first — if it returns `false`, stops (errors are already in store). If valid, sets `isGenerating: true`, clears `generateError`, calls `POST /scripts/generate`. On success: sets `generatedScript`/`generationId`/`generationWarnings`, sets `isGenerating: false`. On error: extracts `error.response?.data?.detail` (FastAPI detail string) or falls back to `'Failed to generate script'`, sets `generateError`, sets `isGenerating: false`
|
||||
- On generate success: `generatedScript` = `response.script`, `generationId` = `response.id`, `generationWarnings` = `response.warnings`
|
||||
|
||||
@@ -158,8 +158,8 @@ export interface ScriptTemplateDetail extends ScriptTemplateListItem {
|
||||
// return ((detail.parameters_schema as ScriptParametersSchema)?.parameters ?? [])
|
||||
// }
|
||||
parameters_schema: ScriptParametersSchema;
|
||||
default_values: Record<string, unknown>;
|
||||
validation_rules: Record<string, unknown>;
|
||||
default_values: Record<string, unknown>; // template-level metadata from backend; not used by Phase 2 UI
|
||||
validation_rules: Record<string, unknown>; // template-level metadata from backend; not used by Phase 2 UI
|
||||
version: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -279,37 +279,39 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
|
||||
- Visual order within the panel (top to bottom): warnings callout → `ScriptPreview` → action bar → error text
|
||||
- `generationWarnings` shown as amber-400 callout above the preview when `generationWarnings.length > 0`
|
||||
- Action bar (below preview):
|
||||
- Generate button (`bg-gradient-brand`) — calls `generate()` with no arguments (Phase 3 will pass `sessionId`); shows spinner + disabled while `isGenerating`
|
||||
- Download `.ps1` button — disabled when `generatedScript` is null; on click triggers `new Blob([generatedScript], { type: 'text/plain' })` → programmatic anchor click with `download="${selectedTemplate.slug}.ps1"`
|
||||
- Generate button (`bg-gradient-brand`) — calls `generate()` with no arguments (Phase 3 will pass `sessionId`); disabled when `isGenerating` OR `!canGenerate`; shows spinner while `isGenerating`
|
||||
- Download `.ps1` button — disabled when `generatedScript` is null OR `!canGenerate`; on click triggers `new Blob([generatedScript], { type: 'text/plain' })` → programmatic anchor click with `download="${selectedTemplate.slug}.ps1"`
|
||||
- `generateError` shown as rose-500 text inline below action bar
|
||||
- Permission check: call `usePermissions()` directly in this component; derive `canGenerate = !isViewer`. Pass `canGenerate` as a prop down to `ScriptParameterForm`. Generate and Download buttons disabled with tooltip "Engineer access required" when `!canGenerate`
|
||||
- Permission check: call `usePermissions()` directly in this component; derive `canGenerate = isEngineer`. Pass `canGenerate` as a prop down to `ScriptParameterForm`. Generate and Download buttons disabled with tooltip "Engineer access required" when `!canGenerate`
|
||||
|
||||
**`ScriptParameterForm`**
|
||||
|
||||
- Accepts `canGenerate: boolean` prop from `ScriptGeneratorPanel`
|
||||
- Iterates `selectedTemplate.parameters_schema.parameters` sorted by `order`
|
||||
- Iterates `selectedTemplate.parameters_schema.parameters` sorted by `order`. Access via cast: `(selectedTemplate.parameters_schema as ScriptParametersSchema).parameters` — `parameters_schema` arrives as `dict` at runtime (backend types it as `dict`; helper comment in `ScriptTemplateDetail` type definition)
|
||||
- Groups by `group` field: renders a `font-label uppercase text-muted-foreground` section label before each group boundary (parameters with `group: null` rendered ungrouped at the top)
|
||||
- Renders a `ScriptParameterField` per parameter, passing `canGenerate` down
|
||||
- Renders a `ScriptParameterField` per parameter, passing `disabled={!canGenerate}` (converts `canGenerate` boolean to the `disabled` prop expected by `ScriptParameterField`)
|
||||
- Does NOT own the Generate button and does NOT call `generate()` or `validate()` directly — validation is triggered from `ScriptGeneratorPanel` via the store's `generate()` action (which calls `validate()` internally)
|
||||
|
||||
**`ScriptParameterField`**
|
||||
|
||||
- Accepts `param: ScriptParameter`, `value: string`, `error: string | undefined`, `disabled: boolean`
|
||||
- Renders input by `type`. Pass `error` prop to shared components (they render their own error message — do NOT add a separate error `<p>` below):
|
||||
- `text` → `<Input error={error} />`
|
||||
- `password` → `<Input type="password" error={error} />` with Lucide `Eye`/`EyeOff` toggle
|
||||
- `textarea` → `<Textarea error={error} />`
|
||||
- `number` → `<Input type="number" error={error} />`
|
||||
- `multi_text` → `<Input placeholder="Comma-separated values" error={error} />`; stored as single string, backend splits on comma
|
||||
- `select` → native `<select className="w-full rounded-[10px] border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(6,182,212,0.3)]">`; options from `parameter.options`; render error manually as `<p className="mt-1.5 text-xs text-red-400">{error}</p>` (no shared component for select)
|
||||
- `boolean` → `<input type="checkbox" className="rounded border-border" checked={value === 'true'} onChange={e => setParamValue(key, e.target.checked ? 'true' : 'false')} />`; render error manually as `<p className="mt-1.5 text-xs text-red-400">{error}</p>`
|
||||
- Renders input by `type`. Two error rendering approaches depending on type:
|
||||
- **Shared error rendering** — pass `error` prop to the shared component; it renders its own error message (do NOT add a separate `<p>` below):
|
||||
- `text` → `<Input error={error} />`
|
||||
- `password` → `<Input type="password" error={error} />` with Lucide `Eye`/`EyeOff` toggle
|
||||
- `textarea` → `<Textarea error={error} />`
|
||||
- `number` → `<Input type="number" error={error} />`
|
||||
- `multi_text` → `<Input placeholder="Comma-separated values" error={error} />`; stored as single string, backend splits on comma
|
||||
- **Manual error rendering** — no shared component; render error explicitly as `<p className="mt-1.5 text-xs text-red-400">{error}</p>` below the input:
|
||||
- `select` → native `<select className="w-full rounded-[10px] border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(6,182,212,0.3)]" disabled={disabled}>`; options from `parameter.options`
|
||||
- `boolean` → `<input type="checkbox" className="rounded border-border" checked={value === 'true'} onChange={e => setParamValue(key, e.target.checked ? 'true' : 'false')} disabled={disabled} />`
|
||||
- Shows `help_text` as `<p className="text-xs text-muted-foreground mt-1">` below the input (before the error)
|
||||
- Calls `setParamValue(key, value)` on every change event; boolean uses `e.target.checked ? 'true' : 'false'`
|
||||
|
||||
**`ScriptPreview`**
|
||||
|
||||
- Two modes determined by `generatedScript` in store:
|
||||
- **Draft mode** (`generatedScript === null`): client-side `{{key}}` substitution in `selectedTemplate.script_body`. Sensitive params (`parameter.sensitive === true`) rendered as `****` regardless of `paramValues` value. Unfilled non-sensitive params left as `{{key}}` for `PowerShellHighlighter` to color amber
|
||||
- **Draft mode** (`generatedScript === null`): client-side `{{key}}` substitution in `selectedTemplate.script_body`. Sensitive params (`parameter.sensitive === true`) rendered as exactly four asterisks `****` regardless of `paramValues` value. Unfilled non-sensitive params left as `{{key}}` for `PowerShellHighlighter` to color amber
|
||||
- **Generated mode** (`generatedScript !== null`): shows `generatedScript`
|
||||
- The component computes `displayScript` as a local variable (the substituted preview string in draft mode, or `generatedScript` in generated mode). Both the render and the copy handler reference this same local variable — no separate store field needed
|
||||
- Copy icon (Lucide `Copy`, 14px) in top-right corner of code block, visible in both modes:
|
||||
|
||||
Reference in New Issue
Block a user