Repository navigation
Commit c580081
fix(ui): judge requiredWhen as a transition gate on the console form page, the kanban drop prompt and the Edit wizard (#30301)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBY37urno69QP7HQ4DEeF5Fw)_
Fixes #30220
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:三处入口都改为按引擎的方式判
`requiredWhen`(转移闸门):控制台表单页(`/forms/:name?recordId=`
编辑)、看板拖放提示、编辑向导的最终提交检查。记录本来就处在闸门内且缺值时,不再拦住无关编辑、不再弹窗索要该字段;把记录移入闸门、清空已有值的编辑照旧拦截。向导按你在
#30220 上裁决的 A 实现:已存记录只交给存量判定,实时判定仍不绑定 `previous`。
- 为什么改:#30216 只修了记录编辑表单;同一条已激活但缺签字件的合同,在表单页保存报「Required: Signed
Copy」,看板拖到别的优先级列弹窗索要签字件,编辑向导最后一步提交被拒,而 REST `PATCH` 本身返回 200。
- 风险与回滚:只改客户端判定,读的都是 #30216 已有的存量判定(`requiredWhenAtRest` /
`requiredWhenRestsOnStoredRecord`),没有第二套规则;最坏情况是客户端放行、服务端拒绝,拒绝信息照常显示。回滚即还原本
PR。
- 先看:`ui/packages/plugin-form/src/WizardForm.tsx`(`gateFinalSubmit` 新增的
`isEditWizard && !field.required &&
requiredWhenRestsOnStoredRecord(...)`
分支)、`ui/apps/console/src/components/FormPage.tsx`(`resolveRowState` 多了
`name:
field.name`)、`ui/packages/plugin-kanban/src/requiredWhenPrompt.ts`(多了
`name`)。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/plugin-form/src/wizardSkipValidation.test.tsx
apps/console/src/components/FormPage.conditionalRules.test.tsx
packages/plugin-kanban/src/requiredWhenPrompt.test.ts`,预期全绿,其中三个「…already
inside the gate…」用例通过。
- 需要你做的:无,席位落地。
## What changes
The engine's rule (`packages/objectql/src/validation/rule-validator.ts`,
the "ADR-0113 non-regression: reject iff the MERGED state violates AND
the PRE state complied" block, `preViolated`) refuses an update only
when the merged record violates `requiredWhen` and the stored row
complied. #30216 mirrored that in `@object-ui/core`
`resolveFieldRuleState`: given `previous` (the stored row) and
`statics.name`, a stored row already inside the gate without the value
resolves `required: false` / `requiredWhenAtRest: true`. A caller that
passes no `name` keeps the plain verdict, and the stored-row check is
exported alone as `requiredWhenRestsOnStoredRecord`. This PR makes the
three remaining doors read it:
- **Console form page** (`ui/apps/console/src/components/FormPage.tsx`,
`resolveRowState`): it already passed the stored record
(`loaded.record`, the `GET /data/:object/:id` row) as `previous`; it now
passes `name: field.name`. The asterisk, `aria-required` and the page's
own submit check (`findSubmitRefusals`) all read this one verdict, so
all three follow. A create form passes no `previous` and is unchanged.
- **Kanban drop prompt**
(`ui/packages/plugin-kanban/src/requiredWhenPrompt.ts`,
`collectRequiredWhenPromptFields`): it already passed the pre-drop
record as `previous`; it now passes `name`. A drop of a record already
inside the gate without the value issues the plain single-key `PATCH`
with no dialog; a drop that moves a compliant record into the gate still
opens the "Complete required fields" dialog.
- **Edit wizard** (`ui/packages/plugin-form/src/WizardForm.tsx`,
`gateFinalSubmit`), as ruled on #30220 (option A, ruling record
6656113078): on an Edit wizard, after the live verdict, a field is
skipped when `requiredWhenRestsOnStoredRecord(field.requiredWhen, name,
persistedRecord, predicateScope)` answers true and the field is not
statically required. The live `resolveFieldRuleState` call still binds
no `previous`, so objectui#8069's option-D refusal (no new binding
pipeline) and its accepted `visibleWhen` residual are unchanged;
`persistedRecord` (the record as first read, the same one the step forms
receive as `previousValues`) reaches only the stored-row check, which
can only relax a `requiredWhen`, reports no fault, and counts an
unevaluable predicate as compliant. The gate's note says so next to the
option-D note.
No new rule and no change in `@object-ui/core`. No engine-side door: the
engine already implements the gate (the `preViolated` check), so there
is no `domain:server` half.
## Why
#30216 fixed the record Edit form only. The same record (a contract
activated before its signed-copy rule existed) was still refused by the
console form page ("Required: Signed Copy"), prompted for the signed
copy by a kanban drop into another priority column, and refused by an
Edit wizard's final submit, while REST `PATCH` answers 200 for these
writes.
## Tests
- `FormPage.conditionalRules.test.tsx` (the file's existing harness, new
top-level describe, real `FormPage` on `/forms/:name?recordId=`): a
record already activated with no signed copy shows no required marker
and saves an unrelated edit (one `PATCH`, body carries the edit).
CONTROL: clearing the value of an activated record that held it is
refused (marker shown, `toast.error`, no write).
- `requiredWhenPrompt.test.ts` (new top-level describe): a record
already inside the gate dropped into another column of an unrelated
grouping collects nothing. CONTROL: a draft dropped into `activated`
still collects `signed_copy`.
- `wizardSkipValidation.test.tsx` (the cross-step gate's harness, new
top-level describe, real `WizardForm` in edit mode, `allowSkip`, the
field on the skipped step so only the gate judges it): an activated
record with no signed copy saves an unrelated edit (one `update`).
CONTROL: a draft moved to `activated` without the copy is refused, no
`update`, and the wizard lands on the step holding the field.
## Acceptance notes
- `Clause-②: no`: `resolveRowState`, `collectRequiredWhenPromptFields`
and `WizardForm` keep their signatures and props; only the verdict for a
stored row already inside the gate changes.
- The wizard leg has unit pins and an ablation but no browser leg: the
dispatch asked for browser legs on the kanban and form page doors, which
were taken in round 1.
- The after-leg form page shot shows the post-submit record page under
the fresh boot's "Set the default currency" dialog; the "Submitted"
toast and the `PATCH ... -> 200` reading are the evidence.
## Verification
Head: `4af42bb2` (base `origin/main` merged at `1013ef11`). Round 2 (the
wizard, ruled option A) added `99f59f62` (the fix) and `fe2c28f0` (the
changeset) plus two merges of `origin/main`; the readings below say
which head they were taken on.
<details><summary>Round 2: the Edit wizard (tests, ablation,
type-check)</summary>
```
pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-form/ packages/core/src/evaluator/ packages/plugin-kanban/ apps/console/src/components/FormPage.conditionalRules.test.tsx (on fe2c28f, under os-verify-lock)
Test Files 330 passed (330)
Tests 3578 passed | 28 skipped (3606)
```
Re-run on `4af42bb2`: `wizardSkipValidation.test.tsx`,
`wizardVisibleWhenFault-8069.test.tsx` (the option-D residual pins,
unchanged and green), `FormPage.conditionalRules.test.tsx`,
`requiredWhenPrompt.test.ts`: Test Files 4 passed (4), Tests 54 passed
(54).
```
WizardForm.tsx: " isEditWizard &&" -> " false &&" (the stored-row skip disabled)
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 4ae849c -> 6fedd3263db6
× saves an unrelated edit to a record already inside the gate without the value
Tests 1 failed | 8 passed (9)
ablation-replace: ok restored: blob == HEAD (4ae849c) and `git diff HEAD` is empty
```
The CONTROL stayed green under the ablation (it pins the gate holding).
`WizardForm` is imported by relative path from the test, so no `dist/`
is involved.
```
pnpm --filter @object-ui/plugin-form --filter @object-ui/console --filter @object-ui/plugin-kanban run type-check
ui/packages/plugin-kanban type-check: Done
ui/packages/plugin-form type-check: Done (tsc --noEmit && tsc -p tsconfig.test.json)
ui/apps/console type-check: Done
os-verify-lock: VERDICT command-exit 0
```
`npx eslint src/WizardForm.tsx src/wizardSkipValidation.test.tsx`
(plugin-form): 0 errors.
</details>
<details><summary>Browser check on a dev boot (showcase, fresh, built
from this branch at origin/main 31260ca + this diff; Vite console
serving src)</summary>
Seeded by API as admin: writable package `com.qa.rwgate`, object
`rwgate_contract` (status draft/activated, priority low/high,
`signed_copy` text) with three rows inserted BEFORE the rule existed,
then the object re-saved with `signed_copy.requiredWhen: record.status
== 'activated'`; a kanban grouped by priority (default list), a kanban
grouped by status (`by_status`), a simple form view, an app.
Engine control on the same boot (REST): `PATCH` the activated row with
no signed copy `{priority: high}` answered 200; `PATCH` the draft row
`{status: activated}` answered 400 `VALIDATION_FAILED` ("Signed Copy is
required").
| Leg | Before (fix removed from src, Vite restarted `--force`, served
code checked) | After (this branch) |
|:--|:--|:--|
| Form page, Edit of the activated row with no signed copy, rename,
Submit | label `Signed Copy*`, refused "Required: Signed Copy", no write
| label `Signed Copy`, `PATCH /data/rwgate_contract/0zgY… -> 200`,
"Submitted" |
| Form page CONTROL, clear the signed copy of a compliant activated row
| refused, no write | refused, no write (label `Signed Copy*`) |
| Kanban by priority, drag the activated row with no signed copy Low to
High | "Complete required fields" dialog, no write | no dialog, `PATCH
... -> 200`, card in High |
| Kanban by status CONTROL, drag the draft row Draft to Activated |
dialog asks for Signed Copy | dialog asks for Signed Copy, no write |
Shots: `/mnt/project-files/ui-orders/30220-shots/` (`before-1..4`,
`after-1..4`, one per leg). The boot and the Vite server were stopped
after the run (health 000).
</details>
<details><summary>Round 1: targeted tests, type-check, lint (on
4febaa7)</summary>
```
pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-kanban/ packages/core/src/evaluator/ (+ every FormPage*.test.tsx and every apps/console/src/__tests__ file naming FormPage / resolveRowState, 50 files)
Test Files 157 passed (157)
Tests 1462 passed | 27 skipped (1489)
os-verify-lock: VERDICT command-exit 0
```
Re-run on the merged head 4febaa7:
`FormPage.conditionalRules.test.tsx`, `requiredWhenPrompt.test.ts`,
`ObjectKanban.requiredWhenPrompt.test.tsx`: Test Files 3 passed (3),
Tests 47 passed (47).
```
pnpm --filter @object-ui/console --filter @object-ui/plugin-kanban run type-check
ui/packages/plugin-kanban type-check: Done (tsc --noEmit && tsc -p tsconfig.test.json)
ui/apps/console type-check: Done (tsconfig include: src, so the test file is compiled)
os-verify-lock: VERDICT command-exit 0
```
`npx eslint` on the four touched files from their package dirs: 0 errors
(26 pre-existing warnings in FormPage.tsx, none on touched lines).
</details>
<details><summary>Round 1: ablation per site, form page and kanban (fix
removed, pin goes red, restored to HEAD)</summary>
```
FormPage.tsx: anchor " name: field.name," deleted
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 75c5f18 -> af3d441afb12
× saves an unrelated edit to a record already inside the gate without the value
Tests 1 failed | 24 passed (25)
ablation-replace: ok restored: blob == HEAD (75c5f18) and `git diff HEAD` is empty
requiredWhenPrompt.ts: "readonly: def.readonly === true, name }" -> "readonly: def.readonly === true }"
ablation-replace: ok mutation landed: anchor 1 -> 0, blob c09359b -> c283327bf78e
× asks for nothing when the stored record already rests inside the gate without the value
Tests 1 failed | 13 passed (14)
ablation-replace: ok restored: blob == HEAD (c09359b) and `git diff HEAD` is empty
```
Both CONTROL cases stayed green under the ablation, as predicted: they
pin the gate holding, which the plain verdict also gives. Both subjects
are imported by relative path from their tests, so no `dist/` is
involved.
</details>
<details><summary>Derived gates (dispatch-gates --commands on 4af42bb,
35 families, all run)</summary>
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
`dispatch-gates --ran`: "35 derived famil(ies) accounted for — 35 run, 0
NOT-MEASURED (a DERIVED zero — all 35 recorded an exit code and none of
them is 3)". Round 1 ran the same 35 on 4febaa7, all exit 0.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; eslint on the touched files reports 0 errors.
- [x] 4. One issue, one branch off an up-to-date `main` (merged in
twice), 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: not touched.
- [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 (the 维护者速读 block is the
required Chinese summary).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufziwFKBXQchtNMQYZSX
<!-- signed: domain:ui#4 · os-support-ai ·
session_01WGufziwFKBXQchtNMQYZSX -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent df4c06e commit c580081
7 files changed
Lines changed: 264 additions & 3 deletions
File tree
- .changeset
- ui
- apps/console/src/components
- packages
- plugin-form/src
- plugin-kanban/src
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
695 | 695 | | |
696 | 696 | | |
697 | 697 | | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1146 | 1146 | | |
1147 | 1147 | | |
1148 | 1148 | | |
| 1149 | + | |
| 1150 | + | |
| 1151 | + | |
| 1152 | + | |
| 1153 | + | |
| 1154 | + | |
| 1155 | + | |
1149 | 1156 | | |
1150 | 1157 | | |
1151 | 1158 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
21 | 27 | | |
22 | 28 | | |
23 | 29 | | |
| |||
772 | 778 | | |
773 | 779 | | |
774 | 780 | | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
775 | 784 | | |
776 | 785 | | |
777 | 786 | | |
| |||
838 | 847 | | |
839 | 848 | | |
840 | 849 | | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
841 | 864 | | |
842 | 865 | | |
843 | 866 | | |
| |||
896 | 919 | | |
897 | 920 | | |
898 | 921 | | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
899 | 936 | | |
900 | 937 | | |
901 | 938 | | |
902 | 939 | | |
903 | 940 | | |
904 | | - | |
| 941 | + | |
905 | 942 | | |
906 | 943 | | |
907 | 944 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
272 | 272 | | |
273 | 273 | | |
274 | 274 | | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
172 | 172 | | |
173 | 173 | | |
174 | 174 | | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
114 | | - | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
115 | 120 | | |
116 | 121 | | |
117 | 122 | | |
| |||
0 commit comments