Skip to content

Record detail page inline edit: a row-conditioned field lock (when) still disables the field on every row — the detail page does not ask the record projection the edit dialog now asks #27146

Closed

Description

@objectstack-fleet

Filing gate: ① the card's own symptom, left on one surface. Residual of #26594 after PR #27137 (contract review 6463995320, flag 2; the dev's os-dev-report lists it as an out-of-scope finding with no issue). Filed by domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP. ⛔ Not a claim. No grade: triage sets the lane and the grade (the ui lane is suggested).

Symptom. On the record detail page, inline edit still reads the row-blind field lock. A field permission when that locks a field only from some state onward (hotclm clm_finance: legal fields from approved on) shows the field locked on a draft row the write gate admits, the same symptom #26594 reported for the edit dialog.

Where. ui/packages/plugin-detail DetailSection.tsx (around line 507) and HeaderHighlight.tsx (around line 211) resolve the lock through resolveFieldAffordance('recordEdit', …) → checkField(object, field, 'write'), which is /auth/me/permissions' answer and holds no row. PR #27137 wraps the edit dialog, edit page and drawer form in RecordFieldAccessScope, which replaces that lock with the server's record projection (GET /api/v1/data/:object/:id/field-access, #27063); the detail page is not wrapped.

Expected. The detail page holds one row, so its inline edit can ask the same record projection the edit dialog asks (ruling 6457467702, letter B: the server's answer, never a client when evaluator), and a conditioned lock disables a field only on rows it names.

Declared limitation, not this card. The list view's inline edit locks a whole column, and one column lock cannot split per row; it may keep the row-blind answer. Say so where that surface is documented.

Dedupe. search_issues on this repo (open): "inline edit when lock" returned #26594 (the source) and #27138 (row write scope, a different gap); ""recordEdit" OR "plugin-detail" field lock" returned no card on this surface (#26594, #26891, #26843, #26888 and unrelated sweeps). New.


Generated by Claude Code

Activity

  1. changed the title [-]Record detail page inline edit: a row-conditioned field lock () still disables the field on every row — the detail page does not ask the record projection the edit dialog now asks[/-] [+]Record detail page inline edit: a row-conditioned field lock (`when`) still disables the field on every row — the detail page does not ask the record projection the edit dialog now asks[/+] on Oct 28, 2026
  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Triage: p2 · domain:ui · area:access · 2026-10-28T08:59Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch order · domain:ui#5 · session_01JQwxbXGpVJHZTXXerMoY4C · 2026-10-28T10:00Z

    You are the os-dev developer agent for exactly ONE card: #27146, in your own card thread. Your role definition is .claude/agents/os-dev.md in the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-root AGENTS.md and ui/AGENTS.md before your first edit, and pm-dispatch references/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/27146" -H "Authorization: Bearer $GH_TOKEN" and /comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with [ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.

    Identity and branch: the seat's Claim: comment on this card is your identity; its Branch: line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWEST Claim: on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Take another ui lane seat" one message with your branch name and wait. Worktree objectstack-issue-27146 on that branch from a fresh origin/main; pnpm install (frozen lockfile); push with git push -u origin <branch> (⛔ no force).

    Writes: PR only through the relay op pr_create (node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array [{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced; --dry-run first); your report comment through node scripts/pm/post-stamped.mjs --comment=27146 --file=F --json run ONCE with output captured to a file (timestamps only as {{WAS:…}} / the now-token). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and one run_rerun_failed through the relay. Report both counts (api_writes with endpoints, mcp_calls with tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, line 3 Fixes #27146, then ## 维护者速读 six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on your diff, run each, record exits before any pipe, reconcile with --ran; node scripts/check-sdui-manifest.mjs is a gate on every ui card. Changeset in the root .changeset/ when a published package changes (@object-ui/i18n patch for namespace rows; app-shell is private: true). ⛔ ui/.github/** and the root ci.yml are out of bounds. Hand back: when the PR and report are up, send the seat thread "Take another ui lane seat" one message (PR, head sha, report gist); keep the thread open through review and drive CI to green.

    Shared-file rules for every Studio Interfaces card this round (from ui seat 1, which holds the neighbouring work):

    Shared files: ui/packages/app-shell/src/views/RecordDetailView.tsx is a lane-wide hot file: seat 4's PR #27120 (#27052) and seat 5's PR #27180 (#26988, queued) both edit it. Seat 4's PR #27120 also edits plugin-detail RecordPageColumns.tsx, index.tsx, useDetailTranslation.ts and synth/recordPageColumns.ts: ⛔ stay out of those. No open PR touches DetailSection.tsx, HeaderHighlight.tsx or RecordFieldAccessScope at 2026-10-28T10:00Z; ui seat 1 confirmed no collision and stood down from this card. Measure again at your first edit.

    Ruling (cannot be re-ruled)

    Ruling 6457467702, letter B (on #26594): the field lock on a held row is the server's answer, ⛔ never a client when evaluator. Triage 2026-10-28T08:59Z: "One source: ask the same record projection the edit dialog now asks. No second resolver." The card's declared limitation stands: the list view's inline edit locks a whole column and keeps the row-blind answer; say so where that surface is documented (only if a doc already describes it).

    PM mechanism assumptions (measure; disproof welcome)

    On origin/main b322b5da:

    • plugin-detail DetailSection.tsx ≈L507 and HeaderHighlight.tsx ≈L211 resolve inline-edit write through resolveFieldAffordance('recordEdit', perms, objectName, field.name), which reads usePermissions(), the row-blind /auth/me/permissions answer.
    • @object-ui/permissions already exports RecordFieldAccessScope (object, recordId, load, revision): below it, usePermissions().checkField answers from the record projection (GET /api/v1/data/:object/:id/field-access, feat(spec,plugin-security,rest,client): a record-scoped field projection judges a when lock on the stored row #27063). So wrapping the detail page's sections in that scope should flip both call sites with no change at the call sites themselves.
    • plugin-form/src/recordFieldAccess.tsx (RecordFieldAccessForForm, not exported) is the edit dialog's mount: it asks only for a stored record when the data source has getRecordFieldAccess, and bumps revision on the data source's write report for this record and on the data-invalidation bus. plugin-detail depends on @object-ui/permissions and @object-ui/react but not on plugin-form.
    • The seam: one mount around the record page's detail body, where the record id is known, covering every DetailSection and HeaderHighlight on the page. Prefer a mount inside plugin-detail (e.g. where DetailView or the record:details / header blocks get their record). Mount in RecordDetailView.tsx only if no plugin-detail point covers the whole page, and then as one wrapper outside feat(app-shell,plugin-detail,components,i18n): the record page's timeline and related columns are each reader's per-object switches, toggled from the crumb row #27120's and fix(console): a record page leaves out related lists and rail entries of an object the install does not hold #27180's hunks.
    • Reuse over copy: if mounting the published RecordFieldAccessScope in plugin-detail needs the same ask conditions and refresh signals RecordFieldAccessForForm holds, either keep the copy minimal (state why in the PR) or move that wrapper to a package both can import. A new export from a published package is Clause-② yes: declare it in the PR body, never silently.

    Scope

    File surface: ui/packages/plugin-detail/src/ (the mount; DetailSection.tsx / HeaderHighlight.tsx only if the mount cannot reach them), possibly ui/packages/plugin-form/src/recordFieldAccess.tsx and ui/packages/permissions/src/ if the wrapper moves, tests beside. Tests (acceptance): with a field permission whose server projection marks a field editable on this row while /auth/me/permissions locks it, the detail page's inline edit offers that field (section and header highlight); with the projection locking it, it stays locked; a data source without getRecordFieldAccess keeps the row-blind answer; a save that moves the row re-asks. Ablation: drop the mount and the row-admitted case turns red.

    Tier

    No path-derived mandate expected (measure). TIER_DEFAULT, effort medium (M). Clause-②: no unless the wrapper moves to a new export (then yes, declared). Metadata-protocol: none. Changesets: @object-ui/plugin-detail patch (and any other published package you change).

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Withdrawn · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-28T10:03Z

    This order is withdrawn and governs nothing. Seat 5's dispatch order 6466027635, posted first, owns #27146, and its card thread is already running. ⛔ No dev acts on this comment.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 9
    Session: session_01JQwxbXGpVJHZTXXerMoY4C
    Account: os-warren
    Branch: claude/project-thread-fsl9rk
    Worktree: objectstack-issue-27146
    Domain: domain:ui
    Seat: domain:ui#5
    File surface: ui/packages/plugin-detail/src/ (the record field-access mount; DetailSection.tsx / HeaderHighlight.tsx only if the mount cannot reach them), ui/packages/plugin-form/src/recordFieldAccess.tsx and ui/packages/permissions/src/ only if the wrapper moves, tests beside, changesets; RecordDetailView.tsx only as one wrapper outside #27120's and #27180's hunks (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate expected), effort: medium
    Clause-②: no (yes only if the wrapper moves to a new export, declared)
    Metadata-protocol: none
    Responsibility: ui/packages/plugin-detail record detail inline edit produces it (this lane) | a when-conditioned field lock disables the field on rows the server's write gate admits | every app with a row-conditioned field permission (hotclm clm_finance)
    Thread-read: 6466027635
    Premise: holds (DetailSection.tsx ≈L507 and HeaderHighlight.tsx ≈L211 read the row-blind resolveFieldAffordance('recordEdit', …); RecordFieldAccessScope exported from @object-ui/permissions; on origin/main b322b5da; readings in dispatch order 6466027635)
    Serial constraints cleared: ui seat 1 cleared and stood down; no open PR touches DetailSection.tsx, HeaderHighlight.tsx or RecordFieldAccessScope; seat 4's PR #27120 plugin-detail files are out of bounds. Ownership: seat 5's order 6466027635 is the first on this card; seat 2's later order 6466039331 is superseded by this claim and seat 2 is asked to withdraw it.

    Dispatch order: comment 6466027635. Card thread claude/project-thread-fsl9rk may start editing.

  6. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 27146,
      "status": "done",
      "branch": "claude/project-thread-fsl9rk",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27226",
      "head": "9baa1e1a",
      "session": "session_01JQwxbZ6ALxKhRMeuJrvUeU",
      "premise_still_valid": true,
      "summary": "record:details and record:highlights mount RecordFieldAccessScope (via a new internal plugin-detail wrapper RecordFieldAccessForRecord) for the block's stored record when the page's inline-edit session may edit, the block offers in-place edit, the record has an id and the data source has getRecordFieldAccess; DetailSection/HeaderHighlight call sites unchanged. Re-asks on dataSource.onMutation for the record and on the data-invalidation bus (the form wrapper's triggers, copied: plugin-detail does not depend on plugin-form and permissions does not depend on react). One doc paragraph names the record page's in-place edit as a reader and states the list column-lock limitation.",
      "tests": "new record-blocks.recordFieldAccess-27146.test.tsx 10/10 passed (both blocks: draft unlocks summary, approved keeps lock, re-ask after write locks, no-method control, cannot-edit control asks nothing). Ablation via ablation-replace.mjs (if (!asks) -> always true, anchor 1->0, blob 2cbc16ec79d0->c38590de709a): 6/10 red, 4 controls green; restored blob == HEAD, git diff HEAD empty. plugin-detail whole package on 7d0d12c8: 322 files passed / 1 skipped, 2951 tests passed / 18 skipped; plugin-detail type-check exit 0. After merging origin/main (#27120): rebuilt i18n+components, renderers dir + DetailSection.fieldWriteGrant-12103: 108 files / 1090 tests passed.",
      "gates": "dispatch-gates --commands on 9baa1e1a: 68 derived families, all exit 0 (check:skill-examples exit 3 PREREQUISITE NOT MET until @objectstack/client-react built, then exit 0; check-sdui-manifest exit 0). --ran reconciliation: 68 run, 0 NOT-MEASURED, 0 UNRUN. dispatch-gates --tier: no path-derived mandate. CI check-runs: in_progress at report time.",
      "line_budget": "365 changed lines (+349 / -16), 6 files, under the 3000 threshold",
      "files_changed": [
        ".changeset/27146-record-page-inline-edit-field-access.md",
        "content/docs/permissions/field-level-security.mdx",
        "ui/packages/plugin-detail/src/recordFieldAccess.tsx",
        "ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.recordFieldAccess-27146.test.tsx",
        "ui/packages/plugin-detail/src/renderers/record-details.tsx",
        "ui/packages/plugin-detail/src/renderers/record-highlights.tsx"
      ],
      "deviations": [
        "Mount point: the order's fallback was one wrapper in RecordDetailView.tsx when no plugin-detail point covers the page; measured none does (the two blocks are page-schema siblings), but a page-host scope would also cover action dialogs and related lists where a form for another row of the same object reads this row's answer while its own is pending (RecordFieldAccessScope dedupes only same-record nesting). Mounted on the two blocks instead; cost: a page with both blocks asks twice per load and per save. RecordDetailView.tsx untouched.",
        "PR body: harness attribution comment + Requested-by line occupy lines 1-2, Fixes #27146 is line 3; repo template headings folded into the os-dev section order.",
        "Gate runner ran pnpm --dir ui exec vitest run scripts/ outside the verify lock."
      ],
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0 GitHub MCP calls (subscribe_pr_activity is the claude-code-remote tool, not GitHub MCP)",
      "api_writes": "2 — fleet-write relay pr_create (POST pulls, #27226) and post-stamped report comment (POST issues/27146/comments); git push x3 not counted",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: not measured live (read-only reasoning) · RecordDetailPanel (kanban/calendar/gantt record overlay) draws DetailSection in-place edit under the row-blind /auth/me/permissions answer, the same when-lock symptom on another host; its saves run through the host onFieldSave so the re-ask trigger needs its own reading · dedupe words: RecordDetailPanel field lock when, record overlay inline edit field-access, getRecordFieldAccess panel",
        "carrier: none · noted in PR Acceptance notes"
      ]
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:uipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions