Repository navigation
Dynamic values: consumer API — standalone settings dialog, labels, portalContainer - #95
Merged
Merged
Conversation
Products that pick dynamic values from outside a field — a page builder's
dialog, a block editor's toolbar — had no way in: the settings modal and
its helpers were internal, and the API object needed a host field.
- index.tsx exports DynamicFieldSettings, DynamicValueSettings (a value's
settings sub-form on its own) and a `dynamicValues` namespace: createAPI,
buildGroupedChoices, stringify, parse, regex, defaultLabels.
- createDynamicValuesAPI({ types, categories, mode }) returns the same
shape as dynamicValuesAPI without a value to read or write.
- DynamicFieldSettings gains `modes` (one mode hides the Field Type
toggle), `extraGroups` (consumer-provided groups listed after the
registry; keys submitted unchanged, no settings), `labels` (every
string — the dialog was hardcoded English), `container` (which also
receives the interface wrapper classes while open, so the module's
styles and TUI's portal roots resolve inside it) and `context`; onSubmit
receives a second argument describing the pick. Blank settings no longer
travel in the token. Defaults keep today's behaviour; BaseWrapper is
unchanged.
- The settings loop moves out of the modal into DynamicValueSettings, and
the modal uses buildGroupedChoices instead of duplicating it. Both work
without a ControlContext above them (EnsureControlContext).
- Tests for the factory, the modal's new props and the sub-form; a
Storybook page with five stories under Dynamic Values / Settings dialog.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The consumer that motivated it (Certificates) keeps its document-scoped custom fields in its own picker, behind a Dynamic field / Custom field toggle of its own, so nothing lists client-side groups next to the registry. Without a use case the prop was the one client-side notion in a registry-driven dialog; the rest of the consumer API stands. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`container` was a name of this dialog's own, and mounting straight into the consumer's element meant adding the interface classes to someone else's DOM. The module already has the concept: renderField() accepts `portalContainer`, and usePortalContainer creates the interface wrapper inside it for every dialog. The standalone dialog now takes the same prop and goes through the same hook, which learns to accept the portal container and wrapper explicitly when there is no context above it. No consumer DOM is touched; the wrapper is the module's own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
KapplerJulien
marked this pull request as ready for review
September 21, 2026 16:14
nicolas-jaussaud
approved these changes
Sep 22, 2026
nicolas-jaussaud
left a comment
Contributor
There was a problem hiding this comment.
Looks good to me!
The only thing we might want to change is the format of raw returned by onSubmit() (see associated comment)
…HasSettings Review follow-ups (#95): - The dialog's onSubmit returned the token without its delimiters — an internal convenience of the ProseMirror editor, whose serialiser adds them back, that had become the public contract. Everywhere else the saved value carries [[ ]], so a consumer can store what it receives and hand it to render_value() as is. BaseWrapper no longer wraps; the editor strips before storing in its node; custom mode wraps what was typed unless the brackets were typed too. `meta` still carries the value name and settings for anyone who prefers structure to parsing. - valueHasSettings() from choices.ts replaces the dialog's own copy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
|
Merged despite failing checks. Tests are passing and issue is linked to #96 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Products that pick dynamic values from outside a field — a page builder's own dialog, a block editor's toolbar (Tangible Certificates' template builder is the first) — had no way in. The settings modal and its helpers were internal to the bundle, the API object needed a host field (
dynamicValuesAPI(value, setValue, …)), and the dialog's strings were hardcoded.Everything here is additive; defaults reproduce today's behaviour and
BaseWrapperis unchanged.What
Exports (
assets/src/index.tsx):DynamicFieldSettings,DynamicValueSettings(a value's settings sub-form on its own) and adynamicValuesnamespace —createAPI,buildGroupedChoices,stringify,parse,regex,defaultLabels.createDynamicValuesAPI({ types, categories, mode })returns the same shape asdynamicValuesAPIwith the value-bound members inert, so the modal, wrappers and choice builder don't care which one they got.DynamicFieldSettingspropsmodes—['builtin']hides the Field Type toggle (a consumer whose "custom" means something of its own).labels— every string of the dialog.portalContainer— the same proprenderField()accepts. The dialog goes throughusePortalContainerlike every other dialog in the module, which creates the interface wrapper (tf-interface,tf-context-<name>,tui-interface) inside it; the hook learns to take the portal container and wrapper explicitly when there is no context above it. Our styles are scoped under those classes and TUI portals the pickers' panels into the nearest.tui-interface, so the panel is styled by the module's own TUI copy (without it a consumer's TUI styles the panel; we hit a half-width search box).context— the tf-context name when rendered outside any field.onSubmit(raw, meta)—rawis the full[[name::setting=value]]token, the saved-value formatrender_value()takes (it used to omit the delimiters, an internal convenience of the editor's serialiser — review follow-up).metadescribes the pick:mode,value,label,group,settings.[[user_meta::meta_name=x]], not[[user_meta::source=::meta_name=x]](the parser defaults missing settings to''anyway).Refactor: the modal's settings loop moves into
DynamicValueSettings; the modal usesbuildGroupedChoicesinstead of its own copy. Both work without aControlContextabove them (EnsureControlContextprovides the same contextrenderField()builds).Consumer example
Verification
tests/jest/cases/dynamic/consumer-api.test.tsx(factory, modal props, portal wrapper, editing, sub-form); dynamic-values cases at 54 passing, full suite at 572.Dynamic Values / Settings dialog— autodocs plus four stories (default, built-in narrowed, custom labels, editing a reference). Built and driven headlessly.window.tangibleFields— grouped list, settings sub-form, edit-in-place, blank settings dropped.tscreports no errors in the changed files (the two inindex.tsxare pre-existing).Notes for review
extraGroupsprop (consumer-provided groups listed after the registry). Dropped: the consumer that motivated it keeps its document-scoped choices in its own picker, and without a use case it was the one client-side notion in a registry-driven dialog.portalContainerthrough the shared hook, so no consumer DOM is touched and the module keeps one concept for "where overlays go".storybook buildneeds Node ≥ 20.19 and two native bindingsnpm ciskips (@oxc-parser/binding-win32-x64-msvc,@rolldown/binding-win32-x64-msvc) — unrelated to this PR, noting it for whoever builds Storybook next.🤖 Generated with Claude Code