docs: final spec fixes — resolve inputValue ownership, spinner mode, clear-search wiring
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -249,9 +249,9 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
|
||||
|
||||
- Renders an "All" tab first (calls `setCategory(null)`, active when `activeCategoryId === null`), followed by one tab per category
|
||||
- Category tabs: pill style, `bg-primary/10` + left 3px cyan accent bar on active tab
|
||||
- Search: local `inputValue` state controls the `<Input>` value (NOT `searchQuery` from store — avoids input lag during debounce). `useEffect` watches `inputValue`, schedules `setTimeout(() => setSearch(inputValue), 300)`, clears timeout on cleanup
|
||||
- Reads `categories`, `activeCategoryId` from store; does NOT use `searchQuery` from store to control the input
|
||||
- Exposes a `onClearSearch` callback (or reads a `clearSearch` prop) — see `ScriptTemplateList` below for why. Simplest implementation: `ScriptLibraryPage` passes a `clearSearch` callback to both components; `ScriptFilterBar` exposes it as a function ref or the page wires `setInputValue` via a `useRef`
|
||||
- Receives `inputValue: string` and `setInputValue: (v: string) => void` as props from `ScriptLibraryPage` (state is lifted — `ScriptFilterBar` does NOT own local search state)
|
||||
- `<Input>` is controlled by `inputValue` prop. `useEffect` inside `ScriptFilterBar` watches `inputValue`, schedules `setTimeout(() => setSearch(inputValue), 300)`, clears timeout on cleanup — debounce lives here but the value lives in the page
|
||||
- Reads `categories`, `activeCategoryId` from store
|
||||
|
||||
**`ScriptTemplateList`**
|
||||
|
||||
@@ -260,7 +260,7 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
|
||||
- Accepts `inputValue: string` and `onClearSearch: () => void` props from `ScriptLibraryPage`
|
||||
- Shows 3 skeleton placeholder cards while `isLoadingTemplates` is true
|
||||
- Shows "No templates found" empty state when `templates.length === 0` and `!isLoadingTemplates` and `inputValue === ''`
|
||||
- Shows "No templates match your search" + "Clear search" button when `templates.length === 0` and `!isLoadingTemplates` and `inputValue !== ''`. "Clear search" calls `onClearSearch()` which resets both `inputValue` in `ScriptFilterBar` and `searchQuery` in the store. Using `inputValue` (not `store.searchQuery`) avoids the 300ms debounce lag in empty-state detection
|
||||
- Shows "No templates match your search" + "Clear search" button when `templates.length === 0` and `!isLoadingTemplates` and `inputValue !== ''`. "Clear search" calls `onClearSearch()` — a callback prop from `ScriptLibraryPage` defined as `() => { setInputValue(''); store.setSearch(''); }`. Using `inputValue` (not `store.searchQuery`) avoids the 300ms debounce lag in empty-state detection
|
||||
|
||||
**`TemplateCard`**
|
||||
|
||||
@@ -274,13 +274,14 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
|
||||
**`ScriptGeneratorPanel`**
|
||||
|
||||
- Shows placeholder (Terminal icon + "Select a template to get started") when `selectedTemplate` is null
|
||||
- Shows spinner overlay when `isLoadingDetail` is true (template list remains visible behind it)
|
||||
- When template selected and `!isLoadingDetail`: renders template name/description header, `ScriptParameterForm`, `ScriptPreview`, action bar
|
||||
- Action bar (visible when template selected):
|
||||
- Generate button (`bg-gradient-brand`) — calls `generate()`; shows spinner + disabled while `isGenerating`
|
||||
- Shows a full-panel centered spinner when `isLoadingDetail` is true, replacing the panel content (not an overlay — no prior template content shown while loading). The template list column remains fully visible and interactive
|
||||
- When template selected and `!isLoadingDetail`: renders template name/description header, `ScriptParameterForm`, `ScriptPreview`, action bar (in that order, top to bottom)
|
||||
- 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"`
|
||||
- `generateError` shown as rose-500 text inline below action bar
|
||||
- `generationWarnings` shown as amber-400 callout above the preview when `generationWarnings.length > 0`
|
||||
- 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`
|
||||
|
||||
**`ScriptParameterForm`**
|
||||
@@ -310,9 +311,10 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
|
||||
- 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
|
||||
- **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:
|
||||
- On click: copies the currently displayed script string (preview in draft mode, generated script in generated mode) via `navigator.clipboard.writeText()`. On success: shows "Copied!" for 2s then resets. On failure: silently fails — no error displayed, icon does not change state
|
||||
- Passes current script string to `PowerShellHighlighter`
|
||||
- On click: calls `navigator.clipboard.writeText(displayScript)`. On success: shows "Copied!" for 2s then resets. On failure: silently fails — no error displayed, icon does not change state
|
||||
- Passes `displayScript` to `PowerShellHighlighter`
|
||||
|
||||
**`PowerShellHighlighter`**
|
||||
|
||||
@@ -330,6 +332,8 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
|
||||
7. Keywords: /\b(if|else|elseif|foreach|for|while|function|return|try|catch|finally|param|switch)\b/
|
||||
```
|
||||
|
||||
Note: variables (priority 4) consume tokens like `$foreach` before keywords (priority 7) can match — this is intentional. Do not reorder the alternation without reviewing this interaction.
|
||||
|
||||
Token colors:
|
||||
- Comments → `text-[#8b949e]`
|
||||
- String literals → `text-[#a5d6ff]`
|
||||
@@ -358,9 +362,9 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx
|
||||
| ------ | ------------ |
|
||||
| View Script Library page | Any authenticated user |
|
||||
| Browse templates, see draft preview | Any authenticated user |
|
||||
| Fill form, generate, copy, download | Engineer, owner, or super_admin (i.e. `!isViewer`) |
|
||||
| Fill form, generate, copy, download | Engineer, owner, or super_admin |
|
||||
|
||||
`usePermissions()` is called once in `ScriptGeneratorPanel`. Derive `canGenerate = isEngineer` (which returns `true` for engineer, owner, and super_admin via the role hierarchy — matching "Engineer or above" intent more explicitly than `!isViewer`). Pass `canGenerate` as a prop down to `ScriptParameterForm`. Leaf components (`ScriptParameterField`) receive `disabled` as a prop — they do not call `usePermissions()` directly.
|
||||
`usePermissions()` is called once in `ScriptGeneratorPanel`. Derive `canGenerate = isEngineer` (`usePermissions().isEngineer` returns `true` for engineer, owner, and super_admin via the role hierarchy check — equivalent to `!isViewer` given the four-role system, but `isEngineer` is more semantically explicit). Pass `canGenerate` as a prop down to `ScriptParameterForm`. Leaf components (`ScriptParameterField`) receive `disabled` as a prop — they do not call `usePermissions()` directly.
|
||||
|
||||
Viewer-blocking is frontend-only in Phase 2 (backend role guard deferred — known limitation, documented in Architecture section).
|
||||
|
||||
@@ -374,7 +378,7 @@ Viewer-blocking is frontend-only in Phase 2 (backend role guard deferred — kno
|
||||
- 300ms debounce in `ScriptFilterBar`: local `inputValue` state, `useEffect` + `setTimeout`/`clearTimeout`
|
||||
- `setSearch` is called once after the debounce delay; it immediately calls `loadTemplates()`
|
||||
- Category filter and search compose: `loadTemplates()` sends both `category_slug` and `search` simultaneously
|
||||
- `inputValue` is lifted to `ScriptLibraryPage` (as state) and passed as a prop to both `ScriptFilterBar` (controls the input) and `ScriptTemplateList` (drives empty-state variant). `onClearSearch` callback (also from `ScriptLibraryPage`) resets `inputValue` to `''` and calls `setSearch('')`
|
||||
- `inputValue` is owned by `ScriptLibraryPage` (lifted state). `ScriptLibraryPage` passes it as a prop to `ScriptFilterBar` (which controls the `<Input>`) and to `ScriptTemplateList` (which uses it to choose the correct empty-state variant). `onClearSearch` — also defined in `ScriptLibraryPage` as `() => { setInputValue(''); store.setSearch(''); }` — is passed to `ScriptTemplateList` for the "Clear search" button
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user