Skip to content

feat(apollo-wind): metadata-form controlled-host seam, string-list field, InfoTooltip - #1107

Open
apetraru-uipath wants to merge 6 commits into
mainfrom
feat/apollo-wind-guardrail-validator-form
Open

feat(apollo-wind): metadata-form controlled-host seam, string-list field, InfoTooltip#1107
apetraru-uipath wants to merge 6 commits into
mainfrom
feat/apollo-wind-guardrail-validator-form

Conversation

@apetraru-uipath

@apetraru-uipath apetraru-uipath commented Sep 1, 2026

Copy link
Copy Markdown

What changed?

This PR was restructured per the review decision (thread): the guardrails domain family moved out of apollo-wind into apollo-react (canvas) — see the follow-up PR (linked below) — and this PR now carries only the generic apollo-wind layer the family is built on:

forms/ — MetadataForm enablers for controlled hosts

  • Controlled-host seam on MetadataForm: values (per-field sync-in via setValue, no full RHF reset), onValuesChange (live subscription), errors (external errors merged as RHF type: 'external'), disableValidation, a synchronous components registry prop, and container: 'form' | 'div' for embedding inside an outer form. MetadataFormProps is now exported.
  • string-list field type: StringListField (repeated rows with Add/Remove, maxItems/maxLength, stable row ids) + StringListFieldMetadata + zod conversion; generic formatTemplate util.
  • Field metadata additions: tooltip/tooltipAriaLabel on BaseFieldMetadata, textarea minRows/maxLength, multiselect emptyMessage/searchPlaceholder.
  • Field renderer: label/htmlFor association + aria-invalid wiring, FieldLabel helper (required indicator + InfoTooltip), and the Controller ref is no longer spread onto function-component custom fields (fixes a React warning consumers' console-strict test suites turn into failures).
  • useWatch re-export from the forms barrel, so packages composing MetadataForm custom fields (apollo-react's guardrails) subscribe through the same react-hook-form instance.

ui/

  • InfoTooltip promoted out of the guardrails prototype into components/ui (with its a11y test).
  • Select and Textarea get aria-invalid error styling.

Removed relative to earlier revisions

  • custom/guardrails/ (the whole family), its root-barrel re-exports, the ./components/custom* export entries, the locale catalogs + loader, and the .gitignore negation. custom/ is fully unexported again.

Why the split?

Per maintainer review + the design doc §7.4 (revised 2026-09-09): the family is domain-shaped with exactly two consumers, would have made custom/ public for the first time, and introduced a wind-local i18n mechanism against the LOCALIZATION_GUIDE. It now lives canvas-adjacent in apollo-react on lingui, built on this PR's wind layer.

Follow-up PR (stacked on this one): #1138feat(apollo-react): guardrails component family under canvas.

How has this been tested?

  • Full apollo-wind vitest suite green (coverage above the package thresholds), tsc clean, biome clean, rslib build clean (no custom/ exports in dist).
  • Consumers (flow-workbench#3533, Agents#6195) re-pointed at this PR's preview + the follow-up PR's apollo-react preview.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings September 1, 2026 15:40
@apetraru-uipath apetraru-uipath added the dev-packages Adds dev package publishing on pushes to this PR label Sep 1, 2026
@github-actions github-actions Bot added pkg:apollo-wind size:XXL 1,000+ changed lines. labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Apollo Coded App preview deployments finished with failures.

Project Status Preview Updated (PT)
apollo-design Failed Logs Sep 09, 2026, 03:45:45 PM
apollo-docs Ready Preview · Logs Sep 09, 2026, 03:45:45 PM
apollo-landing Ready Preview · Logs Sep 09, 2026, 03:45:45 PM
apollo-vertex Ready Preview · Logs Sep 09, 2026, 03:45:45 PM

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Dependency License Review

  • 1937 package(s) scanned
  • ✅ No license issues found
  • ⚠️ 2 package(s) excluded (see details below)
License distribution
License Packages
MIT 1708
ISC 88
Apache-2.0 55
BSD-3-Clause 27
BSD-2-Clause 23
BlueOak-1.0.0 8
MPL-2.0 4
MIT-0 3
CC0-1.0 3
MIT OR Apache-2.0 2
(MIT OR Apache-2.0) 2
Unlicense 2
LGPL-3.0-or-later 1
Python-2.0 1
CC-BY-4.0 1
(MPL-2.0 OR Apache-2.0) 1
Unknown 1
Artistic-2.0 1
(WTFPL OR MIT) 1
(BSD-2-Clause OR MIT OR Apache-2.0) 1
CC-BY-3.0 1
0BSD 1
(MIT OR CC0-1.0) 1
MIT AND ISC 1
Excluded packages
Package Version License Reason
@img/sharp-libvips-linux-x64 1.3.2 LGPL-3.0-or-later LGPL pre-built binary, not linked
khroma 2.1.0 Unknown MIT per GitHub repo, missing license field in package.json

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

📦 Dev Packages

Package Status Updated (PT)
@uipath/apollo-wind@2.48.0-pr1107.08915ce 🟢 Published Sep 09, 2026, 03:40:56 PM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces the first shared “Guardrails” component family in apollo-wind by adding GuardrailValidatorForm (a fully controlled, host-validated parameter editor) along with supporting types, i18n catalog loading, and save-time utilities. This also begins exposing components/custom as a public API surface for the package.

Changes:

  • Added GuardrailValidatorForm with editor support for 7 parameter types plus an override slot (renderParameter) for product-specific editors.
  • Added guardrails utilities (seedGuardrailParameters, syncMapEnumParameters, dropEmptyOptionalParameters, getRequiredEmptyParameterIds) and a lightweight locale-catalog loader with 14 locale modules.
  • Updated barrels/exports (src/index.ts, src/components/*, and package.json exports) and adjusted .gitignore to include handwritten locale TS sources.
File summaries
File Description
packages/apollo-wind/src/index.ts Exposes the guardrails component family + utilities/types from the package root entrypoint.
packages/apollo-wind/src/components/index.ts Adds custom/ shelf to the components barrel.
packages/apollo-wind/src/components/custom/index.ts Publicly exports only the reviewed guardrails family from custom/.
packages/apollo-wind/src/components/custom/guardrails/index.ts Guardrails family barrel (component, types, i18n, utilities).
packages/apollo-wind/src/components/custom/guardrails/types.ts Defines the parameter/definition unions and the GuardrailValidatorFormProps contract.
packages/apollo-wind/src/components/custom/guardrails/utils.ts Adds seeding/cleanup/sync helpers and the required-empty predicate for hosts.
packages/apollo-wind/src/components/custom/guardrails/utils.test.ts Unit coverage for guardrails utilities (seeding, syncing, pruning, required-empty predicate).
packages/apollo-wind/src/components/custom/guardrails/i18n.ts Defines chrome-string labels, default English labels, interpolation and merge logic.
packages/apollo-wind/src/components/custom/guardrails/load-messages.ts Adds supported-locale list, locale normalization, and lazy catalog loading.
packages/apollo-wind/src/components/custom/guardrails/load-messages.test.ts Tests locale resolution, catalog loading/fallback behavior, and label merging.
packages/apollo-wind/src/components/custom/guardrails/guardrail-validator-form.tsx Implements the main controlled form renderer and async label resolution hook.
packages/apollo-wind/src/components/custom/guardrails/guardrail-validator-form.test.tsx Comprehensive RTL + jest-axe coverage across all parameter types, overrides, i18n, a11y.
packages/apollo-wind/src/components/custom/guardrails/guardrail-validator-form.stories.tsx Storybook coverage for interactive scenarios, errors, override slot, and localization.
packages/apollo-wind/src/components/custom/guardrails/components/parameter-label.tsx Shared label + tooltip + inline error rendering primitives for parameter fields.
packages/apollo-wind/src/components/custom/guardrails/components/number-parameter-field.tsx Number editor field implementation.
packages/apollo-wind/src/components/custom/guardrails/components/text-parameter-field.tsx Multiline text editor field implementation.
packages/apollo-wind/src/components/custom/guardrails/components/boolean-parameter-field.tsx Boolean editor field implementation.
packages/apollo-wind/src/components/custom/guardrails/components/enum-parameter-field.tsx Enum select editor (including “synthetic option” behavior for stale stored values).
packages/apollo-wind/src/components/custom/guardrails/components/enum-list-parameter-field.tsx Enum-list chips editor with popover fallback for long option lists.
packages/apollo-wind/src/components/custom/guardrails/components/map-enum-parameter-field.tsx Map-enum editor rendering rows derived from a sibling enum-list parameter.
packages/apollo-wind/src/components/custom/guardrails/components/text-list-parameter-field.tsx Text-list repeated-row editor with add/remove and stable-row key strategy.
packages/apollo-wind/src/components/custom/guardrails/README.md Local package documentation for contract, utilities, localization, and shadow-DOM consumption notes.
packages/apollo-wind/src/components/custom/guardrails/locales/en.ts English chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/de.ts German chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/es.ts Spanish chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/es-MX.ts Spanish (Mexico) chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/fr.ts French chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/ja.ts Japanese chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/ko.ts Korean chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/pt.ts Portuguese chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/pt-BR.ts Portuguese (Brazil) chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/ro.ts Romanian chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/ru.ts Russian chrome-string catalog module (currently empty; relies on per-key English fallback).
packages/apollo-wind/src/components/custom/guardrails/locales/tr.ts Turkish chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/zh-CN.ts Chinese (Simplified) chrome-string catalog module.
packages/apollo-wind/src/components/custom/guardrails/locales/zh-TW.ts Chinese (Traditional) chrome-string catalog module.
packages/apollo-wind/package.json Adds ./components/custom exports to publish the new public custom shelf.
.gitignore Un-ignores the handwritten guardrails locale TS sources under packages/apollo-wind/.../locales/*.ts.
Review details
  • Files reviewed: 37/38 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/apollo-wind/package.json Outdated
Comment thread packages/apollo-wind/src/components/forms/string-list-field.tsx
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Storybook visual diff

⏭️ Skipped: the apollo-design preview deployment did not succeed, so no comparison ran. Logs

Updated (PT): Sep 09, 2026, 03:45:53 PM

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage + size by package

Per-package coverage and bundle size on this PR. New-line coverage = of the source lines this PR adds or changes, the % hit by tests.

Package Coverage New-line coverage Packed (gzip) Unpacked vs main
@uipath/apollo-core 42.01 MB 50.12 MB ±0
@uipath/apollo-react 39.9% 40.0% (2/5) 7.56 MB 29.14 MB +6 B
@uipath/apollo-ui-icons 2.85 MB 6.91 MB ±0
@uipath/apollo-wind 66.8% 89.9% (89/99) 458.3 KB 2.90 MB +7.3 KB
@uipath/ap-chat 85.8% 43.92 MB 56.68 MB ±0

"Coverage" is each package's own coverage.include scope (e.g. apollo-core instruments only scripts/). "Packed"/"Unpacked" come from npm pack --dry-run and only cover built packages — "—" means not measured this run (package not affected / not built). "vs main" is the packed (gzipped) delta against the last successful main build (the package-sizes artifact from the Release workflow); "—" there means no main baseline was available this run. The baseline is main's latest build, not this PR's exact merge-base, so it includes any drift since the branch diverged. Packages with no vitest config are omitted.

Copilot AI review requested due to automatic review settings September 1, 2026 15:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

packages/apollo-wind/package.json:52

  • The ./components/custom/* export will expose every top-level module under src/components/custom/ (e.g. canvas-studio.tsx, chat-*, panel-*), even though src/components/custom/index.ts explicitly says only reviewed families should be public. It also likely won’t resolve components/custom/guardrails correctly because the built output will be dist/components/custom/guardrails/index.*, not dist/components/custom/guardrails.*.

Consider removing the wildcard export and adding a single explicit ./components/custom/guardrails export instead (root @uipath/apollo-wind already re-exports the guardrails API).

    "./components/custom/*": {
      "import": "./dist/components/custom/*.js",
      "require": "./dist/components/custom/*.cjs",
      "types": "./dist/components/custom/*.d.ts"
    },

packages/apollo-wind/src/components/custom/guardrails/components/text-list-parameter-field.tsx:39

  • Calling setState during render (if (prevLength !== items.length) { ... }) is an anti-pattern and can trigger React warnings, extra renders, or infinite loops (especially under StrictMode’s double-invocation). Sync rowIds in an effect keyed on items.length instead of mutating state while rendering.
  // Stable per-row ids: without them, removing index N would reuse the DOM node of a
  // surviving sibling and swap a focused textarea's content with someone else's. Resync
  // during render (React's official pattern) when the `items` array length changes
  // out-of-band; the local add/remove handlers below already mutate ids in lockstep.
  const [rowIds, setRowIds] = useState<string[]>(() => items.map(() => crypto.randomUUID()));
  • Files reviewed: 37/38 changed files
  • Comments generated: 1
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 1, 2026 21:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

packages/apollo-wind/src/components/custom/guardrails/components/text-list-parameter-field.tsx:44

  • This component updates state during render (setPrevLength / setRowIds inside the render body). This can trigger React warnings and can behave poorly under StrictMode/double-invocation; the resync should be done in an effect (e.g., useEffect keyed on items.length) or by restructuring state to avoid derived state.
  const [prevLength, setPrevLength] = useState(items.length);
  if (prevLength !== items.length) {
    setPrevLength(items.length);
    setRowIds((prev) =>
      prev.length < items.length
  • Files reviewed: 56/57 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment thread packages/apollo-wind/src/components/custom/guardrails/load-messages.ts Outdated
Comment thread packages/apollo-wind/src/components/custom/guardrails/utils.ts Outdated
@apetraru-uipath apetraru-uipath changed the title feat(apollo-wind): add guardrail validator form component family feat(apollo-wind): guardrails component family (validator form + builder screen) Sep 1, 2026
Copilot AI review requested due to automatic review settings September 2, 2026 08:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

There are concrete correctness/consumption issues (notably exports subpath mapping and a strict-typing/indexing bug in syncMapEnumParameters), plus a React anti-pattern (state updates during render) that should be addressed before merge.

Review details

Suppressed comments (3)

packages/apollo-wind/src/components/custom/guardrails/utils.ts:72

  • syncMapEnumParameters builds currentMap as param.value ? param.value : {}. That widens the type to {} and makes currentMap[key] unsafe (and can fail TypeScript strict indexing). If you want the runtime guard, keep the type as Record<string, number> (and also exclude arrays) before indexing.
    packages/apollo-wind/package.json:52
  • The ./components/custom/* export maps to ./dist/components/custom/*.js, but this package outputs nested index.js files for folder entrypoints (e.g. src/components/custom/guardrails/index.tsdist/components/custom/guardrails/index.js). As written, imports like @uipath/apollo-wind/components/custom/guardrails are likely to fail resolution.
    "./components/custom/*": {
      "import": "./dist/components/custom/*.js",
      "require": "./dist/components/custom/*.cjs",
      "types": "./dist/components/custom/*.d.ts"
    },

packages/apollo-wind/src/components/custom/guardrails/components/text-list-parameter-field.tsx:44

  • This component calls setPrevLength / setRowIds during render when items.length changes. Updating state during render is not safe in React (can trigger render loops and breaks concurrent rendering assumptions). Please move the resync logic into an effect keyed on items.length, or use a useRef-backed id list that you mutate synchronously without setState.
  const [rowIds, setRowIds] = useState<string[]>(() => items.map(() => crypto.randomUUID()));
  const [prevLength, setPrevLength] = useState(items.length);
  if (prevLength !== items.length) {
    setPrevLength(items.length);
    setRowIds((prev) =>
  • Files reviewed: 63/64 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 7, 2026 10:09
@apetraru-uipath
apetraru-uipath force-pushed the feat/apollo-wind-guardrail-validator-form branch from 7833830 to 67fba2d Compare September 7, 2026 10:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are a couple of correctness issues in the new code and package exports (React state updates during render in text-list-parameter-field, and a likely-broken ./components/custom/* export mapping for directory-based entries).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (4)

packages/apollo-wind/src/components/custom/guardrails/components/text-list-parameter-field.tsx:2

  • This file uses useEffect below (or should) to sync rowIds, but useEffect is not imported; without it, the suggested fix for the row-id syncing will not compile.
import { useCallback, useMemo, useState } from 'react';

packages/apollo-wind/src/components/custom/guardrails/components/text-list-parameter-field.tsx:44

  • Calling setState during render (if (prevLength !== items.length) { ... }) is a React anti-pattern and can cause extra render passes or warnings under Strict/Concurrent rendering. Move this resync into an effect keyed on items.length so it only runs after render.
  const [prevLength, setPrevLength] = useState(items.length);
  if (prevLength !== items.length) {
    setPrevLength(items.length);
    setRowIds((prev) =>

packages/apollo-wind/src/components/custom/guardrails/utils.ts:70

  • currentMap is intended to defend against malformed persisted values, but the current check treats arrays as valid objects (since typeof [] === 'object'). If param.value is accidentally an array, indexing into it will yield unexpected results. Consider explicitly excluding arrays when normalizing the persisted map.
    packages/apollo-wind/package.json:52
  • The ./components/custom/* export target points to ./dist/components/custom/*.js, but the guardrails family is emitted as a directory (components/custom/guardrails/index.js) under the current rslib config (source entries include nested paths). As written, deep imports like @uipath/apollo-wind/components/custom/guardrails will resolve to dist/components/custom/guardrails.js (which likely doesn’t exist). Update the export mapping to point at */index files.
    "./components/custom/*": {
      "import": "./dist/components/custom/*.js",
      "require": "./dist/components/custom/*.cjs",
      "types": "./dist/components/custom/*.d.ts"
    },
  • Files reviewed: 63/64 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread packages/apollo-wind/src/components/custom/guardrails/load-messages.ts Outdated
Copilot AI review requested due to automatic review settings September 7, 2026 15:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

It introduces a React render-path state update bug in StringListField and exposes unintended public APIs via the ./components/custom/* package export wildcard.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

packages/apollo-wind/src/components/forms/string-list-field.tsx:51

  • setPrevLength / setRowIds are called during render when items.length changes. Updating state while rendering will trigger React warnings (and can cause extra renders or loops under StrictMode). Sync the rowIds array in an effect keyed by items.length instead of performing state updates in the render path.
  • Files reviewed: 71/72 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread packages/apollo-wind/package.json Outdated
Comment thread packages/apollo-wind/src/components/forms/string-list-field.tsx
Copilot AI review requested due to automatic review settings September 7, 2026 15:29
@apetraru-uipath apetraru-uipath changed the title feat(apollo-wind): guardrails component family (validator form + builder screen) feat(apollo-wind): metadata-form controlled-host seam, string-list field, InfoTooltip Sep 9, 2026
@apetraru-uipath

Copy link
Copy Markdown
Author

Restructured per the review decision: this PR is now only the generic apollo-wind layer (forms/ MetadataForm enablers + string-list field + InfoTooltip promotion + aria-invalid styling), landable independently. The guardrails domain family moved to apollo-react (canvas) on lingui — stacked PR #1138, which also deletes the locale loader and the .gitignore negation flagged here. Design-doc record: §7.4, revised 2026-09-09. Consumers (flow-workbench#3533, Agents#6195) re-point at the apollo-react subpath next.

@github-actions github-actions Bot added size:XL 500-999 changed lines. and removed size:XXL 1,000+ changed lines. labels Sep 9, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

StringListField does not propagate aria-invalid to its row inputs, which prevents the new invalid styling from applying and weakens accessibility semantics.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 14/14 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread packages/apollo-wind/src/components/forms/string-list-field.tsx
Comment thread packages/apollo-wind/src/components/forms/field-renderer.tsx Outdated
Comment thread packages/apollo-wind/src/components/ui/info-tooltip.tsx
apetraru-uipath added a commit that referenced this pull request Sep 9, 2026
Moves the guardrails family out of apollo-wind's custom/ prototype shelf into
apollo-react per the #1107 review decision: canvas-adjacent (MUI-free, built on
wind primitives and the forms/ MetadataForm engine), exported from the canvas
components barrel plus a narrow ./canvas/guardrails subpath.

Strings move to lingui: useSafeLingui labels-hooks with explicit guardrails.*
ids replace the wind-local catalogs and loader; the 60 keys ship translated in
the shared canvas catalog for 13 locales (ru falls back to English per key).
Localized templates that cross into plain-string APIs are ICU messages
formatted with sentinel values, preserving the {{token}} convention.

The family's Tailwind classes ride the existing tailwind.canvas.css scan; adds
class-variance-authority (dep) and jest-axe (dev, matcher registered in the
shared test setup). Also silences Radix's aria-describedby warning on the
description-less builder dialog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up. StringListField rendered the FormFieldError text but never
marked the rows invalid, so the aria-invalid styling this PR adds to Textarea
never activated and assistive tech got no invalid signal for the inputs. Every
row now carries it — the error belongs to the list as a whole.

Also adds the InfoTooltip storybook entry requested in review (default,
label-adjacent, rich and long content), documenting that an ancestor
TooltipProvider is required and that schema-driven forms get the trigger from
the field `tooltip` metadata.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 9, 2026 14:50
@apetraru-uipath

Copy link
Copy Markdown
Author

@CalinaCristian both points are addressed — point 2 reshaped the PR entirely.

2. Placement / i18n — agreed, and done. The guardrails family is out of apollo-wind and now lives in apollo-react at src/canvas/components/Guardrails/ (MUI-free, built on wind primitives, exported via a narrow ./canvas/guardrails subpath): #1138. Strings moved onto the package's existing lingui setup — useSafeLingui with guardrails.* ids and English defaults, translations in the shared canvas catalog (60 keys, 13 locales ported) — so the wind-local TS catalogs, the bespoke loader and the .gitignore negation are all deleted, and apollo-wind stays i18n-free per its LOCALIZATION_GUIDE. Its Tailwind classes ride the existing tailwind.canvas.css scan, so there's no new CSS artifact either.

This PR is now only the generic wind layer you'd expect to keep there: the MetadataForm controlled-host seam, a string-list field type, tooltip field metadata, InfoTooltip promoted into components/ui, and aria-invalid styling on Select/Textarea. Nothing domain-shaped remains, and custom/ is fully unexported again.

Your framing was also the deciding evidence: both hosts already consume apollo-react/canvas (Flow at ~450 import sites; Agents in 16 files, already injecting tailwind.canvas.css into its shadow roots), and Agents' lockfile had been resolving two apollo-wind copies because of the direct pin — placing the family in apollo-react collapses that to one. Full write-up in the design doc §7.4.1: Guardrails Apollo Components.

1. Storybook — done for everything this PR still adds. f5485e4e adds info-tooltip.stories.tsx (default, label-adjacent, rich and long content) and metadata-form.stories.tsx already covers the string-list field type, the tooltip metadata and the controlled-host seam in one story. The guardrails components keep their three story files (builder, validator form, form layout) and moved with them to #1138, where they're picked up by the existing packages/apollo-react/src/canvas story glob.

Both consumer PRs (flow-workbench#3533, Agents#6195) are already re-pointed at the apollo-react subpath and verified running in both products.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It introduces a broad new public API surface and non-trivial form-state behavior changes that should get final human review despite only minor actionable nits found.

Review details
  • Files reviewed: 15/15 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread packages/apollo-wind/src/components/ui/info-tooltip.stories.tsx Outdated
…nts at

Review follow-up: the label-adjacent story set htmlFor="blocked-phrases" with
no such control, which is a dangling association and a misleading a11y example
in the one story specifically about label placement. It now renders the
Textarea the label names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 9, 2026 15:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Input fields in the updated FormFieldRenderer are not marked aria-invalid when an error exists, so invalid styling and accessibility semantics are inconsistent with the newly added aria-invalid patterns for other controls.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

packages/apollo-wind/src/components/forms/field-renderer.tsx:379

  • Input supports aria-invalid:* error styling (and aria-invalid is important for assistive tech), but the text/email Input isn’t marked invalid when error exists, so invalid styling/semantics won’t activate for these fields.

This issue also appears on line 396 of the same file.

packages/apollo-wind/src/components/forms/field-renderer.tsx:400

  • The number Input also isn’t marked aria-invalid when error exists, so error styling/semantics won’t activate for numeric fields.
          <Input
            id={field.name}
            value={formField.value as number | undefined}
            onBlur={formField.onBlur}
            name={formField.name}
  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Comment thread packages/apollo-wind/src/components/forms/field-renderer.tsx Outdated
Review follow-up: rather than a near-duplicate label component, FormFieldLabel
now takes `tooltip`/`tooltipAriaLabel` and renders the trigger after the
required indicator. That removes the reason the field-renderer helper existed
(it composed RequiredIndicator by hand purely to control that ordering), so the
helper is gone and the 11 call sites use FormFieldLabel with its `required`
prop directly.

The tooltip is now available to every FormFieldLabel consumer, not just
schema-driven forms; the InfoTooltip story shows that path as the idiomatic one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 9, 2026 20:56
@github-actions github-actions Bot added size:XXL 1,000+ changed lines. and removed size:XL 500-999 changed lines. labels Sep 9, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate issues affect submission behavior, accessibility, serialization, synchronization, validation, and build correctness.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (12)

Previously missed (3) — in code that hasn't changed since the last review.

packages/apollo-wind/src/components/forms/metadata-form.tsx:268

  • This sync effect runs before the initialization effect below. When stableSchema.initialData is present, the later reset(data) overwrites the externally supplied values; because the prop has not changed, this effect does not run again. A controlled host can therefore lose its values immediately after mount. Apply the external values after initialization/reset, or otherwise re-run the sync when that reset completes.

This issue also appears on line 278 of the same file.
packages/apollo-wind/src/components/forms/validation-converter.ts:83

  • Adding string-list to the array base schema does not make validation: { required: true } reject an empty array. The required branch only adds a non-empty check for string fields, so a required string-list can submit [] despite showing the required indicator. Apply an array-specific minimum of one for required lists and cover the empty-list case.
    packages/apollo-wind/src/components/ui/select.tsx:37
  • The new invalid classes are followed by a future:border-0 style, so an invalid SelectTrigger has no visible error state in the future theme unless it is focused. The established Input pattern adds a future invalid ring; add the same ring to this future variant.

packages/apollo-wind/src/components/forms/field-renderer.tsx:386

  • Number validation errors have the same gap: the error text is rendered, but the Input is not marked aria-invalid, so its invalid styling and assistive-technology state are absent. Forward the error state to this input as well.
          <Input
            id={field.name}
            value={formField.value as number | undefined}
            onBlur={formField.onBlur}
            name={formField.name}
            ref={formField.ref as React.Ref<HTMLInputElement>}

packages/apollo-wind/src/components/forms/field-renderer.tsx:598

  • Date and datetime pickers are also rendered under these labels, but neither picker receives an id or an accessible name derived from field.label; the picker components do not currently accept an id. The visible label is therefore not associated, and screen readers announce only the placeholder/value. Extend the picker props to forward an id/aria-label and pass them from the renderer.
          <FormFieldLabel
            required={required}
            tooltip={field.tooltip}
            tooltipAriaLabel={field.tooltipAriaLabel}
          >
            {field.label}
          </FormFieldLabel>

packages/apollo-wind/src/components/forms/form-schema.ts:177

  • These new metadata properties are not handled by schema-serializer.ts (serializeField currently stops at the existing ariaDescribedBy/old type-specific fields around schema-serializer.ts:209-243). A schema round-tripped through serializeSchema or shown/exported by the form designer will silently lose tooltip, tooltipAriaLabel, minRows, maxLength, emptyMessage, and searchPlaceholder, so the new behavior disappears from serialized forms.
  /** Info tooltip rendered next to the field label. */
  tooltip?: string;
  /** Accessible name of the tooltip trigger (default 'More information'). */
  tooltipAriaLabel?: string;

packages/apollo-wind/src/components/forms/form-schema.ts:281

  • The new string-list metadata is also dropped by serializeField (it only handles the old field properties). Form Designer output or any schema round-trip therefore loses maxItems, maxLength, minRows, addItemLabel, and removeItemAriaLabel, so the rendered list falls back to generic defaults. Add these fields to the serializer and cover the round-trip.
export interface StringListFieldMetadata extends BaseFieldMetadata {
  type: 'string-list';
  /** Cap on how many rows can be added; the Add button hides at the cap. */
  maxItems?: number;
  /** Per-row character cap (DOM maxLength on each row's textarea). */

packages/apollo-wind/src/components/forms/metadata-form.tsx:260

  • deepEqual compares enumerable object keys, so any two Date instances compare equal because they have no enumerable keys. Consequently, a controlled date or datetime value change can skip setValue and leave the form's field value stale. Use an equality check that handles Date values (and other non-plain objects) before deciding to skip the sync.
        if (!deepEqual(get(current, name), v)) {

packages/apollo-wind/src/components/forms/metadata-form.tsx:280

  • The appliedExternalErrorsRef guard means an external error is only re-applied when its message changes. If RHF runs the resolver after a submit or edit, it can replace a type: 'external' error with the resolver result (or clear it); while the errors prop remains unchanged, this effect then skips setError, so the advertised prop-owned error disappears. Reconcile external errors after validation or merge them into the rendered error state instead of relying only on the previous message.
      if (appliedExternalErrorsRef.current[name] !== message) {
        form.setError(name, { type: 'external', message });
      }

packages/apollo-wind/src/components/forms/metadata-form.tsx:240

  • The callback is now invoked before the isInitialized guard, so programmatic setValue calls made by onFormInit are reported as user edits. The built-in autoSavePlugin restores drafts with context.form.setValue during initialization, which will now fire onValuesChange and can make a controlled host echo initialization as a user change. Suppress callbacks during plugin initialization while retaining the intended first user edit.
      if (!name || syncingValuesRef.current) return;

      // Deliberately not behind the isInitialized gate: controlled hosts need the very first
      // user edit, which can land before the async init effect finishes.
      onValuesChangeRef.current?.(value as Record<string, unknown>, name);

packages/apollo-wind/src/components/forms/string-list-field.tsx:57

  • When a controlled host removes a row out of band, truncating prev by length keeps the IDs for the first positions rather than the surviving rows. For example, [a, b] -> [b] gives b the old a key, so React reuses the wrong DOM node and can lose the focused row, defeating the stable-row-id guarantee. Reconcile IDs with the actual removal/reorder (or keep IDs alongside the controlled items) instead of slicing by length.
  if (prevLength !== items.length) {
    setPrevLength(items.length);
    setRowIds((prev) =>
      prev.length < items.length
        ? [
            ...prev,
            ...Array.from({ length: items.length - prev.length }, () => crypto.randomUUID()),
          ]
        : prev.slice(0, items.length)

packages/apollo-wind/src/components/ui/info-tooltip.stories.tsx:23

  • This newly added Storybook documentation uses a spaced em dash. Storybook copy in this repository must not use ; split this into two sentences (or use a colon) so the story follows the documentation convention.
Schema-driven forms get it for free — set \`tooltip\` on any field metadata and
  • Files reviewed: 17/17 changed files
  • Comments generated: 8
  • Review effort level: Lite

Comment thread packages/apollo-wind/src/components/forms/metadata-form.tsx
Comment thread packages/apollo-wind/src/components/ui/info-tooltip.stories.tsx Outdated
Comment thread packages/apollo-wind/src/components/forms/field-renderer.tsx
Comment thread packages/apollo-wind/src/components/forms/field-renderer.tsx
Comment thread packages/apollo-wind/src/components/forms/field-renderer.tsx
Comment thread packages/apollo-wind/src/components/forms/field-renderer.tsx
Comment thread packages/apollo-wind/src/components/ui/form-field.tsx Outdated
Comment thread packages/apollo-wind/src/components/ui/textarea.tsx Outdated
Review follow-up, eight findings:

- container='div' rendered schema submit actions as type="submit" with no
  owning <form>, so a click would submit whatever ancestor form the host
  embedded the fields in — the exact hazard div mode exists to avoid. Submit
  actions are now plain buttons wired to the form's own submit handler.
- The tooltip trigger is a <button>, which <label>'s content model forbids as a
  descendant; it now renders beside the label in an inline wrapper, keeping the
  ref, htmlFor and styling on the label element.
- text/email and number inputs never received aria-invalid, unlike the
  textarea/select paths, so their invalid styling and ARIA state never applied.
- multiselect and file-upload had no label association: both now get the field
  id with htmlFor on the label (file upload also takes the field label as its
  dropzone name).
- string-list never forwarded the controller's onBlur, so blur-mode validation
  and touched state never fired for it.
- Textarea's new aria-invalid border is invisible in the future theme, which
  renders it borderless; it now carries the same invalid ring as Input.
- The InfoTooltip story used the React UMD namespace without importing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 9, 2026 22:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved findings affect controlled form behavior, accessibility, serialization, validation, and error styling.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (8)

Previously missed (3) — in code that hasn't changed since the last review.

packages/apollo-wind/src/components/forms/metadata-form.tsx:258

  • This sync effect runs before the one-time initialization effect. When schema.initialData is present, initializeForm later calls reset(data), overwriting the controlled values that were applied here; because the prop did not change, this effect will not run again. Apply controlled values after initialization (or merge them into the initialization reset) so values remains the source of truth.
    packages/apollo-wind/src/components/forms/metadata-form.tsx:260
  • The controlled sync uses deepEqual for every field, but the shared helper compares only enumerable keys and treats any two Date instances as equal. A controlled date/datetime value therefore stops updating when the host supplies a different date after the first sync. Use a comparison that handles non-plain values (or compare date timestamps) before skipping setValue.
    packages/apollo-wind/src/components/forms/metadata-form.tsx:410
  • Replacing the form with a <div> does not prevent implicit submission from an ancestor <form>: pressing Enter in a text input inside this branch can still submit the host form. That contradicts the documented container="div" guarantee of safe embedding and no implicit Enter-to-submit; handle this at the host boundary without breaking Enter in multiline controls.

packages/apollo-wind/src/components/forms/form-schema.ts:177

  • The new BaseFieldMetadata.tooltip is not rendered for checkbox fields: that renderer still calls FormFieldLabel without tooltip or tooltipAriaLabel, so a schema can declare the new metadata but no info trigger appears for this field type. Forward both properties in the checkbox branch as done for the other built-in fields.
  /** Info tooltip rendered next to the field label. */
  tooltip?: string;
  /** Accessible name of the tooltip trigger (default 'More information'). */
  tooltipAriaLabel?: string;

packages/apollo-wind/src/components/forms/form-schema.ts:177

  • The new metadata is not handled by schema-serializer.ts's serializeField: tooltip fields, textarea sizing/length, multiselect placeholders/messages, and all string-list options are silently dropped when a schema is serialized and loaded again. Add these properties to the serializer and cover the round trip so the new field configuration survives JSON persistence.
  /** Info tooltip rendered next to the field label. */
  tooltip?: string;
  /** Accessible name of the tooltip trigger (default 'More information'). */
  tooltipAriaLabel?: string;

packages/apollo-wind/src/components/forms/metadata-form.tsx:283

  • Manual and resolver errors share RHF's single error map here. With mode: 'onChange' or onBlur, a subsequent resolver run can remove this type: 'external' error while the errors prop still contains it; conversely, clearing it below can also remove a resolver error that was overwritten. Keep host errors in an overlay/reapply them after resolver updates, or require validation to be disabled whenever errors is supplied.
        form.setError(name, { type: 'external', message });
      }
    }
    for (const name of Object.keys(appliedExternalErrorsRef.current)) {
      if (!next[name] && form.getFieldState(name).error?.type === 'external') {

packages/apollo-wind/src/components/forms/string-list-field.tsx:114

  • StringListField ignores the inherited ariaLabel metadata, so a schema-supplied custom accessible name is silently discarded for every row. Use the metadata override as the base of each row's accessible name, falling back to field.label.
              aria-label={`${field.label} ${index + 1}`}

packages/apollo-wind/src/components/forms/validation-converter.ts:83

  • The new string-list branches are not directly covered in validation-converter.test.ts; the added MetadataForm string-list tests all use disableValidation, so the base array schema and minItems/maxItems conversion can regress without detection. Add converter cases for string-list, including its array constraints.
    case 'multiselect':
    case 'string-list':
      return z.array(z.string());
  • Files reviewed: 17/17 changed files
  • Comments generated: 6
  • Review effort level: Lite

Comment on lines +639 to +645
<FormFieldLabel
required={required}
tooltip={field.tooltip}
tooltipAriaLabel={field.tooltipAriaLabel}
>
{field.label}
</FormFieldLabel>
Comment on lines 646 to 650
<FileUpload
id={field.name}
ariaLabel={field.ariaLabel ?? field.label}
accept={field.accept}
multiple={field.multiple}
schema={stableSchema}
context={context}
onReset={handleReset}
onSubmit={container === 'div' ? handleFormSubmit : undefined}
Comment on lines +82 to 83
case 'string-list':
return z.array(z.string());
data-slot="select-trigger"
className={cn(
'flex h-9 w-full cursor-pointer items-center justify-between rounded-md border border-input bg-transparent px-3 py-1 text-base transition-colors data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm [&>span]:line-clamp-1 future:h-10 future:rounded-xl future:border-0 future:bg-surface-overlay future:hover:bg-surface-hover future:px-4 future:gap-4 future:font-normal future:text-muted-foreground',
'flex h-9 w-full cursor-pointer items-center justify-between rounded-md border border-input bg-transparent px-3 py-1 text-base transition-colors data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-error aria-invalid:focus-visible:ring-error md:text-sm [&>span]:line-clamp-1 future:h-10 future:rounded-xl future:border-0 future:bg-surface-overlay future:hover:bg-surface-hover future:px-4 future:gap-4 future:font-normal future:text-muted-foreground',

**In forms, reach for \`FormFieldLabel\` instead of composing this by hand**: it takes
\`tooltip\` / \`tooltipAriaLabel\` and places the trigger after the required indicator for you.
Schema-driven forms get it for free — set \`tooltip\` on any field metadata and
@BenGSchulz

Copy link
Copy Markdown
Contributor

Follow-up review: focus on the controlled-host seam

The ask

Keep MetadataForm's contract as it is: values live in react-hook-form, validation lives in the schema, hosts interact through schema, plugins, and remount. Do the controlled translation in the guardrails adapter, where the frozen contract that needs it actually lives. Then drop values, onValuesChange, errors, disableValidation, and components from MetadataForm's props.

Why

The pattern already exists in production, in one of the two consumers. flow-workbench's ValidationPlugin.ts is a FormPlugin that runs per-field AJV plus an async worker and applies every result through context.form.setError / clearErrors. Custom components display those errors because the custom branch of the field renderer already passes error={error?.message} from RHF fieldState (field-renderer.tsx:280-289). Validation reaches custom components through RHF's error state, never through the resolver. Rules AJV cannot express live in the same plugin as synchronous checks (GOVERNANCE_CHECKS, with the comment "AJV can't model their policy caps"), which is the same category as guardrails' emptiness predicate. External writes, meaning undo/redo, are handled by remount, with recomputed initialData + reset reserved for the one form where remount flickers. In this repo, NodePropertyPanel does the smaller version of the same thing with plugins + resetKey + actions: [].

Three declared-but-never-implemented features are why the intended path looked insufficient. FormPlugin.components has always been typed (form-schema.ts:466) and was never read, which is why consumers register asynchronously and miss the first paint. ValidationConfig.custom is typed, documented as a jsep expression, and serialized (schema-serializer.ts:75), but the converter never reads it. And plugin.onValueChange sits behind the mount-lifetime isInitializedRef gate (:179), so the first keystroke can be swallowed. This PR fixes the first one and routes around the other two.

Validation is mostly expressible in the metadata contract today. Five of guardrails' seven emptiness rules already work: required: true for text and enum, pattern: '\\S' for whitespace-only, minItems: 1 for both list types (this PR correctly added string-list to isArrayType), and number/boolean need nothing. The two gaps are the custom-component boundary, not semantics: getBaseSchemaForType returns z.any() for type: 'custom', and required on z.any() is a no-op, so no schema constraint reaches the chips field, map-enum, or a renderParameter override.

What to change

apollo-wind: repair the three dead features (read plugin.components, implement ValidationConfig.custom through RulesEngine.evaluateExpression, and suppress onValueChange during the init reset instead of for the whole mount). Let custom fields declare a value shape so required and minItems can reach them. Unify the two emptiness predicates: buildZodSchema's superRefine has a thorough one that is only reachable for fields carrying rules, while plain required uses a narrower one. Keep container if you want it on its own merits, but drop the (container !== 'div' || stableSchema.actions) gate, since FormActions already returns null on an empty actions array (:824).

guardrails: move the values sync and error injection into the adapter as one documented hook. Add validation: { min, max } to number parameters. That last one is a live bug: min/max currently reach the DOM as input attributes only, and with no resolver and no browser submit, an out-of-range guardrail number is saveable today.

docs: the only story for the new string-list field type routes it through the full seam, with the host reimplementing a check the schema expresses as minItems: 1. Give the field type a story on the intended path, and state the ownership contract once in forms/README.md, naming NodePropertyPanel and ValidationPlugin as the reference hosts.

Gotchas worth writing down

  • FormContext.form is the full UseFormReturn, and autoSavePlugin already establishes context.form.setValue as a plugin pattern (form-plugins.tsx:64-68). pluginsRef.current is reassigned every render and read at call time (:124-125, :181), so a plugin rebuilt per render with fresh closures works.
  • Per the RHF docs, subscribing to formState.isValid does not force validation; the configured mode governs, so a host Save gate needs mode: 'onChange' in the schema. setError forces isValid false but is overwritten the next time validation runs, so injected errors need a source of truth that re-applies.
  • setError on a bare path spreads the existing error node, and clearErrors on a bare path drops every synthetic entry with it (ValidationPlugin.ts:181-199).
  • A reset refreshes RHF only, so custom components holding their own state go stale. That is why flow-workbench remounts everywhere except the one form with a web component (useUndoRedoFormReset.ts).
  • The jsep evaluator supports literals, identifiers, binary, unary, member, and conditional nodes, with no CallExpression. So ValidationConfig.custom can express otherField.length > 0 but not value.some(...), which is why the all-blank text-list rule needs either call support or a named helper.
  • context.form is an unlabelled backdoor: a reviewer can grep for values= on MetadataForm, nobody greps for context.form.setValue. Keeping the bridge in one named hook is what makes it reviewable.

The counterargument

GuardrailValidatorForm's controlled contract is already consumed by flow-workbench#3533 and Agents#6195. Dropping values outright would silently turn parameters from controlled into seed-at-mount with no type or runtime error, so that part needs a deliberate decision rather than a cleanup. It is cheapest to make now, while both consumers are being re-pointed anyway.

Ship as-is

string-list and its converter wiring, InfoTooltip, the label and htmlFor association, aria-invalid on Select and Textarea, and the Controller ref fix. That last one is the correct upstream fix and makes flow-workbench's withForwardedFieldRef (FormPlugins.tsx:535-550) dead code. Worth confirming from their side: acceptsRef returns true for class, forwardRef, and memo components, which previously received field.ref and now receive none. Nothing I found consumes it, since the editor fields use their own editorRef prop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev-packages Adds dev package publishing on pushes to this PR pkg:apollo-wind size:XXL 1,000+ changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants