You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 5fa9375
Browse filesBrowse the repository at this point in the historyBrowse files
feat(plugin-detail): the default record page lets an editor advance the stage from the path (#30221)
Fixes#30075
Clause-②: no
Metadata-protocol: none
## 维护者速读
-
改了什么:没有自定义页面的默认记录页上,有权编辑阶段字段的用户可以直接点击阶段路径推进记录(先弹确认框);状态机不允许到达的阶段不再显示为可点击。
- 为什么改:推进商机阶段是 CRM 最高频的更新,原来要「编辑 → 改阶段 → 更新」三步;裁决选方案 A,不新增任何元数据键。
- 风险与回滚:风险是误点,已有二次确认;保存仍走普通记录更新,校验与状态机由服务端判定,被拒时就地显示原因。回滚即还原
`buildDefaultPageSchema.ts` 里那一个 `allowStageChange: true`。
-
先看:`ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts`(默认页写入开关的那一行);`ui/packages/plugin-detail/src/renderers/recordPathStageChange.tsx`(`admits`,按状态机收窄可点阶段);`ui/packages/plugin-detail/src/renderers/record-path.tsx`(每个阶段是否渲染为按钮)。
- 两分钟验证:`pnpm dev:crm` 后以管理员打开任一商机记录页,点击下一个阶段,应弹出「Move this record to
…?」确认框,确认后阶段前移;以只读用户打开同一页,阶段路径不可点击。
- 需要你做的:无,席位落地。
## What changes
The landing point is the producer of the default record page,
`buildDefaultHighlights` in
`ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts`: the
`record:path` node it emits now carries `allowStageChange: true`, the
existing spec key (`RecordPathProps.allowStageChange`). No key is added
and nothing under `packages/**` is edited. The console's record page
(`RecordDetailView`) renders that synthesized page whenever no page is
assigned, so this is where "a default record page" is built.
Who may press is unchanged and decided entirely by the existing opt-in
(`useStageChange`): the page's edit session may edit the record, the
stage field is not read-only, computed or a system field, and the
caller's field edit grant holds for the stored row. Everyone else, and
an editor whose inline edit holds unsaved changes, keeps the read-only
path. Every press opens a confirmation, then saves through the record's
ordinary update with the record version (`updateVia`, the inline save
bar's path), so the server's validation and `state_machine` rule judge
it; a refusal is shown under the path.
One change to the renderer, made because the default page now puts the
press in front of every app: a stage the stage field's blocking
`state_machine` cannot reach from the stored value is drawn as a
readout, not a button. It reads the same `stateMachineNextValues` the
record form, the grid's inline editor and the board use, so the path
offers exactly the moves the edit dialog offers. A field without such a
rule offers every stage, and the server still judges each write. This
applies to every path with the opt-in, authored ones included.
A declared page keeps its own setting: an assigned page writes its own
`record:path` node and never reaches the synthesizer, and an authored
`highlights` slot replaces the default strip, so its `true`, `false` or
absent key is kept as written (pinned below).
`record-path.inertReadout.test.tsx` encodes the 2026-08-23 "separate
card" ruling. Its assertions are unchanged: every mount there is a node
without the opt-in, which is still the readout. Only its docblock
changes, to say that the separate card landed as the opt-in and that the
default page now carries it, so that file's pins now describe authored
pages that leave the key out or set it `false`.
## Tests
New pins, as two top-level `describe` blocks in the existing shared
harness `record-path.stageChange-28199.test.tsx`. The synthesized node
is mounted as the JSON document `buildDefaultPageSchema` emits, through
the real `SchemaRenderer`:
1. The synthesized page writes `allowStageChange: true` on its path
node.
2. An editor gets a press on each other stage; nothing is written before
the confirmation; confirming writes `{ status: 'in_review' }` with the
record version and re-reads.
3. A reader the page may not let edit the record keeps the read-only
path; so does a reader without the field edit grant.
4. An authored `highlights` slot keeps `allowStageChange: true`,
`false`, and the key left out, each rendered accordingly.
5. With a blocking `state_machine` (`draft` to `in_review` or `declined`
only), `Accepted` is a readout and not a button. A warning-severity
machine does not narrow the press (control).
6. A move the server still refuses for a required field elsewhere shows
`Amount: is required ...` under the path; the current stage and the
record are unchanged.
The card listed the fill-in path (a stage move that needs another
required field) as not measured. Measured: the press refuses in place
and names the missing field by label (pin 6, client half). The server
half is the existing objectql pin `validate-update-stored-row.test.ts`
(b): an update whose patch turns a `requiredWhen` true without supplying
the field answers `VALIDATION_FAILED` with `fields: [{ field: 'note',
code: 'required' }]` and leaves the row unchanged (18/18 green on this
branch). There is no in-place fill-in; the reader opens the edit dialog
to supply the field. Nothing worse than #28303's refusal in place was
found.
Ablation (each through `scripts/ablation-replace.mjs`, which checks the
mutation landed and restores to the HEAD blob; the subjects resolve to
source, so no rebuild is involved):
- Removing `allowStageChange: true` from the synthesizer fails 5 of 24
in the harness (pins 1, 2, 5 and its control, 6). Restored, blob equals
HEAD.
- Removing `&& stageChange.admits(o.stage.value)` from the renderer
fails exactly pin 5 (1 of 24). Restored, blob equals HEAD. A first
attempt with the anchor ending in a bare `;` was refused by the tool
(its replacement count could not move) and changed nothing; the anchor
was respelled.
<details><summary>Test runs</summary>
- `vitest run --isolate packages/plugin-detail/` (whole package,
isolated as the CI shard runs it): Test Files 369 passed, 1 skipped
(370); Tests 3270 passed, 18 skipped. This ran before `origin/main` was
merged in; the merge changed nothing in these files.
- `record-path.stageChange-28199.test.tsx` verbose: 24/24, including the
10 new `#30075` cases.
- `record-path*` + `synth/`: 30 files, 320 tests passed.
- `packages/objectql` `validate-update-stored-row.test.ts`: 18/18.
</details>
## Acceptance notes
- The registry input description in
`ui/packages/plugin-detail/src/index.tsx` and the spec `.describe()`
still say the key's default is off. Both describe the key, and the key's
default has not changed: the default page sets it explicitly. They are
left as they are, so `sdui.manifest.json` and the spec docs are not
regenerated.
- Studio's page preview (`PagePreview`) renders the same synthesized
page. Its preview host mounts no edit session, so the path stays a
readout there.
## Verification
Local gate families derived by `node scripts/pm/dispatch-gates.mjs
--commands` on this branch. Results and the commit they ran against are
listed in the os-dev report comment on #30075. CI runs the full set.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QSeKYpFBMr917PmVkG9up7)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QSeKYpFBMr917PmVkG9up7
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(plugin-detail): the default record page lets an editor move a record to another stage by pressing it on the stage path
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
**Visible behaviour change on a released page.** The record page the console synthesizes for an object with a stage field (no authored page) used to draw its stage path as a read-only progress rail, so moving a deal to its next stage took 编辑 → change the stage → 更新. That page's `record:path` now carries `allowStageChange: true`, the existing opt-in (no new metadata key):
11
+
12
+
-**Who can press.** Unchanged from the opt-in: a reader the record page lets edit this record, on a stage field that is not read-only or computed, whose field edit grant holds for the stored row. Everyone else keeps the read-only path, and so does an editor while the record's inline edit holds unsaved changes.
13
+
-**Every press asks for confirmation**, then saves the stage through the record's ordinary update with the record's version, so the object's validation rules and `state_machine` decide. A refused move is shown under the path, naming any other field the move needs (for example a field made required by the new stage); nothing is written.
14
+
-**Only reachable stages are offered.** On every page with the opt-in, a stage the stage field's blocking `state_machine` cannot reach from the stored value is drawn as a readout, not a button, the same moves the record form, the grid's inline editor and the board offer. A field with no such rule offers every stage, and the server still judges each move.
15
+
-**Authored pages keep their own setting.** A page, or an authored `highlights` slot, that declares its own `record:path` keeps whatever it declares: `true`, `false`, or the key left out (a read-only rail).
16
+
17
+
To keep a default record page's path read-only, assign the object an authored page (or a `highlights` slot) whose `record:path` leaves `allowStageChange` out or sets it `false`.
0 commit comments