Repository navigation
Commit 5e1342a
fix(plugin-form): a create form offers a state_machine select only its initialStates (#27969)
domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg8CD9CXukrjPYCaAJupMsVw)_
## 维护者速读
- **改了什么**:新建表单里受 `state_machine` 管辖的下拉字段,现在只列出该状态机声明的 `initialStates`(如
showcase 项目只给「Planned」),与编辑表单已有的「只给可达状态」走同一道字段门。
- **为什么改**:服务端新建时只接受 `initialStates`,表单却列出全部选项,用户选别的值点保存才收到
`invalid_initial_state` 拒绝;按分诊「先改平台」裁定,在表单层修好,所有应用的新建表单一起受益。
- **风险与回滚**:只收窄选项列表、不改写任何值;没有声明 `initialStates` 的状态机、warning 级或仅 update
的规则保持全部选项;回滚即 revert 本 PR。
- **先看**:`ui/packages/plugin-form/src/stateMachineOptions.ts`(create
分支与「打开值保留」规则)、`ui/packages/core/src/validation/validators/state-machine-next-values.ts`(新
helper)、`ui/packages/plugin-form/src/ObjectForm.tsx`(gateFields
下移以拿到打开值)。
- **两分钟验证**:`pnpm --dir ui exec vitest run
packages/plugin-form/src/__tests__/stateMachineCreateSelect-27763.test.tsx`,预期
6 条全绿(主从表单、平铺表单、新建对话框都只给 `planned`)。
- **需要你做的**:无,席位落地。
Fixes #27763
Clause-②: no
Metadata-protocol: none
## What changes
- `@object-ui/core` gains `stateMachineInitialValues(objectSchema,
fieldName)` beside `stateMachineNextValues`: the values a new record may
be created in, read from the same served `validations` with the same
rule reading (blocking `severity: 'error'`, `active`, `events` covering
the write, absent keys read as the spec default). It returns `null` when
no applicable rule declares `initialStates`, which is exactly where the
server's insert check is a no-op. The two helpers now share one rule
predicate, so there is still one reading of a machine.
- `narrowStateMachineOptions` (the pass inside `gateFormFields`, which
every form layout draws through) gains a create arm: on `mode: 'create'`
a select governed by such a machine keeps only its `initialStates`. The
edit arm is unchanged.
- The value a create form OPENS with (`openingValues`: the seeded
`default: true` option or the caller's pre-fill) stays offered even when
it lies outside `initialStates`, so the form never silently drops a
value it was handed; the server's refusal stays the one that names it.
Every layout passes the values it opens with: `ObjectForm` (its gate
step moves below `finalDefaultValues` for that), `ModalForm`,
`DrawerForm`, `TabbedForm`, `WizardForm`.
- The master-detail form needs no path of its own: its header is an
`ObjectForm`, so it draws through the same gate (measured: the
master-detail test below goes red under the ablation with the others).
- When no field narrows, the pass returns the very array it was handed,
as before.
- The behaviour is written down in `ui/packages/plugin-form/README.md`
and `content/docs/frontend/plugins/plugin-form.mdx` (neither described
the edit-side narrowing before).
## Why
`showcase_project`'s `project_status_flow` declares `initialStates:
['planned']`, and the server refuses any other initial status with
`invalid_initial_state`. The "New Project + Tasks" create form offered
all five statuses, so four of them were dead ends. The triage ruled
platform first: the form stops offering what the server refuses, for
every app, and the showcase page stays as it is as the proof.
`examples/app-showcase/**` is untouched.
## Tests
-
`ui/packages/core/src/validation/__tests__/state-machine-next-values.test.ts`:
`stateMachineInitialValues` (package-root export, declared states,
absent `events`, no or empty `initialStates`, warning / inactive /
update-only rules, another field, intersection of two rules).
- `ui/packages/plugin-form/src/stateMachineOptions.test.ts`: the create
arm on both option carriers, the opening value kept, the persisted
record ignored on create, the no-`initialStates` control, the edit
control, a `view` form, and through `gateFormFields`.
-
`ui/packages/plugin-form/src/__tests__/stateMachineCreateSelect-27763.test.tsx`
(DOM, read off the native select Radix renders): the showcase
master-detail create form, the flat in-page create form and the console
create dialog each offer only `planned`; a caller's pre-fill outside
`initialStates` stays offered; CONTROL: no `initialStates` keeps every
option; CONTROL: an edit form still narrows from the stored value.
- `ui/e2e/live/master-detail.spec.ts`: one new case opens the status
select on the live showcase page and asserts `planned` is offered and
the four refused states are not. The existing cases are unchanged apart
from one comment.
<details><summary>Ablation (one-shot, on the committed tree)</summary>
Via `scripts/ablation-replace.mjs` under the verify lock;
`@object-ui/core` and the plugin-form sources resolve to `src/` through
the ui vitest alias, so no dist rebuild sits in the path.
1. Create arm off (`if (mode === 'create') {` replaced so it never
matches): 8 failed, 16 passed of the two files. Red: the 4 create-arm
unit cases and the 4 DOM create cases (master-detail, flat, dialog,
pre-fill). Green: both CONTROLs and every edit case. Restored: blob
equals HEAD, `git diff HEAD` empty.
2. Opening value dropped (`return initial && opening !== null ? new
Set([...initial, opening]) : initial;` replaced by `return initial;`): 2
failed, 22 passed. Red: exactly the two pre-fill cases. Restored the
same way.
</details>
## Acceptance notes
- Seeded default: the showcase status field's `default: true` option is
`planned`, which is inside `initialStates`, so the master-detail form
opens on a legal value. A seeded default outside `initialStates` would
stay offered (the opening-value rule) and the server would refuse it on
save; no showcase object has one today.
- The master-detail child rows (`showcase_task`) are not narrowed on
create: `task_status_flow` is update-only and declares no
`initialStates`, so the server does not police their insert either.
- Create surfaces outside the form gate (a grid's inline new row, a
board's quick-create) are not changed here; this PR covers the form
layouts that draw through `gateFormFields`. Filed by the seat as #27976.
- Mode: the create arm keys on `mode: 'create'`, the value every create
form in the console and the showcase page declares. A form that declares
no mode keeps every option, as before.
- The live e2e case is measured by CI's `Live E2E UI` job, not locally.
## Verification
Local readings. Tests and type check ran on `5c360f28`; the head
`54cb3d7d` adds only the README and docs-page text on top of it. The
gate union ran on `54cb3d7d`.
- Targeted: the four state-machine test files, 48 passed (48).
- Affected packages: `pnpm --dir ui exec vitest run
packages/plugin-form/ packages/core/`, Test Files 432 passed (432),
Tests 6533 passed, 28 skipped.
- Type check: `pnpm --filter @object-ui/core --filter
@object-ui/plugin-form run type-check`, both `Done`, VERDICT
command-exit 0.
- Derived gates (`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, 66 families, among them `node
scripts/check-sdui-manifest.mjs` and `pnpm --dir ui exec vitest run
scripts/` at 169 files passed): every one exit 0. `--ran`
reconciliation: "66 derived famil(ies) accounted for — 66 run, 0
NOT-MEASURED". `check:skill-examples` first refused with exit 3 (its
prerequisite, a built `@objectstack/client-react`, was missing; nothing
measured), then ran exit 0 after that build.
- `pnpm --dir ui run check:new-line-citations`: 0 new citations.
- Every other family is CI's run on this PR.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` untouched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` is CI's run.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CbUGFznjxdG6YQZm1Z)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 1c87beb commit 5e1342a
16 files changed
Lines changed: 530 additions & 65 deletions
File tree
- .changeset
- content/docs/frontend/plugins
- ui
- e2e/live
- packages
- core/src/validation
- __tests__
- validators
- plugin-form
- src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
100 | 104 | | |
101 | 105 | | |
102 | 106 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
62 | 72 | | |
63 | 73 | | |
64 | 74 | | |
| |||
87 | 97 | | |
88 | 98 | | |
89 | 99 | | |
90 | | - | |
91 | | - | |
| 100 | + | |
| 101 | + | |
92 | 102 | | |
93 | 103 | | |
94 | 104 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
11 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
12 | 15 | | |
13 | 16 | | |
14 | 17 | | |
| |||
117 | 120 | | |
118 | 121 | | |
119 | 122 | | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
35 | | - | |
36 | | - | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
16 | 18 | | |
17 | 19 | | |
18 | 20 | | |
| |||
25 | 27 | | |
26 | 28 | | |
27 | 29 | | |
28 | | - | |
| 30 | + | |
| 31 | + | |
29 | 32 | | |
30 | | - | |
| 33 | + | |
| 34 | + | |
31 | 35 | | |
32 | 36 | | |
33 | 37 | | |
| |||
42 | 46 | | |
43 | 47 | | |
44 | 48 | | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
| 49 | + | |
| 50 | + | |
50 | 51 | | |
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
54 | | - | |
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
61 | 60 | | |
62 | 61 | | |
63 | 62 | | |
64 | | - | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
65 | 90 | | |
66 | 91 | | |
67 | 92 | | |
| |||
97 | 122 | | |
98 | 123 | | |
99 | 124 | | |
100 | | - | |
101 | | - | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
102 | 151 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
434 | 434 | | |
435 | 435 | | |
436 | 436 | | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
437 | 455 | | |
438 | 456 | | |
439 | 457 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
284 | 284 | | |
285 | 285 | | |
286 | 286 | | |
| 287 | + | |
| 288 | + | |
287 | 289 | | |
288 | 290 | | |
289 | 291 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
374 | 374 | | |
375 | 375 | | |
376 | 376 | | |
| 377 | + | |
| 378 | + | |
377 | 379 | | |
378 | 380 | | |
379 | 381 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
609 | 609 | | |
610 | 610 | | |
611 | 611 | | |
612 | | - | |
613 | | - | |
614 | | - | |
615 | | - | |
616 | | - | |
617 | | - | |
618 | | - | |
619 | | - | |
620 | | - | |
621 | | - | |
622 | | - | |
623 | | - | |
624 | | - | |
625 | | - | |
626 | | - | |
627 | 612 | | |
628 | 613 | | |
629 | 614 | | |
| |||
1195 | 1180 | | |
1196 | 1181 | | |
1197 | 1182 | | |
| 1183 | + | |
| 1184 | + | |
| 1185 | + | |
| 1186 | + | |
| 1187 | + | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
| 1191 | + | |
| 1192 | + | |
| 1193 | + | |
| 1194 | + | |
| 1195 | + | |
| 1196 | + | |
| 1197 | + | |
| 1198 | + | |
| 1199 | + | |
| 1200 | + | |
1198 | 1201 | | |
1199 | 1202 | | |
1200 | 1203 | | |
| |||
0 commit comments