Repository navigation
Commit 3f61e4f
feat(fields): Setup's notification topic and channel fields pick from the served list and still take a typed value (#31069)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoC9MDip31tiDLU5QajkcAgR)_
Fixes #31062
Part of #30208
Clause-②: yes (widening) — five new `fields.preferenceOption.*` keys in
every published `@object-ui/i18n` pack, and `field:notification-topic` /
`field:notification-channel` join `@object-ui/fields`' registered widget
keys (`RegisteredFieldWidgetType`); nothing is narrowed.
Metadata-protocol: none
## 维护者速读
- 改了什么:Setup 的通知偏好(主题、渠道)和通知模板(主题)不再是纯文本框,改成下拉选择器,列表来自服务端
`preference-options` 接口,`*` 排第一;仍可直接输入列表外的值。
- 为什么改:管理员手敲主题/渠道容易打错(如 `invoices.paid`、`emial`),通知就会静默不发或发错渠道;按裁决
E(#30527)给出可选列表并保留自由输入。
- 风险与回滚:只影响声明了 `notification-topic` / `notification-channel`
的三个字段;接口失败或返回空列表时退回原来的文本框;回滚即 revert 本 PR。
- 先看:`ui/packages/fields/src/widgets/RegistryRefField.tsx`(填充分支
`PreferenceOptionField`)、`ui/packages/fields/src/widgets/preferenceOptions.ts`(读取与共享请求)、`ui/packages/fields/src/index.tsx`(注册)。
- 两分钟验证:打开 Setup → Notification Preferences → New,点 Topic,应看到 "All
topics (*)" 排第一的列表;在搜索框输入 `invoices.paid` 点 "Use …",字段显示 "invoices.paid
(not in the list)";Channel 列表里 push 显示 "(not configured)" 且可选。
- 需要你做的:无,席位落地。
- **席位意见**:ui 席 2 对照 head `43dd3c69` 的 diff 复核:24 个文件、+776 /
−28,无受治理路径。设置里通知偏好与模板的「主题」「渠道」字段改为可填写的下拉:列表来自 `GET
/api/v1/messaging/preference-options`(经宿主已认证的 fetch,整页共用一次请求),`*`
排在最前;手输的值原样保存,不在列表中的值保留并标注;未配置的渠道照列、标注、可选;请求失败或列表为空时回到原来的文本框。新增五条文案进所有语言包。Clause-②
改为 yes (widening),席位 TIER_DEFAULT 复核 PASS(6700168365)。CI 全绿,同意落地。
## What changes
`sys_notification_preference.topic` / `.channel` and
`sys_notification_template.topic` declare `widget: 'notification-topic'`
/ `'notification-channel'` since #31004; nothing registered them, so
they rendered as text boxes. This PR registers both as `field:WIDGET`
keys in `@object-ui/fields`, so the record form picks them up with no
per-page code.
**Home: option (a), a fillable arm on `RegistryRefField`.** It registers
through `fieldWidgetMap`, the field widget registry the record form
already consults for `FieldSchema.widget`, beside `position-ref` /
`permission-ref`; option (b) would live in `app-shell` beside
`FlowReferenceField`, which only the flow inspector reads, and would
need its own route into the record form. The arm reuses the registry
picker's four-state catalog machinery (IDLE / LOADING / FAILED / LOADED,
generalised from `useRegistryCatalog` to `useCatalog(reader, origin,
list)` with one module-level reader per arm), and differs from the
registry arm only where an open list must:
- **The list** comes from `GET /api/v1/messaging/preference-options`
through the host's authenticated fetch
(`SchemaRendererContext.apiFetch`, the console's existing
`createAuthenticatedFetch` path; no new fetch helper). One request per
fetch function for the page, shared by the topic and the channel field;
a failed read is not kept. Listed in the server's order, `*` first,
shown as "All topics (*)" / "All channels (*)".
- **No list** (no host fetch, a failed or malformed answer, or an empty
list): the plain text box, with no notice. The registry arm's failure
banner is not shown here.
- **A typed value**: the picker's search box offers a "Use "TEXT"" row
that commits the text exactly as typed. A stored value the list lacks
stays its own option, marked "TEXT (not in the list)"
(`data-not-in-list`), on the edit and the read face. Nothing is refused
or rewritten.
- **A channel the server marks not configured** is listed as "push (not
configured)" (`data-not-configured`) and is selectable.
- **Strings**: five `fields.preferenceOption.*` keys in every console
pack; `zh-Hant` regenerated from `zh-Hans` with `gen:zh-hant`.
- **Registration ripple, the `position-ref` (objectui#7611) ripple**:
`FIELD_WIDGET_LABELLING`, the bare-name skip list,
`INLINE_EXCLUDED_FIELD_TYPES` and the two fields widget sweeps; under
the seat's claim amendments 6699576193 and 6699989026, two
`widgetHintOnly` rows in `app-shell` `paramValueShape.ts`, the two names
in `plugin-detail`'s `inlineEditTypeCoverage` excluded bucket, and
`ui/packages/cli/src/utils/known-schema-types.ts` regenerated by `node
ui/scripts/regenerate-known-schema-types.mjs` (two lines, no hand edit).
The `de` quote census moves 108 → 109 for the new `„{{value}}“
verwenden` row.
Out of scope, unchanged: the template's `channel`
(`channel_renders_template`, no widget, still a text box), quiet hours,
row 5 of #30208, any server change.
## Tests
`ui/packages/fields/src/widgets/__tests__/RegistryRefField.preferenceOptions-31062.test.tsx`,
14 cases:
- the list renders from the route (`*` first, the door's order, one
shared read for two fields), and both keys resolve through
`field:WIDGET` in the registry;
- the fallback to the text box when the route answers 500, when the
fetch rejects, when the list is empty, and when no host fetch exists
(nothing read);
- a value outside the list is committed as typed and shown marked, on
the edit and the read face;
- a "not configured" channel is shown labelled and is selectable.
**Ablation** (`scripts/ablation-replace.mjs`, src import, no build in
the path): deleting the `'notification-topic'` loader from
`fieldWidgetMap` turns "`notification-topic` is registered and lists the
door" red (`expected undefined to be defined`, 1 failed / 12 passed);
restore verified blob == HEAD.
## Acceptance notes
- Clause-② is copied from the claim (`no`). The PR adds five keys to
every published `@object-ui/i18n` pack and two registered widget keys;
some recent i18n-key PRs declared that `yes (widening)` and some `no`,
so the seat may want to amend.
- Open PR #31037 also edits all ten hand-written
`ui/packages/i18n/src/locales/*.ts` packs, in a different block;
whichever lands second takes a textual merge.
- `pnpm check:eager-closure` needs a Console build and is CI's: the arm
adds code to `@object-ui/fields`, which rides the eager `ui-components`
chunk.
## Verification
Readings name their tree. Gates and type-check at `da4d45a2`; the last
commit `1fc7c752` adds only the regenerated `known-schema-types.ts`, and
the ui `scripts/` suite was re-run on it. Re-deriving at `1fc7c752`
gives the same 38 families.
<details><summary>Live before, on main 23cf11a (showcase backend
--fresh, Console dev server, admin sign-in)</summary>
- Setup → Notification Preferences → New: Topic and Channel are `INPUT
type=text`, prefilled `*`. Notification Templates → New: Topic and
Channel are `INPUT type=text`. No request to the options route.
- `GET /api/v1/messaging/preference-options` (signed in): `200`, 28
topics (`*` first, then `approval.*`, `auth.*`, `collab.mention`, the
showcase flow topics such as `invoice.signoff`, `project.digest`,
`task.assigned`, and `notify`), 5 channels: `*`, `inbox`, `email`,
`sms`, and `push` with `configured: false`. Anonymous: `401`.
</details>
<details><summary>Live after, on this branch (same backend)</summary>
- Notification Preferences → New: Topic trigger reads "All topics (*)",
its list has 28 options with "All topics (*)" first; Channel lists `All
channels (*)`, `inbox`, `email`, `sms`, `push (not configured)`.
- Typed `invoices.paid` in the topic search and picked "Use …": the
trigger reads "invoices.paid (not in the list)" with
`data-not-in-list=true`. Picked `push (not configured)`. Save, then `GET
/api/v1/data/sys_notification_preference` reads back `topic:
invoices.paid`, `channel: push`.
- Notification Templates → New: Topic is the picker; Channel stays
`INPUT type=text`.
- The options route answered `200` once per page load.
</details>
<details><summary>Tests</summary>
- `pnpm --dir ui exec vitest run
packages/fields/src/widgets/__tests__/RegistryRefField.preferenceOptions-31062.test.tsx`:
14 passed.
- `packages/fields/` whole, `packages/i18n/`,
`packages/components/src/renderers/form/`, the plugin-form
`position-ref` form test, the app-shell form-type guards and the cli
ratchet: 8 reds before the ripple rows (all named above), then green on
the rerun of every red file (361 + 367 + 48 tests).
- `pnpm --filter @object-ui/fields --filter @object-ui/i18n --filter
@object-ui/app-shell run type-check` after a dts build of their closure:
exit 0.
</details>
<details><summary>Derived gates (dispatch-gates --commands, 38 families;
every line exit 0)</summary>
Reconciled with `dispatch-gates.mjs --ran`: 38 derived, 38 run, 0
NOT-MEASURED. Record:
`/mnt/project-files/ui-seat-2/r2026-11-06/31062/gates-ran-1fc7c752.list`.
Includes `node scripts/check-sdui-manifest.mjs` (exit 0), `pnpm
check:nul-bytes`, `pnpm check:cross-package-test-inputs`, `pnpm
check:type-check-coverage`, `pnpm check:type-check-debt`, `pnpm
check:undeclared-dep-imports`, the changeset gates, and `pnpm --dir ui
exec vitest run scripts/` (170 passed, 2 skipped).
</details>
<!-- signed: domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf
-->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a539ba8 commit 3f61e4f
24 files changed
Lines changed: 776 additions & 28 deletions
File tree
- .changeset
- ui/packages
- app-shell/src/utils
- cli/src/utils
- fields/src
- __tests__
- widgets
- __tests__
- i18n/src
- __tests__
- locales
- plugin-detail/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 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
179 | 179 | | |
180 | 180 | | |
181 | 181 | | |
| 182 | + | |
| 183 | + | |
182 | 184 | | |
183 | 185 | | |
184 | 186 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
146 | 146 | | |
147 | 147 | | |
148 | 148 | | |
| 149 | + | |
| 150 | + | |
149 | 151 | | |
150 | 152 | | |
151 | 153 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
| 193 | + | |
193 | 194 | | |
194 | 195 | | |
195 | 196 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
137 | | - | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
138 | 143 | | |
139 | 144 | | |
140 | 145 | | |
| |||
188 | 193 | | |
189 | 194 | | |
190 | 195 | | |
| 196 | + | |
| 197 | + | |
191 | 198 | | |
192 | 199 | | |
193 | 200 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
144 | 144 | | |
145 | 145 | | |
146 | 146 | | |
147 | | - | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
148 | 153 | | |
149 | 154 | | |
150 | 155 | | |
| |||
207 | 212 | | |
208 | 213 | | |
209 | 214 | | |
| 215 | + | |
| 216 | + | |
210 | 217 | | |
211 | 218 | | |
212 | 219 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4890 | 4890 | | |
4891 | 4891 | | |
4892 | 4892 | | |
| 4893 | + | |
| 4894 | + | |
| 4895 | + | |
| 4896 | + | |
| 4897 | + | |
| 4898 | + | |
| 4899 | + | |
| 4900 | + | |
4893 | 4901 | | |
4894 | 4902 | | |
4895 | 4903 | | |
| |||
5072 | 5080 | | |
5073 | 5081 | | |
5074 | 5082 | | |
| 5083 | + | |
| 5084 | + | |
5075 | 5085 | | |
5076 | 5086 | | |
5077 | 5087 | | |
| |||
5220 | 5230 | | |
5221 | 5231 | | |
5222 | 5232 | | |
| 5233 | + | |
| 5234 | + | |
5223 | 5235 | | |
5224 | 5236 | | |
5225 | 5237 | | |
| |||
0 commit comments