Repository navigation
feat(app-shell): Studio uses shared picklists — select-field picker, served options left out of object saves, read-only picklist page (objectui#10202) - #11669
Conversation
…served options left out of object saves, read-only picklist page (objectui#10202) - ObjectFieldInspector: an "Options from" picker over the picklists the runtime serves (GET /meta/picklist). Choosing one sets `picklist` and removes `options`; a bound field shows the list's values read-only and offers no inline options editor; a retype to a non-option type (spec SINGLE_OPTION_TYPES / MULTI_OPTION_TYPES) drops the binding; the default value picker offers the list's values. - dropServedPicklistOptions: the served -> authored conversion. Every picklist-bound field leaves without its resolved `options`; nothing else is touched. Applied as the `object` resource's `fromDraft` (metadata-admin designer) and at the Studio data page's two object PUTs (autosave, column reorder). The metadata-admin live Zod pass judges the `fromDraft` body. - PicklistPreview: the read-only detail view of the package-owned kind — label, name, description, owning package, options, and the `picklistExtensions` other packages declare (read from GET /meta/package). Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…e picklist's own only (objectui#10202) `GET /meta/picklist` serves a list's own options, not the ones other packages add with `picklistExtensions`. After a save the designer's draft carries no served copy, so the bound view lists the roster's own values; it now says the extensions' values are offered too. Measured live: after an edit of an object bound to a list with one extension, the view listed three of the field's four runtime values with nothing saying so. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
… of its own (objectui#10202) PicklistPreview.tsx exported `extensionsOf` beside the component, which react-refresh/only-export-components flags; and its pin imported an unused `waitFor` (object-ui/no-unused-imports, an error). The reader moves unchanged to previews/picklist-extensions.ts. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ce per mount, not keyed on a memoised client (objectui#10202) usePickerLoad re-runs its request whenever the loader's identity changes and re-enters `loading` as it does. The roster loader was a useMemo keyed on `client`, so a host whose client is not referentially stable re-minted it on every render: a render loop. Measured: ObjectFieldInspector.optionLabel.test.tsx, whose mock returns a fresh client per call, held a worker at 100% CPU (killed at 120 s); it passes with this change. Both loaders now live in useState, whose identity React guarantees (AGENTS.md #10). Pinned by a fresh-client mount that counts one roster read. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
ACCEPT: PR objectui#11669, head
|
Fixes #10202
Clause-②: no
Studio now reads shared picklists, the objectui half of objectstack#18164 phase 2, scoped by ruling 6006062645 (A′) items (a)–(c). Item (d), the studio-authoring checklist item, lives in objectstack; its ready-to-file text is in the
os-dev-reportcomment on objectui#10202, for the seat to file.Dispatched work: claim 6007167258, session
https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL.What changes
(a) "Use picklist" in the select-field editor (
inspectors/ObjectFieldInspector.tsx, shared by the metadata-admin object designer and the Studio data page).select,radio,multiselectorcheckboxesfield, an Options from picker (the existingInspectorSelectField, a closed list) offers "This field's own options" and every picklistGET /meta/picklistserves, by name. The roster loads throughusePickerLoad, with its loaders held in state once per mount (3114a9f), so a failed read is its own state: the picker says so and still offers the field's own options. A stored name the served list no longer carries is drawn flagged "(not found)".picklistand removesoptionsoutright. Choosing the field's own options removespicklistand the served copy of the options.GET /meta/picklistdoes not merge extensions). The default-value picker offers the same values.SINGLE_OPTION_TYPES∪MULTI_OPTION_TYPES, not a re-listed set.(b) Served options left out of the object PUT (
metadata-admin/picklist-binding.ts,dropServedPicklistOptions).picklistis set leaves withoutoptions, not only the edited field. Nothing else is touched: a field withoutpicklistkeeps its inlineoptionsbyte-identical, and so does every other key. The function is pure and returns the same reference when there is nothing to drop.objectresource'sfromDraftinservices/builtinComponents.tsx, thepageresource'spageSaveBodyprecedent. The Studio data page applies it at its two object PUTs that re-send the served buffer: the draft autosave and the column reorder.fromDraft(draft)in edit mode, the bodydoSavesends. Before, it showed "picklistandoptionscannot both be declared" on load for every object with a served bound field, measured below.fromDraftis read from the registry bytype, so no memoised identity keys the effect.MetadataClient.savecannot tell a served copy from an author who wrote both keys, and the second has to stay a loud refusal. The writers this PR does not touch are listed under Acceptance notes.(c) The picklist page is read-only (
previews/PicklistPreview.tsx, registered inpreviews/index.ts; reader inpreviews/picklist-extensions.ts)./meta/typesrow declaresallowRuntimeCreate: falseandallowOrgOverride: false, and the generic list and edit pages already honour it. So this adds no read-only mechanism of its own: it registers the detail view._packageId), each option with its label and value, and each extension under the package that declares it. It offers no create, edit or delete.GET /meta/package, where each manifest carries itspicklistExtensionsas authored. The list's own read does not serve them. A failed package read says the extensions are unknown, never that there are none.Live measurement
objectstack
mainfaf8dce4in its own worktree, showcase booted--fresh. The console was served from this branch. The stock showcase declares no picklist, so a local, uncommitted fixture was added to that worktree only: picklistshowcase_priority_tier(Gold, Silver, Bronze), onepicklistExtensionsentry (Platinum), and one bound field onshowcase_account. Writable objectsacme_account(fieldtierbound) andacme_contact(fieldsegmentinline) were created in a runtime packagecom.acme.crm.GET /meta/picklist,GET /meta/picklist/NAME_packageId; no extensionGET /meta/object/acme_account/layerseffective.fields.tierpicklist+ 4 resolved options (Platinum included)INVALID_METADATAatfields.tier.optionstier.optionsremovedpicklistandoptionscannot both be declared…" on loadtiersent as{type, label, picklist}; inlinestatusoptions intact; no live issue on loadacme_contact.segment: Options from → Priority Tierpicklistand nooptions; "Add value" 1 → 0acme_account: add a field (autosave)tierwithoutoptionsTests
New pins, each read off the wire or the real component:
metadata-admin/picklist-binding.test.tsinspectors/ObjectFieldInspector.usePicklist-10202.test.tsxmetadata-admin/ResourceEditPage.picklistServedOptions-10202.test.tsx, real edit page, realMetadataClient, a transport that refuses the served pair as the door doesstudio-design/DataPillar.picklistServedOptions-10202.test.tsx, both Studio PUTspreviews/PicklistPreview.test.tsx, fed the measured package-row shapemetadata-admin/ResourceEditPage.picklistReadOnly-10202.test.tsx, the generic route offers no create, edit or deleteAblations through objectstack's
scripts/ablation-replace.mjs(WRAP mode, run from the committed trees3019cff/1d8c6c2, and A9 on3114a9f). Each one: landing proven by anchor count and blob change; restore proven by blob == HEAD and an emptygit diff HEAD.objectfromDraftexpected 422 to be 200atfields.tier.optionsandfields.regions.options), the advisory pin, the registration pinobjDraftbodyoptionspicklistextendoptionsfrom every fielduseMemousePicklistpins and the existingObjectFieldInspector.optionLabelsuite hang (killed bytimeout 120, exit 124)The first A3 attempt was a no-op: its replacement text was a substring of the anchor, so the tool refused before running anything. It was re-run with a distinct marker.
Gates
Read on
3114a9f, the final head, unless a line says otherwise (corrected by the seat at landing; this section was first written at46320ed, while the locked runs were still queued). Each exit code was written to a file before any pipe.pnpm exec vitest run --maxWorkers=2 FILE…): 30 passed (30), exit 0.check:*gates, exit 0 each:i18n-designer-parity,i18n-keys,i18n-drift,designer-field-key-parity,metadata-write-doors(17 doors, 3 object-capable, all guarded),new-line-citations(0 new),control-bytes,test-path-roots,vi-mock-specifiers,vi-mock-inherit,vi-mock-override-shape,changeset-claims,pending-changeset-literals,unreferenced-sources,spec-symbols,installed-pin-claims,phantom-deps.scripts/check-changeset-presence.mjsandcheck-changeset-no-major.mjsalso exit 0.3114a9f:@object-ui/app-shelltype-check,lintandbuild: exit 0 each.pnpm exec vitest run packages/app-shell/, as two--shardruns: 1/2Test Files 509 passed | 1 skipped (510),Tests 4662 passed | 9 skipped (4671); 2/2Test Files 510 passed (510),Tests 5423 passed (5423). Together 1019 of 1020 files passed and 1 skipped.@object-ui/app-shell^...closure build ran on the pre-merge tree: exit 0. The merge brought no declaration change.3114a9fbuild:.d.tsreachable frompackages/app-shell/dist/index.d.tsthrough relative specifiers: 150 files.dropServedPicklistOptions,PicklistPreview,extensionsOf,shownOptions,PicklistExtensionRow,ShownOption,usePicklistRoster,fetchPicklistRoster,PicklistSourceField,BoundPicklistOptions,takesPicklistorPicklistRosterEntry.registerMetadataResourceandSupportedLocaleare both found.i18n.d.tsis reachable, and it carries none of the new copy keys. Control: an existing key is absent from it too; the new key is present in the emittedi18n.js.useMemokeyed on the client, andusePickerLoadre-enteredloadingon each new loader.3114a9fholds the loaders in state once per mount and pins it (A9); the two shards above ran after that fix.check:eager-closure. It weighs a built console bundle; CI's performance-budget job runs it. This PR adds the picklist preview to the eager metadata-admin registrations.Acceptance notes
?surface=owd:overview), changeacme_account's sharing model and Save. The PUT came back 422 atfields.tier.options, with the server's prescription on screen. By reading, the same shape exists inviews/runtime-metadata-persistence.ts(two object saves), plugin-designerMetadataFieldsPage/MetadataObjectsPage, andMetadataService.saveObject/saveFields/saveMetadataItem. None of them is touched by this PR, so none is changed here. The fix shape is the samedropServedPicklistOptionsat each writer that seeds from a served read.options. Runtime forms show the list's options (measured above).Generated by Claude Code