Skip to content

feat(plugin-detail): record:path stage press moves the record when allowStageChange is set - #28303

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-7k5czo
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-7k5czo

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #28199
Refs #28061

Clause-②: yes
Metadata-protocol: none

维护者速读

What changes

  • record:path honours allowStageChange. With allowStageChange: true, each stage other than the current one carries a real button (accessible name "Move to STAGE") inside its existing listitem, on both the desktop and the phone row. A press opens a confirmation (AlertDialog); confirming writes { [statusField]: value } and re-reads the record. Without the key, or with false, the DOM is the readout it was.
  • Who can press (useStageChange, new renderers/recordPathStageChange.tsx): the record is stored and its data source can update; the record page's edit session allows editing the record (useInlineEdit().canEdit, the page host's record-level verdict); the status field is not read-only, computed or a system field; the caller's recordEdit field grant holds (resolveFieldAffordance). RecordPathRenderer now mounts RecordFieldAccessForRecord around the body when the key is on, so that grant is asked of this stored row, as record:details and record:highlights ask it. While the edit session holds an inline draft, the path is a readout again.
  • One update path. The write goes through updateVia (now exported from InlineEditSaveBar.tsx, not from the package index), the function the inline-edit save bar uses, with the record's updated_at as ifMatch.
  • Refusals in place (stageRefusalLines), under the rails, once for both rows: a CONCURRENT_UPDATE 409 gets a fixed line and the record is re-read; a producer-marked userMessage verbatim; a field-scoped refusal (VALIDATION_FAILED fields[], the shape an illegal state_machine transition and a missing required field arrive in) one line per entry, an entry about another field prefixed with its label; a permission refusal the delegated-scope reason or a fixed line; anything else the server message or a fixed line. The next press clears it.
  • Published. allowStageChange joins the block's registry inputs (boolean, renderer-accurate description), so sdui.manifest.json, scripts/sdui-manifest.record.json and the generated component reference (content/docs/ui/components/record.mdx, index.mdx) are regenerated, and the UNPUBLISHED_EXEMPTIONS entry record:path.allowStageChange is deleted from registry-inputs-spec-parity.test.ts.
  • Copy. Seven detail.pathStageChange* keys in all ten packs and in DETAIL_DEFAULT_TRANSLATIONS.
  • Changeset. .changeset/28199-record-path-stage-change.md, @object-ui/plugin-detail and @object-ui/i18n patch.

Why

The spec half (#28198) declared RecordPathProps.allowStageChange, and the console registry carried it as unpublished because nothing read it. Authors who set it got the read-only rail. This is the console half that #28061 names.

Tests

ui/packages/plugin-detail/src/renderers/__tests__/record-path.stageChange-28199.test.tsx, 14 cases:

  • the press writes: buttons on every non-current stage in both rows; confirm calls update('crm_quote', 'q1', { status: 'in_review' }, { ifMatch: VERSION }) and refresh; cancel writes nothing;
  • a refusal renders in place: an illegal state_machine transition (the server sentence alone), a required field elsewhere (label-prefixed), a 403 (fixed line, not the raw text), a stale token (fixed line plus a re-read), and the next press clears it;
  • the readout stays: the key absent (control, for a reader who could edit), false, the page's edit session refusing the record, the field edit grant denied, a read-only status field, an inline draft in progress.

Ablations (one-shot, each through scripts/ablation-replace.mjs, which proved the mutation landed and restored git diff HEAD empty; the subject resolves to source through the vitest alias, so no rebuild applies):

Mutation in recordPathStageChange.tsx Red
ifMatch token dropped from the update the write case (1 of 14)
allowStageChange removed from the verdict both opt-in controls (2 of 14)
!inline.editing removed the inline-draft case (1 of 14)
resolveFieldAffordance(...) removed the field-grant case (1 of 14)
setRefusal(...) removed all five refusal cases (5 of 14)

Acceptance notes

  • "Reachable" means "drawn and not current." The path does not read the object's state_machine transitions to hide stages: that would be a second evaluator of a rule the server owns. Every drawn stage the record is not on is pressable, and an illegal move comes back as the server's refusal, shown in place. Stages a record's own columns rule out (option visibleWhen) are already not drawn.
  • Inline draft. While the record's inline edit holds a draft the stages are not pressable, so a stage write never races the draft's Save. Said here because the card did not ask for it.
  • Concurrency. A 409 is not routed to ConcurrentUpdateDialog's overwrite: a stage move is one field, and the re-read record plus a second press is the whole recovery.
  • File surface. Beyond the claim's surface, the diff carries the generated half of a registry change that root AGENTS.md requires ("A registry change has a second half"): sdui.manifest.json, scripts/sdui-manifest.record.json, and the generated reference pages content/docs/ui/components/record.mdx and index.mdx (node scripts/gen-sdui-reference.mjs, gated by check:sdui-reference). No hand edit in any of them.
  • Clause-②: yes, measured from the diff: the block's published inputs gain a key (the manifest and the component reference grow by one input). The claim recorded no as the seat's expectation; the dispatch order says publishing the input counts.
  • ui/AGENTS.md ✨ Set up Copilot instructions #2 (package README and docs): the docs page for this input is the generated component reference (content/docs/ui/components/record.mdx), regenerated here. plugin-detail's README covers record:details / record:highlights / record:quick_actions / record:related_list binding and says nothing about record:path today; it is outside the claimed surface and left untouched — the seat may want a README line.

Verification

Every reading below is on head 2837cf76 unless it says otherwise.

Tests (local, under the shared verify lock)
  • pnpm --dir ui exec vitest run packages/i18n/ packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx apps/console/src/__tests__/registry-inputs-spec-parity.test.ts packages/plugin-detail/ on bc74bc54 → Test Files 442 passed, 1 skipped (443); Tests 4842 passed, 31 skipped. The only later commit types two object-definition reads in the new module.
  • pnpm --dir ui exec vitest run packages/plugin-detail/src/renderers/__tests__/record-path.stageChange-28199.test.tsx → 14 passed on 2837cf76.
  • pnpm --filter @object-ui/plugin-detail run type-check and pnpm --filter @object-ui/i18n run type-check → exit 0 (full declaration build of the closure).
  • eslint on the changed plugin-detail files → 0 errors; the new module adds no warning.
Derived gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 76 families; every one run with stdin closed and its exit code recorded; --ran reconciliation: 76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN.
  • On the first run (head bc74bc54) check:skill-examples answered exit 3 (PREREQUISITE NOT MET: no packages/spec declarations) and exit 0 after a declaration build; on 2837cf76 all 76 answered exit 0 in one pass.
  • node scripts/check-sdui-manifest.mjs → exit 0 (present and intact, recorded by the in-tree producer).
  • pnpm check:new-line-citations (ui) → 0 new citations.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec untouched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (Changed files linted locally; the repo-wide run is CI's.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer brief above is Chinese by rule).

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 2837cf76ffd8be61c34f21a8cba653ef0632fec3 · 2026-10-31T04:14Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 2837cf76ffd8be61c34f21a8cba653ef0632fec3
Local-runs: none

Inputs: card #28199 (body and all five comments: triage 6528549948, the unblock 6533351162, the seat's claim 6537955118, the dev report 6538697224, the claim amendment 6538708352 which flips Clause-② to yes and accepts the generated registry half, the updateVia module export and the ten packs), PR #28303 (body, no comments, the file list of 22 files, the diff of 1001 lines against main: the hand-written files read whole, the ten packs and the generated registry copies by their hunks, the four commits from merge-base 6285203), the 66 check-runs on this head (read 2026-10-31T04:18Z: all 66 runs completed, none failed, none cancelled, none in progress; success: Build & E2E UI, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Dogfood Regression Gate, Governed Surface Queue Guard, Lint & Repo Gates, Lint UI, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Test UI (all 16), Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; skipped: 18.x upgrade harness (opt-in), Auto Label, Build Core, Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), UI Published Artifacts), and the tree at this head for RecordFieldAccessForRecord (recordFieldAccess.tsx:94, with enabled), resolveFieldAffordance (affordanceGrants.ts:320), updateVia (InlineEditSaveBar.tsx, the update / updateOne / patch fallback with ifMatch), isConcurrentUpdateError (ConcurrentUpdateDialog.tsx:243), RecordContextValue (dataSource, objectSchema, refresh, recordId) and the spec's RecordPathProps.allowStageChange pins (component.test.ts:2605 onward). The head is mergeable and clean against today's main; none of the files main moved since the merge-base is in this diff.

① Derived judgments

  • The card's four wants, as ordered - right. (1) With allowStageChange: true, every stage but the current one is a real button (accessible name "Move to STAGE") for a reader who may edit statusField on this record: useStageChange requires the page edit session's canEdit, no live inline draft, a stored record with an updating data source, a status field that is not read-only, computed or a non-editable system field, and the caller's recordEdit field grant through resolveFieldAffordance, asked row-scoped because RecordPathRenderer now mounts RecordFieldAccessForRecord with enabled only behind the opt-in. Absent or false, the rail is the The record page's lifecycle path looks like a control and is inert: stageField renders as a segmented button group with no handler, no cursor, and no tab stop #11243 readout, byte for byte. (2) A press opens an AlertDialog, and confirm writes { statusField: value } through updateVia, the same update path record:details' save bar uses, with the record's updated_at as ifMatch, then re-reads. (3) A refusal renders in place under the rails as role="alert": a 409 concurrent update says the record changed and re-reads it; a producer's userMessage verbatim; VALIDATION_FAILED field entries one per line, the status field's or _record's alone and any other field prefixed by its label; a permission refusal as the delegation-scope reason or a fixed line; else the server's message or a fixed line. No server change. (4) allowStageChange is published in the path registry entry, the UNPUBLISHED_EXEMPTIONS entry is deleted, and the manifest, its record and the component reference are regenerated.
  • The tests the card asked for - right. The new suite has 14 cases over the press writing the update with ifMatch, each refusal class in place, a reader without edit rights, a live inline draft, and the absent-key and false controls; five ablations each shown red in the dev report.
  • Accept-set: nothing an author writes moves - right. The spec key landed with feat(spec): a record:path block may opt in to allowStageChange, so a stage can be pressed to move the record there #28198; this PR reads it. Metadata-protocol: none is right.
  • Copy - seven detail.pathStageChange* keys in all ten packs and in DETAIL_DEFAULT_TRANSLATIONS, so the provider-less host and the mirror pin agree; the confirmation reuses the existing detail.cancel.
  • Check-runs as gate verdicts - read as above; every run on this head completed and none is red, cancelled or in progress at the read.

② Semver level

  • @object-ui/plugin-detail patch, @object-ui/i18n patch; Clause-②: yes; Metadata-protocol: none - accepted. A declared-but-inert key becomes live behind its own opt-in and a registry input is published; no released signature changes (updateVia is a module export the package index does not re-export). The Clause-② line carries no arm word; the measure is a widening (a key that did nothing now does what the spec says), and the PR-body guards accepted the line as written, so this is a wording note, not a finding.

③ Boundary flags

Verdict reasoning. The diff does exactly the console half #28061 named: an opt-in press gated by the reader's row-scoped edit right, one update path with the concurrency token, the server's refusal shown in place, the input published with its exemption cleared, every want pinned with an ablation shown red; nothing an author writes moves and nothing released changes signature.

Implemented-by: claude/project-thread-7k5czo
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibzC6RxMfB59EsgAoNN

席位核对

ACCEPT · seat domain:ui#1 · head 2837cf76 · 2026-10-31T04:22Z

席位意见:记录路径组件声明 allowStageChange: true 后,有权编辑状态字段的读者可以直接点非当前阶段来推进记录。点击后先弹确认框,确认后走和详情页保存栏相同的更新路径,并带上 updated_at 做并发校验。服务器拒绝时,原因显示在路径下方:并发冲突会提示并重新读取记录,校验失败按字段逐行列出。哪个阶段可以去由服务器的状态机判断,前端只决定谁能点。未声明时,路径和以前一样只读。已按扩面申报 Clause-②: yes。

  1. Shape: Fixes #28199, draft, base main, 22 files, +691/−40. Adds recordPathStageChange.tsx (useStageChange). updateVia becomes a module export of InlineEditSaveBar.tsx, but the package index does not re-export it. Publishes the allowStageChange registry input, clears the UNPUBLISHED_EXEMPTIONS entry, regenerates sdui.manifest.json and the component reference, and adds ten i18n packs. This matches claim amendment 6538708352.
  2. Seat read: the press is gated by the edit session's canEdit, no live inline draft, a status field that is not read-only or computed, and the row-scoped recordEdit grant. RecordFieldAccessForRecord mounts only behind the opt-in, so a rail without allowStageChange does no extra work.
  3. Contract review: PASS at this head, record 6538910207 (TIER_DEFAULT), by the contract-review lane, not the drafter. No should-fix. The review's wording note on the Clause-② line carrying no arm word is noted, and the measure is a widening. Its README flag (plugin-detail says nothing about record:path) is outside the claimed surface, and the seat carries it as a docs follow-up.
  4. Gates / check-runs: on 2837cf76: 54 success, 14 skipped, 0 failure. One run is still in progress after a label event, and auto-merge waits for it.
  5. Landing: check-governed-merges --pr 28303 reports NOT governed, 731 changed lines ≤ 3000, which includes the sdui.manifest.json regeneration. The seat readies the PR and arms auto-merge, on the maintainer's 「按 pm-dispatch 审过的pr你可以自主落地」 (seat thread, 2026-10-30T14:19Z).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 04:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit be25e2d Oct 31, 2026
70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-7k5czo branch October 31, 2026 05:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Record path: a stage press requests that status when record:path sets allowStageChange (console half of #28061)

1 participant