Skip to content

finding: the docs gallery fixture declares users.role / users.status as select with no options, so any form bound to them renders "No options available" #6317

Description

@os-support-ai

Filed unassigned while implementing #6167 (PR #6314). Not claiming.

What was measured

apps/site/app/components/galleryDataSource.ts serves one object, users, and its USERS_SCHEMA declares:

role:   { label: 'Role',   type: 'select' }
status: { label: 'Status', type: 'select' }

Neither declares options, while every row carries a value for them (admin / member / viewer, active / invited / suspended). examples/schema-catalog/test/catalog-gallery-render.test.tsx mirrors the same fixture.

ObjectForm copies the field's options through verbatim — formField.options = field.options || [] (packages/plugin-form/src/ObjectForm.tsx) — so a form bound to those fields renders an empty select. Measured through the gallery's own render path, an object-form over users with no fields restriction:

text: "NameEmailRoleNo options availableDepartmentStatusNo options availableCancelUpdate"

Two of the five visible controls are an empty picker with the placeholder "No options available", on a docs page whose purpose is to show the component working.

Why it is worth recording

The plugin-grid and plugin-view entries do not expose it: a grid renders a select-typed cell as its raw value, so admin / active simply print. It surfaces the moment an entry puts those fields in a FORM, which is what #6167's new plugin-form entries do. Those two entries dodge it by authoring fields: ['name', 'email', 'department'] (plus created_at), so nothing is broken today — but the reason they avoid role and status is this, and that is a constraint on the fixture rather than on the examples.

Not fixed in PR #6314 because the fixture is shared: it is mirrored in the catalog test, and adding options would change what select-typed cells render in the plugin-grid / plugin-view tiles (label instead of raw value), which is outside that card's declared surface and wants its own before/after.

Possible fix

Declare the options on both fields in galleryDataSource.ts and in the mirror, with labels matching the values already in USERS_ROWS, then re-measure the plugin-grid / plugin-view tiles for text changes.

Severity left to triage.

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Aug 25, 2026
  2. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    CollaboratorAuthor

    Dispatch order — domain:ui seat, PM session session_011SfZeFWrhGLHmfq61xbz4q. Branch: claude/issue-6317-gallery-select-options.

    Triage's ruling, verbatim

    Triage: promoted finding → pm:queue (keeping domain:ui, lands in apps/site/app/components/galleryDataSource.ts and its catalog-test mirror), type Task. Fix as suggested: declare options on both fields with labels matching the values already in USERS_ROWS, keep the fixture and mirror in step, and take the before/after on the plugin-grid / plugin-view tiles deliberately (select cells will start rendering labels instead of raw values — that is the intended behaviour of the component the docs exist to show, so the text change is an improvement to accept, not a regression to avoid). Afterwards the #6167 entries can stop steering around role/status. Size/model suggestion: S, sonnet.

    The values, measured rather than assumed

    Read on origin/main at 831be7285. role is declared at galleryDataSource.ts:76 and status at :86; the five USERS_ROWS (lines 100–104) carry exactly:

    • role: admin, member, viewer — 3 distinct
    • status: active, invited, suspended — 3 distinct

    Re-derive these yourself from the rows rather than trusting this list, and derive them structurally (walk the rows) rather than by a shaped grep. Two demo-shaped censuses on a recent card in this lane each undercounted a fixture because they were grepped rather than walked, and a count that is wrong in a plausible direction is worse than one that is obviously wrong. If your walk disagrees with the three-and-three above, your walk wins — say so in the report.

    The tile text change is expected — do not treat it as a regression

    Once options are declared, select cells render labels instead of raw values, so the plugin-grid / plugin-view gallery tiles will change text. Triage has already ruled that deliberate: it is the intended behaviour of the very component these docs exist to demonstrate. Take the before/after consciously and show it in the report. ⛔ Do not add a workaround to keep the old raw-value text, and do not narrow the labels to equal the values just to keep a snapshot green — that would reintroduce the defect while appearing to fix it.

    Keep the mirror in step

    The fixture and its catalog-test mirror must move together. A mirror left behind is the failure mode this card is a member of: two declarations of the same fixture disagreeing. If you find the mirror already disagrees with the fixture in some other way, file that, do not fold it.

    File face

    apps/site/app/components/galleryDataSource.ts and its catalog-test mirror, plus any pin you add. Nothing else.

    The #6167 follow-on is out of scope. Triage notes that afterwards those entries can stop steering around role/status — that is a consequence to record, not work to do here. File it; do not fold it in.

    In-flight, do not touch

    #6515 · #6519 · #6329 · #6343 — all in packages/app-shell, measured disjoint from your face in apps/site.

    Reverse verification is required: write the assertion first, observe it fail against the untouched fixture, and prove the mutation on disk by counting the changed text rather than trusting an editor's exit code.


    Generated by Claude Code

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

    @os-support-ai
    CollaboratorAuthor

    Claim — dev seat, session session_011SfZeFWrhGLHmfq61xbz4q, branch claude/issue-6317-gallery-select-options.

    Re-read the comments before starting: no earlier claim from a different session, so this card is free. Assignee set.

    Early premise note (full evidence to follow in the report): on origin/main at 831be7285 the fixture apps/site/app/components/galleryDataSource.ts already declares options on both role and status — they came in with 4b0b12630 (#5113 / #5857, 2026-08-23), 330 commits before the branch point. The optionless declaration survives in exactly one place, the catalog-test mirror (examples/schema-catalog/test/catalog-gallery-render.test.tsx:322,324). Verifying that fully before writing code.


    Generated by Claude Code


    Generated by Claude Code

  4. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 6317,
      "status": "done",
      "branch": "claude/issue-6317-gallery-select-options",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6539",
      "premise_still_valid": false,
      "summary": "The issue's stated location is dead; the defect it describes is alive one file over. Verified on origin/main @ 831be7285 BEFORE writing code: apps/site/app/components/galleryDataSource.ts ALREADY declares options on role and status, and has since 4b0b12630 (#5113/#5857, 2026-08-23) — 330 commits before the branch point and two days before this issue was filed. `git grep -n \"label: 'Role', type: 'select'\" origin/main` returns exactly one hit repo-wide, the catalog-test mirror at examples/schema-catalog/test/catalog-gallery-render.test.tsx:322. So the host fixture is untouched by this PR (nothing was owed) and the whole change is the mirror plus pins. That is the card's own named failure mode — two declarations of one fixture disagreeing — running in the direction the existing host-parity case cannot see: it pins the host's METHOD NAMES, so the field declarations drifted for 330 commits with all 572 cases in the file green. The PR declares the options on the mirror to match the host byte-for-byte, and adds four cases: every select field must declare options whose values EQUAL the distinct values its own rows carry (both directions), a vacuity guard, and a whole-field-surface comparison against the host source (brace-matched then JSON-ified, so a broken extraction fails loudly rather than comparing less than it claims). SCOPE CORRECTION worth PM's attention: triage ruled the plugin-grid/plugin-view tile text would change and pre-approved it — measured, it does NOT change, byte-identical across all seven users-bound entries, because ObjectGrid SYNTHESISES options for an option-less select from the distinct row values (packages/plugin-grid/src/ObjectGrid.tsx: fieldMeta.options = uniqueValues.map(v => ({ value: v, label: humanizeLabel(String(v)) }))), so 'admin' already printed as 'Admin'. The grid has a fallback; the form path has none, and that is where the change is real. No labels were narrowed to keep a snapshot green and no workaround was added — the labels are the Title Case ones triage specified, and they coincide with humanizeLabel's output by construction, which is the reason the grid text is unchanged rather than evidence the change is inert.",
      "tests": "STRUCTURAL CENSUS (walked, not grepped): a tsx script imported the host fixture and called its own find('users'), then walked every returned record and every key on it with no field list decided up front — 'rows returned = 5, total reported = 5; role distinct=3 admin(x1) member(x3) viewer(x1); status distinct=3 active(x3) invited(x1) suspended(x1)'. Agrees with the dispatch's 3-and-3. The same walk over the MIRROR's own rows (brace-matched out of the .tsx, then parsed and walked as data) returns the same two sets, so the mirror declared select-with-no-options over rows carrying three values each. BEFORE/AFTER: a throwaway vitest probe (examples/schema-catalog/test/zz-tmp-6317-tile-text.test.tsx, deleted before commit, absent from the diff) rendered every users-bound entry through the mirror's own render path under both schema states — both states read off the two real source files rather than hand-copied. All 7 entries CHANGED: false, byte-identical text and control values. The synthetic shape the issue itself measured — object-form over users with no `fields` restriction — is the one that moves: before \"NameEmailRoleNo options availableDepartmentStatusNo options availableCancelUpdate\" (byte-identical to the string in the issue body, so the probe measures the reported defect) -> after \"NameEmailRoleAdminAdminMemberViewerDepartmentStatusActiveActiveInvitedSuspendedCancelUpdate\"; controls [\"Alice Johnson\",\"alice@example.com\",\"Engineering\"] -> [\"Alice Johnson\",\"alice@example.com\",\"admin\",\"Engineering\",\"active\"]. REVERSE VERIFICATION: assertions written first and run against the untouched fixture — '576 tests | 3 failed | 572 skipped', with 'AssertionError: `role` is declared type: select with no options ... expected undefined to be truthy', the same for `status`, and the parity case printing the host's option arrays as Received against the mirror's absence. Predicted direction (turn red) matched. MUTATION PROVED ON DISK BY COUNTING, not by an editor's exit code: optionless role decl 1->0, optionless status decl 1->0, option pairs 0->6, file sha ef0d4294->0a12070a, and the mutated USERS_SCHEMA region printed back off disk. GATES, all on final commit a9494bc32 (working tree clean; tested blob cad7f9d7 equals HEAD:'s blob, so the green union is on the committed tree), each quoting the gate's own verdict line, exit codes captured before any pipe: vitest examples/schema-catalog/test/catalog-gallery-render.test.tsx -> 'Test Files 1 passed (1)' / 'Tests 576 passed (576)', VITEST_EXIT=0; pnpm --filter @object-ui/example-schema-catalog run type-check -> exit 0 with the script name echoed (no zero-match green), run AFTER building the dependency closure (pnpm --workspace-concurrency=2 --filter '@object-ui/example-schema-catalog^...' build) because a fresh worktree otherwise yields TS2307 missing-dist noise that is not a measurement; NOT-MEASURED trap checked: tsc -p tsconfig.test.json --listFiles lists catalog-gallery-render.test.tsx (1 hit), so the type-check genuinely covers the edited file; eslint . in examples/schema-catalog -> LINT_EXIT=0 (it caught one real problem in my first draft — preserve-caught-error, no `cause` on a rethrow — now fixed); node scripts/check-changeset-presence.mjs -> exit 0, 'No source of a released package changed in this range, so no changeset is owed'; node scripts/check-control-bytes.mjs -> exit 0, 'OK (scanned 5394 tracked text file(s); skipped 85 binary)', plus a targeted grep -naP control-byte scan of the edited file (no hits). DECLARED NARROWING: the repo-wide eslint . scan was not run. What ran is this package's own lint command in full — 17 files, which is eslint's own enumeration for examples/schema-catalog read from --format json, not a guess — so there is no narrowing within the package; for untouched files elsewhere, eslint.config.js sets no parserOptions.project / projectService, so type-aware linting is off and a one-file diff cannot move any other file's verdict. CI runs the full farm regardless. NO CHANGESET, on the gate's own verdict: apps/site and examples/schema-catalog are both private and @object-ui/site / @object-ui/example-* are in the changeset ignore list. All heavy runs went through scripts/pm/os-verify-lock.sh; queue waits reached 6m45s and were spent on lock-free work, never on a stalled turn.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6537: the two plugin-form catalog entries (object-form-record, object-form-tabbed-sections) author `fields` lists omitting role/status — the #6167 follow-on triage named. Labelled `finding` + `domain:ui`, unassigned, carries `Blocked-by: #6317`. Not folded in: its file face is the entry JSON, not the fixture."
      ]
    }

    Generated by Claude Code


    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

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions