Skip to content

fix(app-shell): the notify recipients editor writes and keeps { emailField } - #25996

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-05x6gm
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-05x6gm

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #25900

Clause-②: no
Metadata-protocol: none

What changes

The Studio notify node's recipients editor (FlowRecipientsField) now knows the spec's fourth recipient form, the email-field recipient { emailField: 'FIELD' } (NotifyEmailFieldRecipientSchema, the whole recipients value, on a node whose only channel is email).

  • Data loss closed. A stored recipients: { emailField } read as no recipients (readRecipients kept only strings and CEL envelopes), so the first edit of the node replaced it. It now reads as one Email field row, and an unedited row is written back as the object it was read from.
  • An Email field row. Picks an email-typed field of the trigger object (the suggestions are filtered by field type) and stores { emailField: 'FIELD' } as the whole value. It is offered in the type select only while it is the sole row, and "Add recipient" is disabled beside it, so the editor never writes it as a list entry (which the spec refuses).
  • Channels in the same patch. The commit that first stores an email-field recipient also writes channels: ['email'] when the node does not already name email alone (either spelling, string or list, counts). A blur that changes nothing writes no channel. A stored { emailField } on a node naming other channels shows a note and a one-click "Send by email only" fix.
  • Record field row on a node that sends email. Where channels include email, the Record field picker no longer suggests email-typed fields: the runtime refuses the address that row's record.FIELD envelope evaluates to unless a user holds it (service-automation notify node, screenComputedAddresses). A stored Record field row over such a field keeps its value and says the address must belong to a user.

Where the fix lives, and one file beyond the card's list

  • FlowRecipientsField.tsx — the row kind, read/write, notes.
  • FlowReferenceField.tsx — ResolvedRef gains optional onlyFieldTypes / exceptFieldTypes for the object-field kind; resolveObjectName is exported so the recipients field reads the trigger object's field types.
  • FlowNodeConfigField.tsx — onCommit takes an optional alongside map of sibling keys and the recipients case forwards it.
  • FlowNodeInspector.tsx (declared to the seat as a file-surface addition) — setField applies alongside to the same node before the one patch. The write is the inspector's: two sequential setField calls each start from the node the render closed over, so the second would drop the first key.
  • i18n.ts — en and zh strings for the new type, placeholder and notes.

Tests

New file FlowRecipientsField.emailField-25900.test.tsx, 11 tests: stored { emailField } round-trip and its row, Add disabled beside it, the Email field picker's suggestions and its commit (accepted by the installed NotifyConfigSchema, with channels), no channel write when the node already names email alone, not offered beside another row, the stored-wrong-channels note and fix, a non-email field named, the Record field filter on email and non-email nodes, and the inspector writing recipients and channels in exactly one patch.

  • Before the fix (the new file against the unmodified editor): 10 failed, 1 passed (11). The data-loss pins failed on expected [] to deeply equal [ { kind: 'emailField', … } ] and expected null not to be null.
  • After, at e25d3a6e: FlowRecipientsField* 2 files, 28/28 passed; the whole metadata-admin/ directory 597 files, 6561 passed, 1 skipped.
  • Ablation (committed fix, scripts/ablation-replace.mjs, mutation landed anchor 1 -> 0, restored blob == HEAD, git diff HEAD empty): dropping the email-field arm from the readRecipients filter turns exactly the 5 stored-value tests red (11 → 6 passed), the write and filter tests stay green.
  • pnpm --filter @object-ui/app-shell run type-check: exit 0 (the test program lists the new test file).

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at e25d3a6e derived 34 commands; all 34 exit 0, and --ran reconciles 34 derived, 34 run, 0 NOT-MEASURED (a derived zero, each line carrying its exit code). check:type-check-debt first returned queue-timeout (exit 3, nothing measured) and was re-run under the lock: OK. Type Check UI, Lint UI and Build & E2E UI are CI's.

Acceptance notes

  • Channels editor reads a string as empty (not fixed here). The notify node's Channels field is the offline stringList editor, which reads a stored channels: 'email' string as an empty list; an edit there then writes a list without email. This PR writes channels as a list so the field shows it; a stored string spelling stays affected. Reported to the seat for filing.
  • Lint on the touched files: 0 errors. Four new react-refresh/only-export-components warnings come from the new helper exports (isEmailFieldRecipient, readChannels, isEmailOnlyChannels, resolveObjectName), the same pattern as the helpers these files already export.
  • A stored list that contains an { emailField } entry (which the spec refuses) is now kept as read instead of dropped; the spec's refusal names it.

Session: https://claude.ai/code/session_01VrsYtPiyNCyFG3fUsVvNEQ


Generated by Claude Code

…Field }

A stored `recipients: { emailField }` read as no recipients, so the first
edit replaced it. The editor now reads it as one Email field row and writes
it back as read, offers an Email field row that suggests only email-typed
fields of the trigger object and stores the whole value, and writes
`channels: ['email']` in the same patch when the node does not already name
email alone. On a node that sends email the Record field row no longer
suggests an email-typed field, and a stored one says the address must be a
user's.

Claude-Session: https://claude.ai/code/session_01VrsYtPiyNCyFG3fUsVvNEQ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 26, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · seat domain:ui#1 · 2026-10-26T08:35Z

Head e25d3a6efe4c (read from the PR object).

  • Shape. Draft on main; Fixes #25900; Clause-②: no, Metadata-protocol: none; changeset .changeset/25900-notify-recipients-email-field-row.md in the root .changeset/ (patch, app-shell).
  • Checks pinned to the head. 54 named check runs, latest per name: 46 success, 8 skipped, 0 failing, 0 pending. Lint & Repo Gates, TypeScript Type Check, Test UI 16/16, Type Check UI all success.
  • Governed surface. check-governed-merges.mjs --pr 25996: 0 of 7 paths governed.
  • Surface. FlowNodeInspector.tsx was declared before the hand-back and is on the claim's amendment. The alongside plumbing writes the sibling channels into the same nextNode before the one patch, so two commits cannot race over a stale node. Every other field still calls onCommit(value).
  • Changeset against the diff. A stored { emailField } reads as one Email field row and round-trips; the Email field row suggests only email-typed trigger fields and is offered only as the sole recipient; its commit writes channels: ['email'] unless the node already names email alone, with a note and one-click fix for a stored mismatch; the Record field row drops email-typed fields on email-sending nodes and flags a stored one. Each sentence matches the code.
  • Out-of-scope finding. The class-a finding in the report (a stored channels: 'email' string reads as an empty list in FlowStringListField) is filed as Studio flow editor: a notify node's stored channels: 'email' (string form) reads as an empty list, and the next edit drops email #25999.

Landing: ready + auto-merge through the relay.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40293762961 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (15/16) — 失败步骤: Run the ui tests (shard 15/16)

    × after a successful save: the shell leaves at once 1040ms
    FAIL   @object-ui/console  src/pages/settings/__tests__/SettingsView.shellLeave-13231.test.tsx > CONTROLS: the page registers only while it holds edits (objectui#13231) > after a successful save: the 
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/pages/settings/__tests__/SettingsView.shellLeave-13231.test.tsx — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 14 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat domain:ui#1, 2026-10-26T08:51Z: queue build 40293762961 failed in ui/apps/console/src/pages/settings/__tests__/SettingsView.shellLeave-13231.test.tsx:195. A findByText('Profile page') gave up at 1040 ms, just past the default 1 s wait. That is a timing failure in a Console settings test, and this PR does not touch that package (its files are all under app-shell/src/views/metadata-admin/inspectors/). The full suite runs only in the queue. Re-queued once as the discriminator. A second red on the same test is real and goes to a card, not to another re-queue.

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 tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants