You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
A fourth loader in metadata-admin widgets.tsx swallows a failed field fetch into an empty field list — FieldSelectorWidget #5227
Found while implementing objectui#5170 (PR objectui#5226), which fixed the three option-picker loaders in ResourceEditPage.tsx and the Audit tab. Filed unassigned for PM triage — deliberately not fixed in that PR, see "Why not folded in" below.
The reading
packages/app-shell/src/views/metadata-admin/widgets.tsx, inside FieldSelectorWidget (locate by text, not by line number):
setLoading(true);fetch(`/api/v1/objects/${objectName}/fields`).then(r=>r.json()).then(data=>{setFields(data.fields||[]);setLoading(false);}).catch(err=>{console.error('Failed to load fields:',err);setFields([]);setLoading(false);});
This is the same defect class objectui#5170 and objectui#5169 were filed for: the catch writes setFields([]) — the exact value a successful response with no fields writes — and clears the loading flag. The picker then renders as a completed, empty field list.
It is a fourth loader, and it shares nothing with the three that card named:
It does not go through MetadataClient. It is a raw fetch to /api/v1/objects/:name/fields, a REST path that no other widget in this file uses, and it does not check r.ok — so a 4xx/5xx whose body happens to parse as JSON lands in the success branch with data.fields undefined, and || [] renders it as "no fields". That is a second, distinct way to reach the same false empty, and it is not reachable in the three loaders [finding] Three option-picker loaders in ResourceEditPage swallow a failed load into an empty option list, with no trace at all #5170 covered.
So it needs its own loader rewrite and its own render wiring, not a line in an existing one. #5170's declared completion scope was the three option-picker loaders and their render sites; this one is outside it and would have widened that PR's verification surface without being covered by either card's acceptance criteria.
Shape, if it is taken
The same one, already landed twice in this directory: the four-arm LoadState union in views/metadata-admin/loadState.ts (added by PR objectui#5226, mirroring the union objectui#5110 landed for the References panel), plus the shared PickerLoadFailure render that PR added for the other pickers. Both exist now, so this is wiring rather than design. Worth also fixing the missing r.ok check while in there, since the current code cannot tell a refusal from an answer at all.
Dedup
Searched open issues for FieldSelectorWidget by name, for the field-fetch loader by path, and for the failure-rendered-as-empty family in widgets.tsx; nothing covering it. Control: the same search family returns objectui#5040 and objectui#4871 (both real metadata-admin widget cards, and #4871 closed), so the zero is a measurement rather than a dead query. No duplicate.
Deliberately filed standalone rather than as a sub-issue of objectui#5170: #5170's completion scope is the three ResourceEditPage loaders, and its PR is up. This loader is not inside that scope — it only shares its class.
Related
objectui#5170 and objectui#5169 (same class, fixed together in PR objectui#5226) · objectui#5110 (the union this shape comes from) · ADR-0110 D3, "a miss and a fault are different facts"
Triage: promoted to pm:queue, type Bug. Same adjudicated family as #5169/#5170 (failure rendered as a completed empty state; ADR-0110 D3 "a miss and a fault are different facts") — inherits that ruling, no new decision needed. The fix shape is already landed twice in this directory (four-arm LoadState union + shared PickerLoadFailure from PR #5226), so this is wiring; include the missing r.ok check per the card. Filer's dedup is verified (control query returns real neighbors).
Size/model suggestion: S–M, mode:subagent. Serial note: touches views/metadata-admin/widgets.tsx — check for in-flight claims on the metadata-admin views before dispatch.
Claim: PM loop round 6
Session: session_01RV6yuVCxymHYE16PL9vQkE
Branch: claude/issue-5227-fieldselector-load-failure
Worktree: objectui-issue-5227
Domain: repo:objectui (execution seat)
File surface: packages/app-shell/src/views/metadata-admin/widgets.tsx (FieldSelectorWidget and its render site), plus a test in that directory. ⛔ OUT: packages/plugin-dashboard (#4032, dispatched this round), packages/types, packages/spec.
Container & model: M, mode:subagent, model: opus.
Clause-②: no — it turns a swallowed failure into a reported one. No accept-set change, no public surface moves.
Serial constraints cleared: metadata-admin is FREE — #5170+#5169's PR5226 merged at 18:18Z and nothing else open touches it. Re-verify at your claim.
The pattern you need is MERGED, one file over
PR5226 (#5170+#5169) landed the four-arm LoadState union in views/metadata-admin/loadState.ts and wired three picker loaders plus AuditPanel through it. Read that PR and that file first. This card is the fourth loader in the same directory, and it should end up looking like its neighbours rather than inventing a fifth shape.
The defect — and it has a second mouth the card names
FieldSelectorWidget runs its own raw fetch to /api/v1/objects/:name/fields — distinct plumbing from its neighbours: no MetadataClient, no WidgetContext, its own local state. Two ways to reach a false empty:
The catch writes setFields([]) + console.error — a failed load renders as "no fields", the exact defect class PR5226 just closed three doors down.
It never checks r.ok — so a non-ok response whose body happens to parse as JSON lands in the success branch. That one is worse than the catch path, because no error is even raised to swallow.
Fix both. A LoadState union that only guards the catch leaves mouth 2 wide open.
⛔ Premise re-check first
The card was filed at 16:06Z today by the #5170 dev while working one file away, so it is fresh — but widgets.tsxwas modified by PR5226 itself after that observation. Re-read the current file before planning: the loader may already sit beside the new union, and the render site may have changed shape. Take every anchor from the tree.
Scope
The loader and its render site. If the widget currently renders "no fields" copy, keep that sentence for the genuinely-empty case and make it reachable only from loaded — the same treatment PR5226 gave the five picker surfaces. ⛔ Do not invent new empty copy; ⛔ do not reword existing copy.
A failed load (network error) renders the failure state and not an empty field list.
A non-ok response with a JSON body does the same — this is the r.ok mouth, and it is the assertion most likely to be forgotten because it looks like a success to naive code.
A genuinely empty successful load still renders the empty state.
The loading flag clears in every arm; hold a promise so the loading arm is provably reachable and the "no longer loading" assertions are not vacuous.
Reverse-verify: restore the loader, keep the tests, predict the reds before running, report predicted vs observed. Note PR5226's dev found that Radix SelectValue does not render its placeholder in jsdom — if this widget uses one, measure what the closed trigger actually reads before asserting on copy, or you will write a test that passes for every arm.
Gates
Derive from your diff. scripts/pm/dispatch-gates.mjs is objectstack-only — here derive from .github/workflows and package.json. Expect Lint, Type Check, the shards, changeset gates, Control Byte Scan, and the i18n checks if you touch copy. Changeset required (@object-ui/app-shell); patch or minor, ⛔ never major.
⛔ Do not background a long check and end your turn. Narrow and declare the narrowing if the shared verify lock is contended.
Deliver
Draft PR against main, first body line Fixes #5227. Push the branch early. Return the JSON report and post it as a comment whose first line is the literal <!-- os-dev-report --> marker.
{
"issue": 5227,
"status": "done",
"branch": "claude/issue-5227-fieldselector-load-failure",
"pr": "https://github.com/objectstack-ai/objectui/pull/5263",
"premise_still_valid": true,
"summary": "Re-checked against the tree first: PR5226 modified widgets.tsx (it added PickerLoadFailure and CatalogErrors) but never touched FieldSelectorWidget's loader, so the card's reading held verbatim. Both mouths are closed through one door: the loader now throws on a rejected fetch AND on a non-ok response, and runs through the four-arm LoadState the sibling pickers use, so a fault can only reach the error arm. On failure the shared PickerLoadFailure block renders with the server's own message and the picker is replaced rather than decorated (the field-ref shape); the already-stored value stays visible and removable. The genuinely-empty case still renders the disabled picker unchanged and is now reachable only from 'loaded' — no copy added, none reworded. usePickerLoad moved from ResourceEditPage into loadState so this fourth loader reuses the shared hook instead of hand-rolling a fifth union in a second file (widgets.tsx cannot import ResourceEditPage — that module imports widgets); pure relocation, three existing callers unchanged. WidgetContext was not touched; #5228 is untouched and still open.",
"tests": "New: packages/app-shell/src/views/metadata-admin/FieldSelectorWidget.loadFailure.test.tsx, 9 cases (triple per mouth: rejected fetch / non-ok+JSON body / non-ok+unparseable body; genuinely-empty success still renders the picker; populated success; loading arm held then released in BOTH directions; idle fetches nothing; stored value survives a failure). All gates on final commit 50f7c7750. `pnpm exec vitest run packages/app-shell/src/views/metadata-admin/` -> 'Test Files 184 passed (184) / Tests 1896 passed | 1 skipped (1897)'. `pnpm --filter @object-ui/app-shell type-check` -> exit 0 (script name echoed; dependency closure built first via `pnpm --filter '@object-ui/app-shell^...' build`). `pnpm --filter @object-ui/app-shell lint` -> '2469 problems (0 errors, 2469 warnings)', warnings unchanged from baseline. check-control-bytes / check-changeset-presence / check-changeset-fixed / check-changeset-no-major / check-i18n-call-site-keys / check-i18n-en-drift all pass. REVERSE VERIFICATION, run on the committed fix by restoring only the pre-fix loader (`git checkout origin/main -- .../widgets.tsx`): predicted 5 red / 4 green BEFORE running, naming which five; observed exactly those five red ('mouth 1 rejected fetch', 'mouth 2 JSON body', 'mouth 2 no usable message', 'loading arm clears into the failure arm', 'does not also block authoring') and 4 green, all five failing on the same missing [data-testid=field-selector-load-failed]. Re-run after strengthening the assertions: same 5 red. Restore verified byte-identical (`git diff --stat HEAD` empty). NO REBUILD APPLIES to this ablation and none was done: the test imports './widgets' relatively inside the package, so the subject never resolves through a dependency's exports or dist. Also RE-MEASURED rather than inheriting PR5226's jsdom note: Radix SelectValue DOES render its placeholder here (unlike field-ref, where a matching __none__ item wins), but identically in the empty and populated arms ('Add fields…' both) — so copy separates completed from failed and cannot separate empty from populated; assertions read structure for the latter, copy for the former. The test file's rationale block was corrected to that measurement.",
"open_questions": [],
"out_of_scope_findings": []
}
Scope amendment (declared, per the bounded in-place rule). Final file surface is 6 files, 3 beyond the dispatched widgets.tsx + test:
views/metadata-admin/loadState.ts and views/metadata-admin/ResourceEditPage.tsx — usePickerLoad relocated, body unchanged. Justified by the hook's own docstring ("copy-pasted unions in one file is how the next drift starts") — a copy in a second file is exactly how this loader was missed. ResourceEditPage.pickerLoadFailure.test.tsx is the regression net and is green.
views/metadata-admin/selector-placeholder.i18n.test.tsx — a fixture triage, not a batch edit. Its stub spelled a successful field load as { json: async () => … } with no ok/status, which was only expressible while the loader ignored res.ok (mouth 2), so it correctly went red under the fix. Disposition: fix the stub (ok: true, status: 200); the placeholder assertion it exists for is untouched. Radius scan was by the rule's consumers, not by package: a repo-wide grep for anything rendering field-selector or stubbing this endpoint returns only this file plus SchemaForm.widgetLabelling.test.tsx, and the latter binds objectName: '' so it never leaves the idle arm.
Serial constraint re-verified at claim time: no open PR and no sibling worktree touches metadata-admin.
CI was not waited on, per the 2026-08-10 ruling — gate status at report time is in_progress.
Activity
os-support-ai commentedon Aug 18, 2026
Triage: promoted to
pm:queue, type Bug. Same adjudicated family as #5169/#5170 (failure rendered as a completed empty state; ADR-0110 D3 "a miss and a fault are different facts") — inherits that ruling, no new decision needed. The fix shape is already landed twice in this directory (four-armLoadStateunion + sharedPickerLoadFailurefrom PR #5226), so this is wiring; include the missingr.okcheck per the card. Filer's dedup is verified (control query returns real neighbors).Size/model suggestion: S–M,
mode:subagent. Serial note: touchesviews/metadata-admin/widgets.tsx— check for in-flight claims on the metadata-admin views before dispatch.Generated by Claude Code
os-support-ai commentedon Aug 18, 2026
Claim: PM loop round 6
Session:
session_01RV6yuVCxymHYE16PL9vQkEBranch:
claude/issue-5227-fieldselector-load-failureWorktree:
objectui-issue-5227Domain:
repo:objectui(execution seat)File surface:
packages/app-shell/src/views/metadata-admin/widgets.tsx(FieldSelectorWidgetand its render site), plus a test in that directory. ⛔ OUT:packages/plugin-dashboard(#4032, dispatched this round),packages/types,packages/spec.Container & model:
M,mode:subagent,model: opus.Clause-②: no — it turns a swallowed failure into a reported one. No accept-set change, no public surface moves.
Serial constraints cleared:
metadata-adminis FREE — #5170+#5169's PR5226 merged at 18:18Z and nothing else open touches it. Re-verify at your claim.The pattern you need is MERGED, one file over
PR5226 (#5170+#5169) landed the four-arm
LoadStateunion inviews/metadata-admin/loadState.tsand wired three picker loaders plusAuditPanelthrough it. Read that PR and that file first. This card is the fourth loader in the same directory, and it should end up looking like its neighbours rather than inventing a fifth shape.The defect — and it has a second mouth the card names
FieldSelectorWidgetruns its own raw fetch to/api/v1/objects/:name/fields— distinct plumbing from its neighbours: noMetadataClient, noWidgetContext, its own local state. Two ways to reach a false empty:catchwritessetFields([])+console.error— a failed load renders as "no fields", the exact defect class PR5226 just closed three doors down.r.ok— so a non-ok response whose body happens to parse as JSON lands in the success branch. That one is worse than thecatchpath, because no error is even raised to swallow.Fix both. A
LoadStateunion that only guards thecatchleaves mouth 2 wide open.⛔ Premise re-check first
The card was filed at 16:06Z today by the #5170 dev while working one file away, so it is fresh — but
widgets.tsxwas modified by PR5226 itself after that observation. Re-read the current file before planning: the loader may already sit beside the new union, and the render site may have changed shape. Take every anchor from the tree.Scope
loaded— the same treatment PR5226 gave the five picker surfaces. ⛔ Do not invent new empty copy; ⛔ do not reword existing copy.WidgetContextstill spells a failed option catalog as an empty array, with the fault on a side channel a new picker can forget to read #5228 (theWidgetContextboundary that spells a failed catalog as an empty array with the fault on a side channel). That is the structural fix for this whole family, it is filed separately, and the [finding] Three option-picker loaders in ResourceEditPage swallow a failed load into an empty option list, with no trace at all #5170 dev deliberately did not take it because it breaks ~17 fixture lines across 5 test files. If you find yourself editingWidgetContext, stop and report.Tests
Pin the triple PR5226 established, per mouth:
r.okmouth, and it is the assertion most likely to be forgotten because it looks like a success to naive code.Reverse-verify: restore the loader, keep the tests, predict the reds before running, report predicted vs observed. Note PR5226's dev found that Radix
SelectValuedoes not render its placeholder in jsdom — if this widget uses one, measure what the closed trigger actually reads before asserting on copy, or you will write a test that passes for every arm.Gates
Derive from your diff.
scripts/pm/dispatch-gates.mjsis objectstack-only — here derive from.github/workflowsandpackage.json. ExpectLint,Type Check, the shards, changeset gates,Control Byte Scan, and the i18n checks if you touch copy. Changeset required (@object-ui/app-shell);patchorminor, ⛔ nevermajor.⛔ Do not background a long check and end your turn. Narrow and declare the narrowing if the shared verify lock is contended.
Deliver
Draft PR against
main, first body lineFixes #5227. Push the branch early. Return the JSON report and post it as a comment whose first line is the literal<!-- os-dev-report -->marker.Generated by Claude Code
fix(app-shell): a failed field fetch is not an empty field list (#5227)
test(app-shell): a field-selector fetch stub must spell `ok` (#5227)
os-support-ai commentedon Aug 18, 2026
{ "issue": 5227, "status": "done", "branch": "claude/issue-5227-fieldselector-load-failure", "pr": "https://github.com/objectstack-ai/objectui/pull/5263", "premise_still_valid": true, "summary": "Re-checked against the tree first: PR5226 modified widgets.tsx (it added PickerLoadFailure and CatalogErrors) but never touched FieldSelectorWidget's loader, so the card's reading held verbatim. Both mouths are closed through one door: the loader now throws on a rejected fetch AND on a non-ok response, and runs through the four-arm LoadState the sibling pickers use, so a fault can only reach the error arm. On failure the shared PickerLoadFailure block renders with the server's own message and the picker is replaced rather than decorated (the field-ref shape); the already-stored value stays visible and removable. The genuinely-empty case still renders the disabled picker unchanged and is now reachable only from 'loaded' — no copy added, none reworded. usePickerLoad moved from ResourceEditPage into loadState so this fourth loader reuses the shared hook instead of hand-rolling a fifth union in a second file (widgets.tsx cannot import ResourceEditPage — that module imports widgets); pure relocation, three existing callers unchanged. WidgetContext was not touched; #5228 is untouched and still open.", "tests": "New: packages/app-shell/src/views/metadata-admin/FieldSelectorWidget.loadFailure.test.tsx, 9 cases (triple per mouth: rejected fetch / non-ok+JSON body / non-ok+unparseable body; genuinely-empty success still renders the picker; populated success; loading arm held then released in BOTH directions; idle fetches nothing; stored value survives a failure). All gates on final commit 50f7c7750. `pnpm exec vitest run packages/app-shell/src/views/metadata-admin/` -> 'Test Files 184 passed (184) / Tests 1896 passed | 1 skipped (1897)'. `pnpm --filter @object-ui/app-shell type-check` -> exit 0 (script name echoed; dependency closure built first via `pnpm --filter '@object-ui/app-shell^...' build`). `pnpm --filter @object-ui/app-shell lint` -> '2469 problems (0 errors, 2469 warnings)', warnings unchanged from baseline. check-control-bytes / check-changeset-presence / check-changeset-fixed / check-changeset-no-major / check-i18n-call-site-keys / check-i18n-en-drift all pass. REVERSE VERIFICATION, run on the committed fix by restoring only the pre-fix loader (`git checkout origin/main -- .../widgets.tsx`): predicted 5 red / 4 green BEFORE running, naming which five; observed exactly those five red ('mouth 1 rejected fetch', 'mouth 2 JSON body', 'mouth 2 no usable message', 'loading arm clears into the failure arm', 'does not also block authoring') and 4 green, all five failing on the same missing [data-testid=field-selector-load-failed]. Re-run after strengthening the assertions: same 5 red. Restore verified byte-identical (`git diff --stat HEAD` empty). NO REBUILD APPLIES to this ablation and none was done: the test imports './widgets' relatively inside the package, so the subject never resolves through a dependency's exports or dist. Also RE-MEASURED rather than inheriting PR5226's jsdom note: Radix SelectValue DOES render its placeholder here (unlike field-ref, where a matching __none__ item wins), but identically in the empty and populated arms ('Add fields…' both) — so copy separates completed from failed and cannot separate empty from populated; assertions read structure for the latter, copy for the former. The test file's rationale block was corrected to that measurement.", "open_questions": [], "out_of_scope_findings": [] }Scope amendment (declared, per the bounded in-place rule). Final file surface is 6 files, 3 beyond the dispatched
widgets.tsx+ test:views/metadata-admin/loadState.tsandviews/metadata-admin/ResourceEditPage.tsx—usePickerLoadrelocated, body unchanged. Justified by the hook's own docstring ("copy-pasted unions in one file is how the next drift starts") — a copy in a second file is exactly how this loader was missed.ResourceEditPage.pickerLoadFailure.test.tsxis the regression net and is green.views/metadata-admin/selector-placeholder.i18n.test.tsx— a fixture triage, not a batch edit. Its stub spelled a successful field load as{ json: async () => … }with nook/status, which was only expressible while the loader ignoredres.ok(mouth 2), so it correctly went red under the fix. Disposition: fix the stub (ok: true, status: 200); the placeholder assertion it exists for is untouched. Radius scan was by the rule's consumers, not by package: a repo-wide grep for anything renderingfield-selectoror stubbing this endpoint returns only this file plusSchemaForm.widgetLabelling.test.tsx, and the latter bindsobjectName: ''so it never leaves the idle arm.Serial constraint re-verified at claim time: no open PR and no sibling worktree touches
metadata-admin.CI was not waited on, per the 2026-08-10 ruling — gate status at report time is
in_progress.Generated by Claude Code
fix(app-shell): a failed field fetch in the field-selector picker is …