Skip to content

Add the missing core widgets - #263

Open
sneridagh wants to merge 5 commits into
feat/typed-widget-registryfrom
feat/missing-widgets
Open

sneridagh wants to merge 5 commits into
feat/typed-widget-registryfrom
feat/missing-widgets

Conversation

@sneridagh

@sneridagh sneridagh commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Step 7 of #243: the missing core widgets. Each one is built against the widget contract and runs describeWidgetContract.

Stacked on #262. Part of the review guide in #244 (comment).

Closes #243

Widgets

Widget Registered for Value
SelectWidget the choices widget (choices or vocabulary), widget: 'select' a token, or null
ArrayWidget type: 'array', widget: 'array', widget: 'token' a list of tokens
NumberWidget type: 'number', type: 'integer' a number, or null
FileWidget factory: 'File', factory: 'Image', widget: 'file' the file, or null
EmailWidget, PasswordWidget, UrlWidget widget: 'email', 'password', 'url' a string
  • Select (B3). config.widgets.choices is now registered with config.registerWidget({ key: 'choices', definition: SelectWidget }). registerWidget now accepts the choices slot, which holds one widget, and tells a component from a map of widgets even when the component has a displayName (before, counting its keys made it a map). The options come from the schema's choices, or from the field's vocabulary through a new @vocabulary resource route. Optional fields get a "No value" option.
  • Tokens ([CMSUI Widgets] - ArrayWidget #8). Free tokens, like the tags of a page, with suggestions from a vocabulary in widgetOptions. A closed list when items has choices or a vocabulary.
  • Number. A new Quanta NumberField control. It reads minimum and maximum, and takes whole numbers only for integer.
  • File (Add Image Content tab crashes because the image field is rendered as a TextField #163 follow-up). File and image fields (the Image's image, the lead image) used to get ImageWidget, which stores a URL that the content API can't save. They now get FileWidget, which uploads the file (base64) and keeps a stored file as it is. ImageWidget stays the widget of URL-valued widget: 'image' fields, such as the image block's url.
  • Email, password, URL. TextField with the right input type. The validators for widget: 'email' and widget: 'url' already existed (Validate schema-driven forms #256).

Changes found along the way

  • Lookup order. A widget registered for a vocabulary now wins over the choices widget. Without this, plone.app.vocabularies.Catalog fields would get a select that loads the whole catalog instead of the object browser. The guide documents the new order.
  • Term objects. plone.restapi sends some choice values as {token, title} objects (language, sort_tabs_on, displayed_types). Select and array read terms and write tokens. Before, these values crashed the render as "Objects are not valid as a React child".
  • @plone/client content schemas.
    • allow_discussion now accepts its choice tokens "True"/"False", which the backend accepts. Before, Zod rejected the save.
    • preview_image accepts a stored file.
    • The file fields accept null.
  • getVocabulary takes a b_size (-1 returns all the terms).

Contract gap

SelectWidget skips the contract's required check, with the reason recorded in the test. React Aria's Select marks a required field with data-required and the label's marker, not with aria-required, which a button can't have.

Tests

  • Contract tests: 7 new widgets, 117 passing, 1 skipped (the check above).
  • Unit tests: the lookup order, and registering the choices widget with registerWidget (a component with a displayName; fails with the old key counting).
  • New acceptance tests (core-widgets.test.ts). Each one fails when the old widget registrations are restored:
    • Tags are added with Enter and saved as a list.
    • The language select loads its vocabulary, and allow_discussion (stored as a boolean) is read and saved.
    • The navigation control panel shows its term lists and saves them.
    • The mail control panel saves smtp_port as a number and has a password input.
    • The File and Image add forms upload the file. This replaces the Add Image Content tab crashes because the image field is rendered as a TextField #163 add-form test.
  • Updated acceptance tests: the lead image now resolves to the file widget. The Add Image Content tab crashes because the image field is rendered as a TextField #163 object browser location check now runs in the image block settings of a nested page.
  • Full acceptance suite: 217/217 passed against the acceptance backend.

Docs

  • Core widgets reference: a section per widget.
  • Guide: the lookup order and the choices widget.
  • Upgrade guide: "File and image fields store files", with alpha.20 admonitions.

Select (the choices widget), array/tokens, number, file, email, password
and URL widgets, built against the widget contract and its tests. File and
image fields now upload the file instead of storing an image URL. A widget
registered for a vocabulary takes precedence over the choices widget.

Co-Authored-By: Claude <noreply@anthropic.com>
Drop registerChoicesWidget: registerWidget takes the choices slot, which
holds one widget. It now tells a widget component from a map of widgets
even when the component has keys of its own, such as displayName.

Co-Authored-By: Claude <noreply@anthropic.com>
@sneridagh

Copy link
Copy Markdown
Member Author

LGTM, with room to improve, I guess

@sneridagh
sneridagh requested a review from pnicolli October 9, 2026 19:40
@sneridagh sneridagh mentioned this pull request Oct 10, 2026
10 tasks
… feat/missing-widgets

* origin/feat/typed-widget-registry:
  Restore files the pre-commit hook reformatted
  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)
… feat/missing-widgets

* origin/feat/typed-widget-registry:
  Add the components changelog entry for the picker rename
Comment thread packages/cmsui/routes/vocabulary.tsx Outdated
const { data: vocabulary } = await cli.getVocabulary({
path: name,
title,
b_size: -1,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe this should be an optional parameter?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

-1 does not mean "give them all to me", no pagination? I can see the definition is optional in zod schema.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Now I get you, you were referring to the route, not the client! Implementing!

The route no longer returns every term: callers pass b_size, and without
it the content API returns its default batch. The select asks for all the
terms, and the array widget for 25 suggestions.

Co-Authored-By: Claude <noreply@anthropic.com>

This branch has not been deployed

No deployments
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