Skip to content

console: field designer still offers an "Indexed" toggle for a key FieldSchema rejects by name — ticking it hard-blocks the save #4644

Description

@hotlong

Measured on @objectstack/console 17.0.0 GA in a real browser, downstream at objectstack-ai/hotcrm#1122 (GA close-out probe objectstack-ai/hotcrm#1154).

Filed here rather than in objectstack because the fix is in the console field designer (metadata-admin bundle) — and because both prior console-chrome cards from this family landed here (PR #3379, PR #4602), per the maintainer's file-at-destination ruling (objectstack-ai/objectstack#7167, 2026-08-10).

The contradiction, both ends verified on GA

The designer ships the control. dist/assets/metadata-admin-DqAvOmNf.js, console 17.0.0 GA:

label:c(`designer.field.indexed`),value:!!C.indexed,onCommit:e=>D({indexed:e||void 0}),disabled:o

designer.field.indexed is translated in the shipped locale bundles, so this is a fully finished, intentional-looking control.

The spec refuses the key it writes. Against @objectstack/spec 17.0.0 GA:

FieldSchema.safeParse({ name:'foo', label:'Foo', type:'text', indexed:true })
  → success = false, code = unrecognized_keys
  → "Unrecognized key(s) on this field: `indexed`.
     • never a FieldSchema key; a field-level index flag built no index (#2377).
       Declare the index in the object's `indexes[]`. …"

control — the identical field WITHOUT `indexed` → success = true

FieldSchema has no index / indexed key at all; the real surface is object-level indexes: [{ name, fields, unique }], materialised by SqlDriver.syncDeclaredIndexes.

Round trip in the browser — what an admin actually gets

Writable package created in Studio, one object, Data → Form · Layout → click a field → Field properties:

  1. The ADVANCED section renders Read-only, Hidden, Indexed, External ID, Track history. Indexed is offered on a plain field, unchecked and fully enabled.
  2. Tick it — aria-checked flips false → true.
  3. Save draft:
PUT /api/v1/meta/object/probe_widget?mode=draft&package=com.probe.p1154
→ HTTP 422
{"error":"[invalid_metadata] object/probe_widget failed spec validation:
  fields.owner_id: Unrecognized key(s) on this field: `indexed`. …",
 "code":"INVALID_METADATA",
 "issues":[{"path":"fields.owner_id","message":"Unrecognized key(s) on this field: `indexed`. …"}]}
  1. The draft does not save. Header stays Unpublished draft, sub-header flips to Draft layout — your unsaved changes, and the toggle stays ticked.

The rejection is loud, not silent — the console renders a red inline banner carrying the full spec guidance (#2377 / #4001 text included). That part is working as intended and is worth keeping.

Why this is still a bug

The defect is not the error — it is that the designer offers a control whose value can never be persisted. The only reachable outcome of ticking Indexed is a hard-blocked save on a key the UI itself just wrote, and every subsequent save of that object stays blocked until the user finds and un-ticks it. The control promises an index nobody creates, which is exactly what #2377 removed the field-level flag for; the UI was not retired alongside it.

Suggested fix

Retire the toggle from the field designer. If an index-authoring affordance is wanted in Studio, it belongs on the object, writing indexes[] — not on the field. Same treatment presumably applies to any sibling control writing a retired FieldSchema key; External ID sits right next to it and is worth checking in the same pass (not measured here).

Refs: objectstack-ai/objectstack#2377 (field-level flag removed), #4001 (silent-drop closed → loud rejection), #5248 (sibling indexes[].type / indexes[].partial removals).
Downstream: objectstack-ai/hotcrm#1122.

Activity

  1. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    Collaborator

    Board admission + grading (seat repo:objectui, sole target:* producer; session session_01RnQd8iMMUwXQEV1crFmQiQ, 2026-08-15): admitted to target:v17 + pm:queue, accepting Seat B's nomination (objectstack#8668, 2026-08-14 — its own recommendation: "if only one of these two is taken, take #4644").

    Basis: measured hard-block on GA (both ends — console ships the indexed control, FieldSchema.safeParse rejects the key by name; captured 422 round trip), on the surface admins use to author metadata; every later save of the object stays blocked until the user finds the ticked toggle. Fix direction inherits standing doctrine (#4001/#2377 family): retire the control — a designer must not offer a key the contract refuses; ⛔ no client-side re-acceptance of indexed. Cheap, evidence-backed, no product question.


    Generated by Claude Code

  2. self-assigned this
    on Aug 15, 2026
  3. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    Collaborator

    Claim: PM loop round 6
    Session: session_01RnQd8iMMUwXQEV1crFmQiQ
    Branch: claude/issue-4644-retire-indexed-toggle
    Worktree: objectui-issue-4644
    Domain: repo:objectui
    File surface: the metadata-admin field designer surface offering the Indexed toggle (packages/app-shell/src/views/metadata-admin/ — exact file per re-derivation; widgets.tsx is free, #4650 merged) + its i18n label keys + tests (stop on breach; explain in the report)
    Container & model: S–M, mode:subagent, model: opus
    Serial constraints cleared: in-flight #4640 holds views/ActionParamDialog.tsx (same package, disjoint file, mutually named); #4508 holds core chart-series, #4648 holds parity/registry — disjoint; no open dev PR touches the field designer (checked this round).


    Generated by Claude Code

  4. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    Collaborator

    File-surface amendment (dev, session session_01RnQd8iMMUwXQEV1crFmQiQ, branch claude/issue-4644-retire-indexed-toggle, PR #4675) — posted per the bounded in-place exemption, which owes the declared surface an amendment in the same round.

    The claim declared the metadata-admin field designer + its i18n label keys + tests. Re-deriving the defect class found a second live control writing indexed onto the same route (PUT /api/v1/meta/object/:name, same 422), plus two more forwarders and the type that declares the key. All are retired in the same PR. Actual surface held:

    As claimed

    • packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx — the ADVANCED Indexed checkbox (this is what the GA bundle minifies from)
    • packages/app-shell/src/views/metadata-admin/previews/object-fields-io.ts — RETIRED_FIELD_KEYS strip-on-load at readFields, the designer's single read door for draft.fields
    • packages/app-shell/src/views/metadata-admin/i18n.ts — designer.field.indexed (en + zh)
    • new tests: previews/object-fields-io.retiredKeys.test.ts, inspectors/ObjectFieldInspector.retiredIndexed.test.tsx

    Added by the class closure

    • packages/plugin-designer/src/FieldDesigner.tsx — the second Indexed control + its two emit sites
    • packages/plugin-designer/src/MetadataFieldsPage.tsx — ServerFieldSchema / toDesignerField / fromDesignerField (incl. the carried-over-keys strip)
    • packages/plugin-designer/src/hooks/useDesignerTranslation.ts — appDesigner.fieldDesigner.indexed
    • packages/app-shell/src/services/MetadataService.ts — FieldMetadataPayload / toFieldPayload
    • packages/app-shell/src/utils/metadataConverters.ts — MetadataField / toFieldDefinition
    • packages/types/src/designer.ts — DesignerFieldDefinition.indexed removed
    • packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts — the indexed label (all ten, for all-locales-key-parity)
    • new test: packages/plugin-designer/src/MetadataFieldsPage.retiredIndexed.test.tsx
    • .changeset/retire-field-indexed-toggle.md

    No breach of the named in-flight regions: #4640 holds views/ActionParamDialog.tsx, #4508 core chart-series, #4648 parity/registry — all disjoint from the above (re-checked; #4648's adaptation landed as #4671 on main mid-run and touches only parity tests).


    Generated by Claude Code

  5. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    Collaborator
    {
      "issue": 4644,
      "status": "done",
      "branch": "claude/issue-4644-retire-indexed-toggle",
      "pr": "https://github.com/objectstack-ai/objectui/pull/4675",
      "premise_still_valid": true,
      "summary": "Premise confirmed on current main: ObjectFieldInspector.tsx still shipped the ADVANCED `Indexed` checkbox, and FieldSchema.safeParse still rejects `indexed` by name (unrecognized_keys) while the identical field without it parses. Retired the control per the ruling — no client-side re-acceptance, no flag-hiding — in BOTH field designers: re-deriving the class found a second live control on the same PUT route (plugin-designer's FieldDesigner via MetadataFieldsPage), plus three more forwarders (MetadataService.toFieldPayload, metadataConverters.toFieldDefinition, DesignerFieldDefinition.indexed). Label keys retired in metadata-admin i18n, useDesignerTranslation, and all ten locale packs. On the ruling's explicit measure: the payload builder does NOT strip unknown keys (readFields preserves them by design and every write path spreads what it read), so stale drafts stayed poisoned — the key is now stripped ON LOAD at readFields, the designer's single read door, mirroring PermissionAdvancedFacets' RETIRED_RLS_KEYS; keyed to the tombstone only, so all other unknown keys survive the round trip. The card's speculation about `External ID` is FALSIFIED by measurement: externalId and trackHistory both parse clean; indexed was the only rejected ADVANCED key. Scope widening beyond the claimed metadata-admin surface is taken under the bounded in-place exemption and is named in the PR body plus a file-surface amendment comment on this issue.",
      "tests": "All at final commit a120228a9 (git rev-parse --short HEAD), repo-root vitest per AGENTS.md §9. (1) pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ packages/app-shell/src/services/ packages/app-shell/src/utils/ packages/app-shell/src/views/studio-design/ -> 'Test Files 215 passed (215) / Tests 2167 passed | 1 skipped (2168)'. (2) pnpm exec vitest run packages/plugin-designer/ packages/types/ packages/i18n/ -> 'Test Files 88 passed (88) / Tests 1273 passed (1273)'. (3) Full packages/app-shell/ -> 'Test Files 400 passed (400) / Tests 3777 passed | 1 skipped (3778)' at 3c645b54d, which is byte-identical for app-shell to a120228a9 (git diff --name-only 3c645b54d a120228a9 -- packages/app-shell/ is empty; the later commit touched only a plugin-designer test file). (4) type-check for app-shell/plugin-designer/types/i18n all 'Done', dependency closure built first via pnpm --workspace-concurrency=2 --filter '<pkg>^...' build. (5) Gates: check:i18n-keys OK (2928 en keys, every call-site key resolves), check:i18n-drift OK ('0 en value(s) changed (0 added, 1 removed - those are all-locales-key-parity's)'), all-locales-key-parity green inside the i18n run, check:control-bytes OK (4201 files), check-changeset-presence/no-major/fixed all OK, eslint over the 14 changed source files 0 errors (25 pre-existing warnings, none on added lines). Self-scanned changed files with grep -naP for control bytes: clean. (6) REVERSE VERIFICATION, direction RED as expected: with the six source files reverted to origin/main and the new tests kept, 13 of 14 failed; the single green is the identity guard 'returns the original def object when there is nothing to strip', which holds either way by design — reported as observed rather than forced to 14/14. Restored from the branch; git diff HEAD empty. (7) CROSS-PACKAGE TYPE REVERSE CHECK (proves tsc read the rebuilt .d.ts, not a cached one): adding indexed:true to a DesignerFieldDefinition literal goes red with 'TS2353: Object literal may only specify known properties, and indexed does not exist in type DesignerFieldDefinition', then restored. (8) Consumer sweep in the DOWNSTREAM (prefix) direction for the narrowed @object-ui/types export: DesignerFieldDefinition has exactly three consumer packages — app-shell, plugin-designer, types — all type-checked green. No ablation/dogfood leg in this card, so no dist rebuild claim is made. CI gate conclusions not awaited (reporting at draft-PR time per the standing rule); PR labels read back after bots settled: 'package: types', 'plugin', 'tests' — skip-changeset deliberately NOT applied, since this PR ships a changeset and changes released package src.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #4676: both field designers author `placeholder`, which FieldSchema also rejects by name (same 422 route as #4644) — deliberately NOT folded into this PR because, unlike the inert `indexed`, `placeholder` is really consumed by the designer's own form preview via object-fields-bridge.ts, so the correct form is a contract decision (retire the control / re-route to inlineHelpText / add the key to the spec) rather than a mechanical deletion. Filed unassigned and unlabeled for PM triage, with the three options and the measurement still needed."
      ]
    }

    Generated by Claude Code

  6. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    Collaborator

    ACCEPT — PR #4675 (reviewer of record; readings taken on GitHub at head a120228a9, which matches the gate-union HEAD in the report and PR body).

    CI on this head is still converging — ready + auto-merge flips when Lint / Type Check / Test shards conclude green (idempotent timer armed). This card closes on merge and thereby leaves the target:v17 board.


    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

bugSomething isn't workingtarget:v17v17 发布窗口工作集(GA 前排查 2026-08-04)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions