Skip to content

Give widgets a context instead of the route - #261

Open
sneridagh wants to merge 5 commits into
feat/widget-adaptersfrom
feat/widget-context
Open

sneridagh wants to merge 5 commits into
feat/widget-adaptersfrom
feat/widget-context

Conversation

@sneridagh

@sneridagh sneridagh commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

#243 step 5. Stacked on #260. Closes #163.

Problem

Widgets learned where the form is by reading the route:

  • ObjectBrowserWidget and QuerystringWidget read the edit route's loader data (useLoaderData<typeof editLoader>()). QuerystringWidget didn't even use the result.
  • ImageWidget guessed its path from window.location. On the add form, the URL is /@@add/<container>?type=…, so its object browser asked for /@objectBrowserWidget/@@add. plone.restapi rejected the path, the resource route threw, and the error page replaced the whole add form. That's Add Image Content tab crashes because the image field is rendered as a TextField #163: the Image add form, and any type with an image field, crashed on its Content tab.

Changes

  • WidgetContext (components/Form/WidgetContext.tsx): useWidgetContext() gives:

    • mode: add, edit or settings;
    • path: the edited object, or the container a new object is added to;
    • containerPath: where new objects go (the parent when editing, the container when adding).

    ContentForm provides it from a new path prop, set by the edit and add routes. Control panels provide the site root. Block settings render inside the content form, so they inherit its context. Without a provider (Storybook), it's the site root.

  • ObjectBrowserWidget starts browsing from path, and its selection follows the field's value when it changes outside the browser (before, it only read it on mount).

  • QuerystringWidget: the unused edit-route read is removed.

  • ImageWidget browses from path and uploads to containerPath; explicit currentPath / uploadPath props still win. Its extras (image_field, image_scales) were dropped by the form, which only forwards value. With the new extraFields widget option, it writes them to the sibling fields of the same name, in one change. The image block's url field declares it, so an image picked in the sidebar keeps its scales, as one picked in the block's own placeholder does. Without the option (content forms, the block's placeholder), nothing extra is written.

  • RecurrenceWidget reads its own value prop instead of reading its field from the form. It still reads start / end through the form hooks, which is how widgets read sibling fields.

  • The @objectBrowserWidget loader:

    • It reads the browsed folder's breadcrumbs from the root middleware, which already loaded the folder in the same request with breadcrumbs expanded. They're just as fresh, and the separate getBreadcrumbs call (and its TODO) is gone.
    • A search the API rejects (4xx) gives an empty listing instead of the error page; 5xx errors still throw. A folder that doesn't exist is rejected earlier, by the middleware, before the loader runs. The Add Image Content tab crashes because the image field is rendered as a TextField #163 fix is the widget context, which no longer requests such paths.
    • Unit tests cover the loader, and the acceptance test checks that the browser shows the folder in its breadcrumbs.
  • Docs:

    • the guide's new section "What a widget may read besides its props" (form values through @plone/helpers hooks, location through useWidgetContext);
    • a versionadded step in the upgrade guide;
    • a new "Core widgets" reference (docs/reference/widgets.md): for each registered widget, what it's registered for, the value it reads and writes, and its widget options, including extraFields with the image block's schema as the example. It also documents the object browser's value shape, and that Volto's link / image modes and allowExternals (still set by some block schemas) aren't supported yet.

Tests

  • New acceptance test: in a page holding an image, the Image add form shows its image field, and its picker lists the container's image. It fails with the previous ImageWidget (the Add Image Content tab crashes because the image field is rendered as a TextField #163 crash).
  • Unit tests:
    • WidgetContext: path rules for each mode, and the default without a provider;
    • ImageWidget extras: the declared sibling fields are written (an external URL clears stale scales), and nothing is written without extraFields;
    • the recurrence tests pass the value as a prop.
  • Full acceptance suite (local, dev mode): 211 passed, 0 failed. cmsui: 244 unit tests pass; blocks: 29. Type checks, lint, the docs build and the optimizeDeps audit are clean.

Widgets learned where the form is from the edit route's loader data or
the URL. The image widget guessed its path from the URL, which on the
add form is /@@add/..., so its object browser asked the API for /@@add
and the error page replaced the form (#163).

Forms now provide a widget context: mode, path (the edited object, or
the container a new object goes to), and containerPath (where uploads
go). ContentForm takes the path from its route, control panels use the
site root, and block settings inherit the content form's.

- ObjectBrowserWidget starts from the context, and follows its field's
  value when it changes outside the browser.
- QuerystringWidget no longer reads the edit route's data.
- ImageWidget browses and uploads from the context. With the
  extraFields widget option it stores a picked image's details in
  sibling fields; the image block's url field uses it, so images picked
  in the sidebar keep their scales.
- RecurrenceWidget reads its own value prop.
- The object browser listing returns no items for a path the API
  rejects, instead of failing the page.

Closes #163
Refs #243
Adds a Core widgets reference page: for each widget cmsui registers,
what it is registered for, the value it reads and writes, and its
widget options, including the image widget's new extraFields, with the
image block's schema as the example. It also documents the object
browser's value (the selected items, with their selectedItemAttrs), and
that Volto's link and image modes and allowExternals aren't supported
yet.
@sneridagh

Copy link
Copy Markdown
Member Author

LGTM

Resolves the TODO in the @objectBrowserWidget loader. The root
middleware already loads the browsed folder for this request, with its
breadcrumbs expanded, so they are as fresh as the loader's separate
getBreadcrumbs call, which is dropped.

The loader's error handling only ever covered the search: if the folder
doesn't exist, the middleware fails the request before the loader runs.
Its comment now says so.

Adds unit tests for the loader, and checks in the acceptance test that
the browser shows the folder in its breadcrumbs.
…widget-context

* origin/feat/widget-adapters:
  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)
…widget-context

* origin/feat/widget-adapters:
  Add the components changelog entry for the picker rename
@sneridagh
sneridagh added this pull request to stack #270 October 10, 2026 22:48

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.

1 participant