feat(apollo-react): add LockableValueField HITL prototype#893
feat(apollo-react): add LockableValueField HITL prototype#893dbacomputer wants to merge 8 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Dependency License Review
License distribution
Excluded packages
|
There was a problem hiding this comment.
Pull request overview
Adds a HITL-oriented LockableValueField prototype to the NodePropertyPanel Storybook surface, demonstrating quick-form editing patterns (lock/read-only, fixed vs expression, AI assist affordance) and a draggable list of fields.
Changes:
- Introduces
LockableValueField(lock toggle + value-type switcher + AI/insert affordances) built on@uipath/apollo-wind’sInputGroup. - Adds a new “Lockable Field” Storybook story that renders a Quick Approve-style panel, including a draggable/reorderable list of lockable fields.
- Exports
LockableValueFieldand its types from the NodePropertyPanel barrel.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/apollo-react/src/canvas/components/NodePropertyPanel/NodePropertyPanel.stories.tsx | Adds the “Lockable Field” prototype story, including DnD-based field reordering and panel UI. |
| packages/apollo-react/src/canvas/components/NodePropertyPanel/LockableValueField.tsx | Adds the new LockableValueField component (lock + mode switch + AI/insert affordances) using apollo-wind primitives. |
| packages/apollo-react/src/canvas/components/NodePropertyPanel/index.ts | Re-exports LockableValueField and related types from the NodePropertyPanel module. |
| <InputGroupInput | ||
| id={id} | ||
| readOnly={locked} | ||
| value={value} | ||
| onChange={(e) => onValueChange?.(e.target.value)} | ||
| placeholder={FIELD_PLACEHOLDER[mode]} | ||
| className={mode === 'expression' ? 'font-mono' : undefined} | ||
| /> |
| export function LockableValueField({ | ||
| value = '', | ||
| onValueChange, | ||
| locked = true, | ||
| onLockedChange, |
| > | ||
| <Plus size={12} /> | ||
| Add case | ||
| </button> |
📊 Coverage + size by packagePer-package coverage and bundle size on this PR. New-line coverage = of the source lines this PR adds or changes, the % hit by tests.
"Coverage" is each package's own |
| /* Storybook (apps/storybook/.storybook/preview.tsx) imports this file | ||
| * directly, making it the Tailwind entry point for the whole app — including | ||
| * apollo-react's canvas stories. Without this, utility classes unique to | ||
| * apollo-react (not already used somewhere in apollo-wind) silently produce | ||
| * no CSS, since they'd never be scanned otherwise. */ |
| * apollo-react's canvas stories. Without this, utility classes unique to | ||
| * apollo-react (not already used somewhere in apollo-wind) silently produce | ||
| * no CSS, since they'd never be scanned otherwise. */ | ||
| @source "../../../apollo-react/src/**/*.{js,jsx,ts,tsx}"; No newline at end of file |
| {label ?? ( | ||
| <Label htmlFor={id} className="text-xs font-medium text-foreground-muted"> | ||
| {FIELD_LABEL[mode]} | ||
| {required && <span className="ml-0.5 text-destructive">*</span>} | ||
| </Label> |
| {value ? ( | ||
| new Date(value).toLocaleDateString(undefined, { | ||
| year: 'numeric', | ||
| month: 'long', | ||
| day: 'numeric', | ||
| }) | ||
| ) : ( | ||
| <span className="text-muted-foreground">Pick a date</span> | ||
| )} |
| id={id} | ||
| checked={value === 'true'} | ||
| onCheckedChange={(checked) => onValueChange?.(String(checked))} | ||
| disabled={locked} | ||
| /> |
| const handleJsonChange = (value: string) => { | ||
| setJsonDraft(value); | ||
| try { | ||
| const parsed = JSON.parse(value); | ||
| if (!Array.isArray(parsed)) { | ||
| setJsonError('Expected a JSON array of fields.'); | ||
| return; | ||
| } | ||
| setCases(parsed); | ||
| setJsonError(null); | ||
| } catch { | ||
| setJsonError('Invalid JSON.'); | ||
| } | ||
| }; |
| export { | ||
| DEMO_SELECT_OPTIONS, | ||
| FIELD_TYPE_META, | ||
| LockableValueField, | ||
| parseListValue, | ||
| } from './LockableValueField'; |
| * the direction a full code-editing surface would take. Select/multiselect | ||
| * options are demo placeholders; file uploads aren't persisted anywhere. | ||
| */ | ||
| export function LockableValueField({ |
| id?: string; | ||
| className?: string; | ||
| } |
Prototype for a Quick Approve panel demonstrating quick-form editing inside the Property Panel experience. - LockableValueField: a string field built on apollo-wind's InputGroup, with a lock/unlock popover (locked = read-only, not disabled) and a Fixed value / Expression type-switcher popover, plus an AI-assist popover (describe + generate) and an Insert-variable affordance. - NodePropertyPanel "Lockable Field" story: a Quick Approve panel with a Delivery channel multi-select, an Assignment criteria search + dropdown, and a Quick form section (UI/JSON toggle) containing a draggable, deletable, re-orderable list of LockableValueField rows seeded with Invoice Number / Submission Date / Approved Amount. Built entirely on existing apollo-wind primitives (InputGroup, DropdownMenu, Popover, MultiSelect, Select, Card, ToggleGroup) and the existing @dnd-kit dependency already used by StageNode, following this file's established prototype conventions (PanelFrame, CasePanel-style row chrome). No production wiring yet — this is a review prototype.
…build Storybook imports apollo-wind's tailwind.css directly as its Tailwind entry point, including for apollo-react's canvas stories, but the @source glob only covered apollo-wind's own files. Utility classes unique to apollo-react (e.g. container-query variants) silently produced no CSS since they were never scanned. Also pins @tailwindcss/vite to an exact version already cleared by the pnpm minimumReleaseAge policy, keeping it aligned with the tailwindcss core version resolved elsewhere in the workspace.
Iterates the HITL "Quick Approve" panel prototype for coworker feedback: - Field-type switching (string, integer, date, boolean, single/multi select, file) with per-type value controls and Fixed/Expression copy. - Responsive header: field-type/AI-assist/insert-variable controls collapse to icon-only in narrow containers, plus a `compact` prop to force that layout for side-by-side comparison. - Real tooltips on the icon-only header controls instead of native title attributes. - Edit/Preview/JSON toggle for the Quick Form card, restyled to borderless square icon buttons matching the compact header controls. - Showcase panel documenting the component with live Full view / Compact view examples.
… HITL prototype Adds a Required/Not-required toggle (full-view switch, compact icon+popover variant), an upload-triggering icon on the demo file card, a type-selector dropdown behind "+ Add field" (including a new Button field type), and Approve/Cancel form buttons editable inline. Replaces the field-reorder insertion-line indicator with a DragOverlay + synced placeholder, mirroring StageNode's existing dnd-kit pattern, since the prior indicator was visually inconsistent during drag. Also fixes apollo-wind's Switch "sm" size to match shadcn/ui's actual Small spec (track 14x24, thumb 12px) and updates its tests accordingly, and fixes a hover-visibility bug where a field's controls would fade out while their own popover/dropdown was still open (Tooltip/Popover asChild data-state collision fixed by keying off aria-expanded instead).
2604a3c to
fe5294c
Compare
| * directly, making it the Tailwind entry point for the whole app — including | ||
| * apollo-react's canvas stories. Without this, utility classes unique to | ||
| * apollo-react (not already used somewhere in apollo-wind) silently produce | ||
| * no CSS, since they'd never be scanned otherwise. */ | ||
| @source "../../../apollo-react/src/**/*.{js,jsx,ts,tsx}"; No newline at end of file |
| const FIELD_LABEL: Record<LockableValueFieldMode, string> = { | ||
| fixed: 'String value', | ||
| expression: 'Write a string expression', | ||
| }; |
| <Label htmlFor={id} className="text-xs font-medium text-foreground-muted"> | ||
| {FIELD_LABEL[mode]} | ||
| {required && <span className="ml-0.5 text-destructive">*</span>} | ||
| </Label> |
| <InputGroupButton | ||
| icon | ||
| size="3xs" | ||
| aria-label={locked ? 'Locked. Click to unlock.' : 'Unlocked. Click to lock.'} | ||
| > |
| <InputGroupButton | ||
| icon | ||
| size="3xs" | ||
| aria-label={locked ? 'Locked. Click to unlock.' : 'Unlocked. Click to lock.'} | ||
| > |
| const parsed = JSON.parse(value); | ||
| if (!Array.isArray(parsed)) { | ||
| setJsonError('Expected a JSON array of fields.'); | ||
| return; | ||
| } | ||
| setCases(parsed); | ||
| setJsonError(null); |
| export { | ||
| DEMO_SELECT_OPTIONS, | ||
| FIELD_TYPE_META, | ||
| LockableValueField, | ||
| parseListValue, | ||
| } from './LockableValueField'; |
| export function LockableValueField({ | ||
| value = '', | ||
| onValueChange, | ||
| locked = true, | ||
| onLockedChange, |
…ew switcher Moves the form-button edit affordance (Type/Delete popover) inside the button pill and makes it expand on hover instead of floating a separate icon; wires the buttons into the Preview view so they actually render there. Reworks the Quick form view row into a standalone "more options" menu (Preview, Describe the form you want) plus a connected UI/JSON segmented toggle, replacing the earlier three-icon row for a cleaner layout. Also drops the header icon on Form HITL Review to free up title space next to the Reject/Approve actions.
| {label ?? ( | ||
| <Label htmlFor={id} className="text-xs font-medium text-foreground-muted"> | ||
| {FIELD_LABEL[mode]} | ||
| {required && <span className="ml-0.5 text-destructive">*</span>} | ||
| </Label> | ||
| )} |
| <InputGroupButton | ||
| icon | ||
| size="3xs" | ||
| aria-label={locked ? 'Locked. Click to unlock.' : 'Unlocked. Click to lock.'} | ||
| > | ||
| {locked ? <Lock /> : <LockOpen />} | ||
| </InputGroupButton> |
| <DropdownMenuTrigger asChild> | ||
| <InputGroupButton icon size="3xs" aria-label="Choose value type"> | ||
| <typeMeta.icon /> | ||
| </InputGroupButton> | ||
| </DropdownMenuTrigger> |
| export { | ||
| DEMO_SELECT_OPTIONS, | ||
| FIELD_TYPE_META, | ||
| LockableValueField, | ||
| parseListValue, | ||
| } from './LockableValueField'; |
| <button | ||
| type="button" | ||
| className="flex h-8 items-center gap-2 rounded-lg bg-brand px-4 text-sm font-semibold text-foreground-on-accent transition hover:bg-brand-hover" | ||
| > | ||
| <CircleCheck size={14} /> | ||
| Approve | ||
| </button> |
| const handleJsonChange = (value: string) => { | ||
| setJsonDraft(value); | ||
| try { | ||
| const parsed = JSON.parse(value); | ||
| if (!Array.isArray(parsed)) { | ||
| setJsonError('Expected a JSON array of fields.'); | ||
| return; | ||
| } | ||
| setCases(parsed); | ||
| setJsonError(null); | ||
| } catch { | ||
| setJsonError('Invalid JSON.'); | ||
| } | ||
| }; |
| /* Storybook (apps/storybook/.storybook/preview.tsx) imports this file | ||
| * directly, making it the Tailwind entry point for the whole app — including | ||
| * apollo-react's canvas stories. Without this, utility classes unique to | ||
| * apollo-react (not already used somewhere in apollo-wind) silently produce | ||
| * no CSS, since they'd never be scanned otherwise. */ | ||
| @source "../../../apollo-react/src/**/*.{js,jsx,ts,tsx}"; No newline at end of file |
… menu Moves the "String value"/"Write a string expression" text to the field's inside placeholder (it was incorrectly placed as the label above the field) so it now shows consistently across all locked field types, matching the LockableValueField showcase used as the source of truth. Simplifies the Add field/Add button popover down to two flat actions (dropping the field-type submenu, since choosing a type after adding wasn't needed), reorders it above the buttons row, and resizes form buttons to match input height. Also renames the builder tabs to Parameters / Branching / Error handling.
| type="button" | ||
| onClick={() => { | ||
| navigator.clipboard.writeText( | ||
| `${window.location.origin}/forms/quick-approve` | ||
| ); | ||
| setMoreMenuOpen(false); | ||
| }} |
| export type { ExpressionFieldProps, ExpressionMode } from './ExpressionField'; | ||
| export { ExpressionField } from './ExpressionField'; | ||
| export type { | ||
| LockableFieldType, | ||
| LockableValueFieldMode, | ||
| LockableValueFieldProps, | ||
| } from './LockableValueField'; | ||
| export { | ||
| DEMO_SELECT_OPTIONS, | ||
| FIELD_TYPE_META, | ||
| LockableValueField, | ||
| parseListValue, | ||
| } from './LockableValueField'; | ||
| export { NodePropertyPanel } from './NodePropertyPanel'; | ||
| export type { NodePropertyPanelProps } from './NodePropertyPanel.types'; |
| {label ?? ( | ||
| <Label htmlFor={id} className="text-xs font-medium text-foreground-muted"> | ||
| {FIELD_LABEL[mode]} | ||
| {required && <span className="ml-0.5 text-destructive">*</span>} | ||
| </Label> | ||
| )} |
| {label ?? ( | ||
| <Label htmlFor={id} className="text-xs font-medium text-foreground-muted"> | ||
| {FIELD_LABEL[mode]} |
| /** | ||
| * LockableValueField — a field that can be locked to read-only, typed as one | ||
| * of several data types, and (for scalar types) switched between a literal | ||
| * value and a JS expression. | ||
| * | ||
| * Prototype: the expression mode is styled as code (monospace) but does not | ||
| * yet carry real syntax highlighting or evaluation. See ExpressionField for | ||
| * the direction a full code-editing surface would take. Select/multiselect | ||
| * options are demo placeholders; file uploads aren't persisted anywhere. | ||
| */ | ||
| export function LockableValueField({ | ||
| value = '', | ||
| onValueChange, | ||
| locked = true, | ||
| onLockedChange, | ||
| mode = 'fixed', | ||
| onModeChange, | ||
| fieldType = 'string', |
| * directly, making it the Tailwind entry point for the whole app — including | ||
| * apollo-react's canvas stories. Without this, utility classes unique to | ||
| * apollo-react (not already used somewhere in apollo-wind) silently produce | ||
| * no CSS, since they'd never be scanned otherwise. */ | ||
| @source "../../../apollo-react/src/**/*.{js,jsx,ts,tsx}"; No newline at end of file |
Empty commit to refresh the PR's Storybook preview deployment with today's Form HITL changes for coworker review.
- Controls default to Show instead of Hide across the showcase and both
Form HITL / Form HITL Review panels.
- Fixed/Expression trigger icon no longer varies by field type (was
swapping to the per-type icon; now strictly Type vs Code2).
- Simplified the "more" menu to a single AI-generate action (dropped
Preview and Get link) and removed the now-unused Preview/JSON views
and the UI/JSON toggle from the Quick form builder.
- Added a header-synced close (X) action to LockableValueField via
headerActions, wired to controlsVisibility, and fixed the same
affordance on the left-panel field rows (previously hardcoded to
row-hover regardless of the Show/Hide toggle).
- Swapped the Insert-variable glyph from a "{x}" text glyph to a Braces
icon so it reads distinctly from the new close X.
- Added a showFieldActions prop to suppress the AI-assist/Insert-variable
actions for read-only reviewer contexts, and used it on the Form HITL
Review left panel so reviewers can't reconfigure fields they're only
meant to approve.
- Renamed the lock toggle's Locked/Unlocked language to Read-only/Editable
everywhere it appears (aria-labels, dropdown items, showcase copy).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| const lockedDisplayValue = | ||
| fieldType === 'boolean' | ||
| ? value === 'true' | ||
| ? 'True' | ||
| : 'False' | ||
| : fieldType === 'date' | ||
| ? value |
| {label ?? ( | ||
| <Label htmlFor={id} className="text-xs font-medium text-foreground-muted"> | ||
| {FIELD_LABEL[mode]} | ||
| {required && <span className="ml-0.5 text-destructive">*</span>} | ||
| </Label> | ||
| )} |
| <Switch | ||
| id={id} | ||
| checked={value === 'true'} | ||
| onCheckedChange={(checked) => onValueChange?.(String(checked))} | ||
| /> |
Summary
Prototype for a Quick Approve HITL panel demonstrating quick-form building and review inside the Property Panel experience, built on the
InputGroupprimitive from #890.LockableValueField— a reusable field that can be locked to read-only and typed as one of several field types (String, Integer, Date, Boolean, Single select, Multiselect, File):"Form HITL" story — the quick-form builder page:
@dnd-kit, same pattern asStageNode) with aDragOverlay+ synced insertion-line indicator."Form HITL Review" story — a read-only reviewer view of the same form (Reject/Approve actions in the header), sharing the same field-rendering logic as the builder's Preview mode.
apollo-windSwitch — added a propersmsize matching shadcn/ui's actual "Small" switch spec (14×24 track, 12px thumb);defaultsize is unchanged.Notes for reviewers
user-round-arrow-lefticon isn't in the installedlucide-react@0.577.0(added in1.20.0); usedUserRoundCheckinstead rather than bumping a shared dependency across a major version for one icon.@dnd-kitdependency already used byStageNode, following the same sensor + overlay convention.Test plan
pnpm exec tsc --noEmit— cleanpnpm biome check— cleanpnpm vitest run(NodePropertyPanel, Switch) — all existing tests pass, no regressions🤖 Generated with Claude Code