Skip to content

Add adapters for the remaining registered controls - #260

Merged
sneridagh merged 39 commits into
mainfrom
feat/widget-adapters
Oct 11, 2026
Merged

sneridagh merged 39 commits into
mainfrom
feat/widget-adapters

Conversation

@sneridagh

Copy link
Copy Markdown
Member

The rest of #243 step 4. Stacked on #256, which added TextWidget and DateTimeWidget.

Adapters

Every control registered as a widget is now a cmsui adapter that follows the widget contract (FormWidgetProps) and shows the required and invalid state:

Widget Adapter Wraps Before
textarea TextareaWidget Quanta TextAreaField not registered: the summary of a page rendered as a single-line text field
date DateWidget Quanta DatePicker (ISO YYYY-MM-DD strings) @plone/components DateField, which takes and emits DateValue objects
align, size, width AlignWidget, SizeWidget, WidthWidget Quanta AlignPicker, SizePicker, WidthPicker the raw radio-group controls

The three picker adapters share one contract → radio group mapping. They forward their widget options from the block schema (actions, actionsInfoMap), and select the schema default when there's no stored value.

Quanta's pickers are renamed

Following the packaging rule (#243 rule 5, and the guide's "Where controls and widgets live"): Quanta's AlignWidget, SizeWidget and WidthWidget are controls (React Aria radio groups), so they're renamed AlignPicker, SizePicker, WidthPicker, after Quanta's existing DatePicker. Their props types are exported. The old names stay as deprecated aliases, so this isn't breaking. Quanta's README and AGENTS.md, the conventions reference and the components README are updated.

Upgrade guide

The form refactoring had no upgrade notes yet. This PR adds a "Forms and widgets" section to the upgrade guide, covering the whole stack's changes for add-ons:

Tests

  • Unit (PickerWidgets/adapters.test.tsx):
    • the pickers select the stored value and emit the chosen one, fall back to the schema default, honor actions from the schema, and show errors;
    • the textarea renders a <textarea> with its value, required and invalid state;
    • the date widget writes the ISO date to its form input and shows its error.
  • New acceptance test: a page's Summary is a multi-line text area.
  • The image block style tests (align, size and width, with their side effects) pass through the new adapters.
  • Full acceptance suite (local, dev mode): 209 passed, 0 failed. cmsui: 239 unit tests pass; quanta: 6. Type checks, lint, the docs build and the optimizeDeps audit are clean. No references to the old names or Storybook IDs remain.

sneridagh and others added 30 commits June 2, 2026 15:28
* seven:
  Update README with the Plone Aurora
  Seven querystringWidget (#8017)
  [Seven] Add `renderEmptyState` to `Table` component (#8308)
  Recurrence widget (#7200)
Co-authored-by: Steve Piercy <web@stevepiercy.com>
* origin/main: (190 commits)
  Make control panels saveable (#220)
  Public UI: render a single .content-area root (#230) (#234)
  Remove tsconfig test/spec/story excludes that never matched any file (#222)
  Add PloneClient.extend() and clientEndpoints utility for custom endpoints (#221)
  Store the default block width of every top-level block (#190)
  Releasing @plone/aurora 1.0.0-alpha.19
  Release @plone/cmsui 1.0.0-alpha.12
  Release @plone/agave 1.0.0-alpha.9
  Release @plone/theming 1.0.0-alpha.9
  Release @plone/layout 1.0.0-alpha.14
  Release @plone/blocks 1.0.0-alpha.18
  Release @plone/plate 1.0.0-alpha.25
  Release @plone/registry 4.0.0-alpha.5
  Release @plone/quanta 1.0.0-alpha.2
  Content CSS Phase 11: theming guide for blocks and cleanup (#219)
  Rename the Plone blocks' classnames to the content contract (#218)
  Share the block spacing between the Public UI and the editor (#217)
  Content CSS Phase 8: structural blocks and content root to styles/content.css (#216)
  Remove the Plate media nodes from Aurora's presets (#215)
  Move the table styles to styles/content.css (#214)
  ...

# Conflicts:
#	packages/cmsui/components/BooleanWidget/BooleanWidget.stories.tsx
#	packages/cmsui/components/BooleanWidget/BooleanWidget.test.tsx
#	packages/cmsui/components/BooleanWidget/BooleanWidget.tsx
#	packages/cmsui/news/+boolean-widget-adapter.bugfix
Address the review: add a diagram that follows one field from its schema
to its control and back, a comparison with HTML form controls and
database columns, worked adapter examples for a boolean and a date
widget, and a glossary. Apply the suggested wording and title changes.

Add the missing cmsui and docs news fragments.
The content form, the control panels and the block settings each copied
the same fieldset loop and spread the raw field schema into the widget
props. They now render through SchemaFieldsets, and the field component
builds the widget props from the field schema in one place
(buildWidgetProps):

- label and description from the schema, readOnly from readonly
- defaultValue is the schema default, not the current value
- invalid and one errorMessage instead of the raw error list
- no forced placeholder
- schema keys the form understands (type, title, factory...) are no
  longer passed as props; other keys stay widget options, and the raw
  schema is available as schema

The field component is now field.SchemaField; field.Quanta stays as a
deprecated alias. ObjectBrowserWidget now starts from the field value
and uses the label as its title, since it relied on defaultValue being
the current value.

Refs #243
createFormStore keeps one form's values, and the touched, dirty and error
state of its fields, in a Jotai store of its own. The values are the
single source of truth: fields and any code that has the form read and
write them by path, and each field only subscribes to its own path.
Errors are derived from validators and server errors, and only show once
a field is touched or the form submitted.

The React bindings (useFormStore, FormProvider, useFormContext,
useSchemaField, useFieldValue, useSetFieldValue, useFormState) live in
form/react.tsx. useFormStore recreates the store when the form's key
changes.

Also adds Path / PathValue, to replace TanStack Form's DeepKeys /
DeepValue, and getByPath / setByPath.

Refs #244
* origin/main:
  Scope widget lookups to their category (#249)
  Subscribe only to the form fields that are read (#248)
  Give each form its own store and remount it per item (#247)
  Wait for hydration in sharing acceptance tests (#250)
…hema-field-renderer

* origin/widgetsDefinitions:
  Scope widget lookups to their category (#249)
  Subscribe only to the form fields that are read (#248)
  Give each form its own store and remount it per item (#247)
  Wait for hydration in sharing acceptance tests (#250)

# Conflicts:
#	packages/cmsui/components/RecurrenceWidget/RecurrenceWidget.tsx
#	packages/cmsui/routes/controlpanel.tsx
… feat/form-store

* origin/feat/schema-field-renderer:
  Scope widget lookups to their category (#249)
  Subscribe only to the form fields that are read (#248)
  Give each form its own store and remount it per item (#247)
  Wait for hydration in sharing acceptance tests (#250)
The content form, the control panels and the block settings now keep
their values in a store from @plone/helpers (useFormStore, FormProvider)
instead of TanStack Form plus a Jotai atom kept in sync with it. The
store's values are the single source of truth; there is no sync left.

- SchemaField reads its value and state with useSchemaField.
- ContentForm and the control panels submit the store's values.
- Block settings write each change, side effects included, back to the
  Plate node in one call, and start over when the node changes outside
  the form.
- BlocksEditor reads and writes the blocks through useFormContext.
- Plate's title binding finds the form with useOptionalFormContext, so
  the formAtom registry utility is gone, and so are routes/atoms and
  useAppForm.

Removing the per-form Jotai Provider also puts the settings sidebar
toggle and the sidebar on the same store again.

The recurrence modal still uses TanStack Form internally; it moves next.

Refs #244
…migration

* origin/feat/form-store:
  Fix the indentation of the jotai/vanilla optimizeDeps entry
The recurrence modal's own form now uses a form store from
@plone/helpers. It is not provided as the current form, so the fields in
the modal still read the event's start and end from the content form.
The field components take plain onChange(value) setters instead of
TanStack updaters. With that, nothing uses @tanstack/react-form anymore,
and cmsui drops the dependency.

Also fixes the modal crashing the edit form for events that end on days
1 to 9 of a month: the default "until" date was built without padding
the day (2026-10-5), which is not a valid ISO date. Adds the first
acceptance test for the recurrence widget.

Refs #244
…ecurrence-form-store

* origin/feat/form-migration:
  Fix the indentation of the jotai/vanilla optimizeDeps entry
Required fields must not be empty, and the validators registered as
validator utilities run on the fields they match, by fieldType, widget,
format, behaviorName + fieldName, or blockType + fieldName. This keeps
Volto's validator architecture, so add-ons plug in validators the same
way, with i18next's t instead of formatMessage. The validators of a form
are resolved once, from its schema, when the form store is created.

- Ports the Volto validators (lengths, patterns, email, URL, numbers,
  list sizes, unique items, event date range).
- An invalid save shows the errors on their fields, switches the
  content form to the Content tab and focuses the first invalid field.
- The add, edit and control panel actions return plone.restapi's
  validation errors to the form, which shows them on their fields.
- The default widget and the date and time widget become adapters
  (TextWidget, DateTimeWidget) that show the required and invalid state.

Refs #244
* origin/main:
  Fix the recurrence modal crash for events ending on days 1 to 9 (#255)
…hema-field-renderer

* origin/widgetsDefinitions:
  Fix the recurrence modal crash for events ending on days 1 to 9 (#255)
… feat/form-store

* origin/feat/schema-field-renderer:
  Fix the recurrence modal crash for events ending on days 1 to 9 (#255)
…migration

* origin/feat/form-store:
  Fix the recurrence modal crash for events ending on days 1 to 9 (#255)
…ecurrence-form-store

* origin/feat/form-migration:
  Fix the recurrence modal crash for events ending on days 1 to 9 (#255)
… feat/schema-validation

* origin/feat/recurrence-form-store:
  Fix the recurrence modal crash for events ending on days 1 to 9 (#255)
…hema-field-renderer

* origin/widgetsDefinitions:
  Explain where controls and widgets live in the widgets guide
… feat/form-store

* origin/feat/schema-field-renderer:
  Explain where controls and widgets live in the widgets guide
…migration

* origin/feat/form-store:
  Explain where controls and widgets live in the widgets guide
…ecurrence-form-store

* origin/feat/form-migration:
  Explain where controls and widgets live in the widgets guide
… feat/schema-validation

* origin/feat/recurrence-form-store:
  Explain where controls and widgets live in the widgets guide
Covers the same ground as Volto's validation docs, for Plone Aurora:
the default validators, registering validators by format (for blocks
and, through frontendOptions, for content types), by field type,
widget, behavior and field name, or block type and field name, how
validators match a field, overriding them, the validator signature,
translating messages, and invariants. The conceptual guide's validation
section now links to it.

Also updates the block schema side effects how-to, whose example still
showed the block settings form before it moved to the form store.
Volto types its validators with a required formatMessage, so a
validator utility type without it would no longer accept them. The
arguments now carry both: t and fieldName for Plone Aurora, and
formatMessage for validators written for Volto. Plone Aurora passes a
formatMessage built on t, so Volto-style validators, which translate
message descriptors, also run in its forms.

A compatibility test registers validators typed exactly as Volto types
them, so the type check fails if the shared type breaks them again.
The textarea, date, align, size and width widgets now follow the widget
contract, as cmsui adapters around Quanta controls:

- TextareaWidget, registered for the textarea widget (the summary of a
  page used to render as a single-line text field).
- DateWidget, around the Quanta DatePicker, which reads and writes ISO
  date strings. It replaces the @plone/components DateField, which
  worked with DateValue objects.
- AlignWidget, SizeWidget and WidthWidget around the Quanta pickers.
  They forward their widget options (actions, actionsInfoMap) from the
  block schema, and select the schema default without a stored value.

Quanta's AlignWidget, SizeWidget and WidthWidget are controls, so they
are renamed AlignPicker, SizePicker and WidthPicker, keeping the old
names as deprecated aliases.

Adds a Forms and widgets section to the upgrade guide, covering the
changes of the whole form refactoring for add-ons.

Refs #243
Adds versionchanged, versionremoved, versionadded and deprecated
admonitions for 1.0.0-alpha.20 to the Forms and widgets section, as the
other upgrade sections have.
@sneridagh

Copy link
Copy Markdown
Member Author

LGTM

@sneridagh
sneridagh requested a review from pnicolli October 9, 2026 19:40
An error occurred while trying to automatically change base from feat/schema-validation to feat/recurrence-form-store October 10, 2026 22:38
* origin/main:
  Validate schema-driven forms (#256)
  Move the recurrence modal off TanStack Form (#254)
  Move the forms onto the helpers form store (#253)
  Add a Jotai-native form layer to helpers (#252)
  Render every schema form through one field renderer (#251)
  Document and type the form widget contract (#97)
  Match utility dependencies exactly in the registry (#259)

# Conflicts:
#	packages/cmsui/config/widgets.ts
@sneridagh
sneridagh changed the base branch from feat/schema-validation to main October 10, 2026 22:43
sneridagh and others added 2 commits October 11, 2026 00:44
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@sneridagh
sneridagh added this pull request to stack #270 October 10, 2026 22:48
@sneridagh
sneridagh merged commit 96a80b5 into main Oct 11, 2026
41 checks passed
@sneridagh
sneridagh deleted the feat/widget-adapters branch October 11, 2026 22:46
sneridagh added a commit that referenced this pull request Oct 11, 2026
…skip-content

* commit '131c5e236cf8c17189e2ec94de4acbb040a1c0f6':
  Add the missing core widgets (#263)
  Type-check registered widgets and test them against the contract (#262)
  Give widgets a context instead of the route (#261)
  Add adapters for the remaining registered controls (#260)
  Validate schema-driven forms (#256)
  Move the recurrence modal off TanStack Form (#254)
  Move the forms onto the helpers form store (#253)
  Add a Jotai-native form layer to helpers (#252)
  Render every schema form through one field renderer (#251)
  Document and type the form widget contract (#97)

# Conflicts:
#	packages/cmsui/routes/objectBrowserWidget.tsx
sneridagh added a commit that referenced this pull request Oct 11, 2026
…source-prefix

* feat/resource-routes-skip-content:
  Drop the object browser breadcrumbs news fragment
  Add the missing core widgets (#263)
  Type-check registered widgets and test them against the contract (#262)
  Give widgets a context instead of the route (#261)
  Add adapters for the remaining registered controls (#260)
  Validate schema-driven forms (#256)
  Move the recurrence modal off TanStack Form (#254)
  Move the forms onto the helpers form store (#253)
  Add a Jotai-native form layer to helpers (#252)
  Render every schema form through one field renderer (#251)
  Document and type the form widget contract (#97)
sneridagh added a commit that referenced this pull request Oct 11, 2026
…drasekhar-eab6cd

* commit '5729a626fba8358d111bc30925f0294b99346841':
  Use a single @ for the Contents view resource routes (#268)
  Let resource routes skip the root content middleware (#265)
  Update React Aria Components to 1.22 (#273)
  Add the missing core widgets (#263)
  Type-check registered widgets and test them against the contract (#262)
  Give widgets a context instead of the route (#261)
  Add adapters for the remaining registered controls (#260)
  Validate schema-driven forms (#256)
  Move the recurrence modal off TanStack Form (#254)
  Move the forms onto the helpers form store (#253)
  Add a Jotai-native form layer to helpers (#252)
  Render every schema form through one field renderer (#251)
  Document and type the form widget contract (#97)

# Conflicts:
#	apps/aurora/app/middleware.server.ts
sneridagh added a commit that referenced this pull request Oct 11, 2026
* 'main' of github.com:plone/aurora:
  Use a single @ for the Contents view resource routes (#268)
  Let resource routes skip the root content middleware (#265)
  Update React Aria Components to 1.22 (#273)
  Add the missing core widgets (#263)
  Type-check registered widgets and test them against the contract (#262)
  Give widgets a context instead of the route (#261)
  Add adapters for the remaining registered controls (#260)
  Validate schema-driven forms (#256)
  Move the recurrence modal off TanStack Form (#254)
  Move the forms onto the helpers form store (#253)
  Add a Jotai-native form layer to helpers (#252)
  Render every schema form through one field renderer (#251)
  Document and type the form widget contract (#97)
  Match utility dependencies exactly in the registry (#259)
  Fix the recurrence modal crash for events ending on days 1 to 9 (#255)
  Scope widget lookups to their category (#249)
  Subscribe only to the form fields that are read (#248)
  Give each form its own store and remount it per item (#247)
  Wait for hydration in sharing acceptance tests (#250)

# Conflicts:
#	apps/aurora/app/middleware.server.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants