Skip to content

fix(ui): judge requiredWhen as a transition gate on the console form page and the kanban drop prompt - #30301

Draft
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-4b1zu7
Draft

objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-4b1zu7

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Refs #30220

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:控制台表单页(/forms/:name?recordId= 编辑)和看板拖放提示,改为按引擎的方式判 requiredWhen(转移闸门):记录本来就处在闸门内且缺值时,不再拦住无关编辑、也不再弹窗索要该字段;把记录移入闸门、清空已有值的编辑照旧拦截。编辑向导(WizardForm)未改,需要你裁决(见下方 Open question)。
  • 为什么改:fix(ui): judge requiredWhen as the engine's transition gate on the record Edit form #30216 只修了记录编辑表单;同一条已激活但缺签字件的合同,在表单页保存会报「Required: Signed Copy」,在看板上拖到别的优先级列会弹窗索要签字件,而 REST PATCH 本身返回 200。
  • 风险与回滚:只改客户端判定,两处各加一个 name 参数,读的是 fix(ui): judge requiredWhen as the engine's transition gate on the record Edit form #30216 已有的 requiredWhenAtRest,没有第二套规则;最坏情况是客户端放行、服务端拒绝,拒绝信息照常显示。回滚即还原本 PR。
  • 先看:ui/apps/console/src/components/FormPage.tsx(resolveRowState 的 statics 多了 name: field.name)、ui/packages/plugin-kanban/src/requiredWhenPrompt.ts(statics 多了 name)、FormPage.conditionalRules.test.tsx 新增的 describe。
  • 两分钟验证:pnpm --dir ui exec vitest run apps/console/src/components/FormPage.conditionalRules.test.tsx packages/plugin-kanban/src/requiredWhenPrompt.test.ts,预期 39 个用例全绿,其中「saves an unrelated edit to a record already inside the gate without the value」与「asks for nothing when the stored record already rests inside the gate」通过。
  • 需要你做的:在 requiredWhen is still judged against current values in three more form doors: the edit wizard, the console FormPage, and the kanban drop prompt #30220 上裁决编辑向导这一处(是否允许把已存记录只传给 requiredWhenRestsOnStoredRecord),其余两处席位落地。

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. This PR makes two more callers pass 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.

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.

Not changed: the edit wizard (open question)

ui/packages/plugin-form/src/WizardForm.tsx gateFinalSubmit passes undefined as previous in both modes, so requiredWhenAtRest is always false there and an Edit wizard on a record already inside the gate is refused at the final submit (each step's own form renders with previousValues and already lets it through since #30216). Its note fences this:

⚠️ This gate binds no previous in either mode (see the serverOwnedValue note below), so a visibleWhen reading previous is unevaluable here even on an EDIT wizard, and is refused. objectui#8069's ruling declined new binding for previous ("no new binding pipelines", its option D) and accepted the create-form half of that as the residual; the edit-wizard half is the same mechanism, accepted as a residual under the same refusal of option D on objectui#8069 — ⛔ so persistedRecord is deliberately NOT bound here.

The order asked to read the ruling text first; objectstack-ai/objectui is outside this session's GitHub access, and the repository holds only the paraphrase above (and the ADR-0137 header naming the ruling). So the wizard site is left untouched and reported on the card as needs_decision, with these options:

  • A (recommended). In gateFinalSubmit, on an Edit wizard only, after the live verdict, call requiredWhenRestsOnStoredRecord(field.requiredWhen, name, persistedRecord, predicateScope) and skip the field when it answers true and the field is not statically required. The live resolveFieldRuleState call still binds no previous, so the visibleWhen fault residual the ruling accepted is unchanged; the stored row reaches only the stored-row check, which can only relax a requiredWhen, never reports a fault, and counts an unevaluable predicate as compliant. persistedRecord already feeds this component (gateFormFields previousValues, and the step renderer's previousValues). About 10 lines plus one pin and one control.
  • B. Pass persistedRecord as the gate's live previous on an Edit wizard. This is the binding the ruling refused (it would also change the visibleWhen fault answer), so not recommended.
  • C. Keep the wizard as an accepted residual: an Edit wizard cannot save a record already inside a requiredWhen gate, while the record Edit form, the form page and the board can.

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") and prompted for the signed copy by a kanban drop into another priority column, while REST PATCH answers 200 for both 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.

Acceptance notes

  • Clause-②: no: resolveRowState and collectRequiredWhenPromptFields keep their signatures; only the verdict for a stored row already inside the gate changes.
  • 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: 4febaa77 (base origin/main merged at 959c26be).

Browser check on a dev boot (showcase, fresh, built from this branch at origin/main 31260ca + this diff; Vite console serving src)

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).

Targeted tests, type-check, lint
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).

Ablation per site (fix removed, pin goes red, restored to HEAD)
FormPage.tsx: anchor "      name: field.name," deleted
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 75c5f184d4ce -> 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 (75c5f184d4ce) 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 c09359b54998 -> 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 (c09359b54998) 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.

Derived gates (dispatch-gates --commands on 4febaa7, 35 families, all run)
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)".

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; eslint on the touched files reports 0 errors.
  • 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: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the required Chinese summary).

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufziwFKBXQchtNMQYZSX

…age and the kanban drop prompt

An Edit through /forms/:name?recordId= and a kanban drop now read the
stored-row verdict resolveFieldRuleState already carries
(requiredWhenAtRest): a record already inside the gate without the value
saves an unrelated edit, as the engine allows.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziwFKBXQchtNMQYZSX
…ge and the kanban drop prompt

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziwFKBXQchtNMQYZSX
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-support-ai · session_01WGufzinedUGSXteN45poyo

维护者速读

  • 改了什么:Console 表单页编辑和看板拖卡提示,判断字段 requiredWhen 时改为与引擎一致的「迁移闸门」:记录早已处于闸门内却缺值(例如规则建立前就已激活的合同),无关编辑可以保存、拖卡不再弹窗。
  • 为什么改:引擎(REST PATCH)早已放行这类记录,两个 UI 入口却拦下,用户被卡住(requiredWhen is still judged against current values in three more form doors: the edit wizard, the console FormPage, and the kanban drop prompt #30220)。
  • 风险与回滚:两处各只多传一个 name,读取 resolveFieldRuleState 已有的 requiredWhenAtRest 判定,没有第二套规则;把合规记录推进闸门、清空已有值、新建表单仍照旧拦截(对照测试钉住)。回滚即 revert。
  • 先看:FormPage.tsx 的 resolveRowState,requiredWhenPrompt.ts 的 collectRequiredWhenPromptFields。
  • 两分钟验证:pnpm --dir ui exec vitest run apps/console/src/components/FormPage.conditionalRules.test.tsx packages/plugin-kanban/src/requiredWhenPrompt.test.ts。
  • 需要你做的:CI 全绿后请 Ready + 加入合并队列(或回复「允许落地」由我代办);编辑向导(WizardForm)那一处另有一张决策卡等你选。

席位意见:ACCEPT(本 PR 覆盖的两处)。向导一处未动,卡片 #30220 在合并后仍保持打开。

席位核对

ACCEPT → PR #30301 @ 4febaa77dfa2bbe8eaa1642a082d6f522362a0d5 (card #30220, os-dev-report 6650598384).

  • Shape: draft, base main, Refs #30220 (not Fixes: the edit-wizard site is out of this PR and needs a ruling). 5 files, +145/−1; check-governed-merges --pr 30301: NOT governed (0 of 5 paths), under the human-merge threshold.
  • Seat diff read: FormPage.resolveRowState adds name: field.name to the rule-state options; a create form passes no previous, so it keeps the plain verdict. collectRequiredWhenPromptFields adds name (the loop key over objectFields, in scope) with the pre-drop record as previous, so the drop is judged as the engine's transition gate. Both read the existing requiredWhenAtRest verdict; no core or engine change.
  • Tests: pins in FormPage.conditionalRules.test.tsx (unrelated edit on an at-rest record saves; CONTROL clearing a held value is refused) and requiredWhenPrompt.test.ts (at-rest record into an unrelated column collects nothing; CONTROL draft→activated still collects signed_copy). Per-site ablation: each pin red, controls green. Re-run on merged head 4febaa7: 47/47.
  • Browser: before (fix removed, served code checked) vs after, 4 legs each, /mnt/project-files/ui-orders/30220-shots/. Legs taken on the pre-merge base 31260ca; pins re-run on the merged head.
  • Gates: dispatch-gates --commands 35 families exit 0, --ran 35/0 NOT-MEASURED. CI on this head: in progress at this record (8 success, 11 skipped, 14 running, 0 red).
  • Open leg (needs_decision): WizardForm.gateFinalSubmit left untouched; objectui#8069's ruling text is unreadable from this seat. Options A/B/C in the report; seat recommends A. Put to the maintainer as a decision card.
  • Scope: changeset @object-ui/console, @object-ui/plugin-kanban (patch), Clause-②: no, Metadata-protocol: none. No out-of-scope findings.
  • Landing: when every check is green or expected-skip, relay pr_ready + automerge_enable. On MERGED: landing note on requiredWhen is still judged against current values in three more form doors: the edit wizard, the console FormPage, and the kanban drop prompt #30220, keep the card open for the wizard leg.

This branch has not been deployed

No deployments
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant