Skip to content

feat(fields): draw a notification preference's quiet hours as a time-zone picker and two times - #31459

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-g0hzfv
Nov 8, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-g0hzfv

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread
Fixes #31421
Clause-②: yes
Metadata-protocol: none

维护者速读

  • 改了什么:通知偏好的「免打扰」有了专用控件:时区选择 + 开始/结束时间,可跨午夜。写入的仍是 { tz?, start, end },存储格式不变;读不懂的旧值原样显示并附说明,不会被悄悄改写。
  • 为什么改:现在要手写 { "tz": "Europe/Berlin", "start": "22:00", "end": "07:00" } 这样的 JSON,管理员和用户都容易写错。
  • 风险与回滚:只新增一个控件名 quiet-hours 和十个语言包里的十条文案;没有字段声明这个控件之前,界面完全不变。回滚即 revert 本 PR。
  • 先看:ui/packages/fields/src/widgets/QuietHoursField.tsx 的 commit(何时写、何时不写),ui/packages/fields/src/widgets/quietHours.ts 的 readQuietHours(什么算读得懂),ui/packages/fields/src/index.tsx 的三处登记。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/fields/src/widgets/__tests__/QuietHoursField-31421.test.tsx,预期 35 个测试全过。
  • 需要你做的:无,席位落地。控件要等 notification preference: quiet_hours declares the quiet-hours widget hint (server half of #31421) #31426(给字段加 widget: 'quiet-hours')合入后才会在 Setup 出现;两个 PR 先后顺序不限。

What changes

A field that declares widget: 'quiet-hours' is drawn as a time-zone picker plus a start and an end time instead of the JSON box. The stored shape is the one the messaging service reads: { tz?, start: 'HH:MM', end: 'HH:MM' }, an absent tz meaning UTC, a window whose start is later than its end running past midnight.

What turns it on in Setup: #31426 (server lane) adds widget: 'quiet-hours' to sys_notification_preference.quiet_hours. The two land in either order: until a field declares the hint, the form resolves json to the JSON editor as before, and an unregistered hint degrades to that same editor.

File Change
ui/packages/fields/src/widgets/quietHours.ts New. readQuietHours (empty / window / unreadable), the HH:MM test the service applies, and the zone list: UTC first, then Intl.supportedValuesOf('timeZone') filtered by the spec's iana_time_zone membership test, the currency-code-picker precedent.
ui/packages/fields/src/widgets/QuietHoursField.tsx New. The control and its readonly face.
ui/packages/fields/src/index.tsx quiet-hours in fieldWidgetMap, FIELD_TYPES_SKIP_FALLBACK and FIELD_WIDGET_LABELLING ('group': three controls under one label).
ui/packages/fields/src/FieldEditWidget.tsx quiet-hours in INLINE_EXCLUDED_FIELD_TYPES: edited in the record form, not in a grid cell.
ui/packages/fields/src/widgets/useFieldTranslation.ts The ten new strings in the provider-less fallback table, same values as en.
ui/packages/i18n/src/locales/*.ts Ten fields.quietHours.* keys in the ten hand-written packs; zh-Hant regenerated from zh-Hans (gen:zh-hant).
ui/packages/app-shell/src/utils/paramValueShape.ts One widgetHintOnly row (base: 'object'), which its test requires for every registered widget.
ui/packages/cli/src/utils/known-schema-types.ts Regenerated (node scripts/regenerate-known-schema-types.mjs): gains field:quiet-hours.
ui/packages/plugin-detail/src/__tests__/inlineEditTypeCoverage.test.tsx The excluded-bucket pin gains quiet-hours.
.changeset/31421-quiet-hours-control.md @object-ui/fields, @object-ui/i18n, @object-ui/cli patch.

Behaviour:

  • Writes exactly { tz?, start, end } once both times are set, null once both times of a stored window are cleared, and nothing for a zone chosen alone. One time without the other writes nothing and marks the missing time invalid through constraint validation (the ObjectField channel), so the form does not submit a half window.
  • Reads a stored window back with each part kept verbatim: a part nobody edits is written back as stored (a one-digit stored hour stays one digit). tz stays absent until a zone is chosen.
  • Unlisted zone: a stored tz the list does not hold stays shown, marked "(not in the list)", and is written back as stored.
  • Unreadable value (any other shape: an extra key, a missing time, a time the service would not read, a tz that is not a non-empty string, a string or an array): shown as stored with a note, never rewritten; a "Set a time range" button starts a new window, written only once both times are set.
  • Notes: "Runs past midnight and ends the next day." when start is later than end; "Start and end are the same, so nothing is held." when they are equal, because the service's quietHoursDeferral treats that as no window.

Tests

ui/packages/fields/src/widgets/__tests__/QuietHoursField-31421.test.tsx (35 tests). The card's three pins:

  • An empty value writes nothing: an empty control calls onChange for nothing; a zone alone writes nothing; a half window writes nothing and the missing time carries aria-invalid and the constraint message; clearing both times of a stored window writes null.
  • A 22:00 to 07:00 window round-trips: filling zone, start and end writes { tz: 'Europe/Berlin', start: '22:00', end: '07:00' } (strict equality, key for key); the stored window reads back into the three controls and an edit that restores it writes the identical object.
  • A stored value without tz reads as UTC: the picker shows UTC, an edit keeps tz absent, the readonly face says (UTC).

Plus the read table (15 shapes), the zone list, the unlisted zone, the same-minute window, the unreadable value (shown, not written, replaced only on request), and the record form path: the form renderer draws a json field whose metadata declares widget: 'quiet-hours' with this control as a named group, and the same field without the hint stays the JSON box.

Test runs (all under the shared verify lock, from ui/)
Run Head Result
vitest run packages/fields/ packages/i18n/ packages/cli/ + the five named pins (plugin-detail inline-edit coverage, app-shell paramValueShape / paramToField / degradeSetTwin-5880 / widget-dom-leak-sweep) + scripts/__tests__/known-schema-types-derivation-5115.test.ts + scripts/__tests__/check-doc-component-types.test.ts + examples/schema-catalog/test/ 3e7c3da7 483 files, 9687 passed, 3 failed: the two registry tables (widget-aria-invalid-registry-e2e, widget-dom-leak-e2e) and group-labelling-declaration did not yet list the widget. Fixed in e18cf4c7; the four files then 387/387.
vitest run packages/i18n/ + the new test + the three fixed pins + paramValueShape.test.ts + inlineEditTypeCoverage.test.tsx + scripts/ 1db93f4e (after merging main at 9728ab15, closure rebuilt) 277 files, 7314 passed, 15 skipped, 0 failed
type-check of @object-ui/fields, i18n, cli, app-shell, plugin-detail 1db93f4e working tree all five Done
eslint --no-inline-config on the touched source and test files 1db93f4e 0 errors; one warning in the new widget, react-hooks/set-state-in-effect on the external-value adoption, the ObjectField pattern and its inline disable

Acceptance notes

  • Docs. ui/AGENTS.md ✨ Set up Copilot instructions #2 asks for the package README and a content/docs/ page. Neither documents the sibling notification-topic / notification-channel hints either, and both paths sit outside this card's claimed surface; the seat has the call on whether that page lands with notification preference: quiet_hours declares the quiet-hours widget hint (server half of #31421) #31426, when the control goes live.
  • The grid cell keeps the JSON face (JsonCellRenderer): WIDGET_CELL_RENDERER_TYPES promotes only over a textual base type, and the card scopes the record form.
  • The zone list is ICU's, not the Localization manifest's 17. That curated list is served by the settings manifest to the console only and is labelled a convenience list, not the domain (seat ruling, claim amendment 6718720700).
  • Clause-②: yes is copied from the claim. Measured: ten new @object-ui/i18n locale keys and a new RegisteredFieldWidgetType member widen the published surface and narrow nothing, so the arm would read (widening); the claim's line is kept as written.

Verification

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 39 families for this diff; all 39 ran at 1db93f4e with exit codes captured before any pipe, and --ran answers: 39 derived families accounted for, 39 run, 0 NOT-MEASURED (a derived zero). Model tier: no path-derived mandate.

Derived gates at 1db93f4 (39/39 exit 0)
Command Exit
node scripts/check-adr-0087-registration.mjs --base origin/main 0
node scripts/check-adr-0087-registration.mjs --self-test 0
node scripts/check-changeset-no-major.mjs --base origin/main 0
node scripts/check-closing-keyword-parity.mjs 0
node scripts/check-closing-keyword-parity.mjs --self-test 0
node scripts/check-comment-mask-corpus.mjs 0
node scripts/check-empty-changeset.mjs --base origin/main 0
node scripts/check-empty-changeset.mjs --self-test 0
node scripts/check-sdui-manifest.mjs 0
node scripts/check-sdui-manifest.mjs --self-test 0
node scripts/ci/snapshot-next.mjs --self-test 0
node scripts/pm/release-rehearsal-clone.mjs --self-test 0
node scripts/release-pending-publish.mjs --self-test 0
pnpm --filter @objectstack/spec run check:duration-unit-keys 0
pnpm --filter @objectstack/spec run check:error-code-provenance 0
pnpm check:changeset-gate-self-tests 0
pnpm check:closed-ceremony-text 0
pnpm check:cross-package-test-inputs 0
pnpm check:dispatcher-error-vocabulary 0
pnpm check:driver-memory-census 0
pnpm check:engine-double-contract 0
pnpm check:gitlink-declared 0
pnpm check:merge-driver 0
pnpm check:nul-bytes 0
pnpm check:published-files 0
pnpm check:query-options-erasure 0
pnpm check:refd-timer-probe 0
pnpm check:registry-log-declared 0
pnpm check:rest-log-spy-declared 0
pnpm check:role-word 0
pnpm check:skill-identifier-liveness 0
pnpm check:test-source-alias 0
pnpm check:tier-file-adoption 0
pnpm check:type-check-coverage 0
pnpm check:undeclared-dep-imports 0
pnpm check:watch-hint-literal 0
pnpm check:where-matcher 0
pnpm --dir ui exec vitest run scripts/ 0 (inside the locked final run above)
pnpm check:type-check-debt 0 (under the verify lock)
  • node scripts/check-sdui-manifest.mjs: present and intact. The manifest is the console's public-tier registry and holds no field: key (0 today), so registering a field widget does not move it; CI's Build & E2E UI regenerates and compares the bytes.
  • pnpm check:new-line-citations (from ui/): 0 new citations.
  • CI families (Type Check UI, Test UI, Build & E2E UI and the rest) are read from this PR's check runs, not re-run here.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint on the touched files: 0 errors; the repo-wide run is CI's.)
  • 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: its generated artifacts are regenerated and committed. (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.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QsR2jTaiww8wGfNrqAW3vs


Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 8, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 1db93f4ef78224de12e512e76c6298db285d113e
Local-runs: none

The lane seat's own review of record, owed by the Clause-②: yes declaration alone (face declaration). No path of the diff is a published schema or a governed path (check-governed-merges --pr 31459: NOT governed, 0 of 24 paths).

① Derived judgments

  • @object-ui/i18n is published (no private); the diff adds ten fields.quietHours.* keys to every locale pack and removes or rewords none. Built leaves are readonly literals, so the published TranslationKeys widens: Clause-②: yes is RIGHT, arm (widening), which this record states and the claim's bare yes leaves unstated.
  • @object-ui/fields (published) registers one new widget name, field:quiet-hours, and exports nothing removed; @object-ui/cli (published) adds the name to its known component types. Both widen, neither narrows.
  • Metadata-protocol: none is RIGHT: packages/spec is untouched; FieldSchema.widget is already a free string, and the field hint itself is the server card notification preference: quiet_hours declares the quiet-hours widget hint (server half of #31421) #31426.
  • Accept sets: none change. The control writes the stored shape { tz?, start, end } or null, which the messaging service already reads.

② Semver level

  • @object-ui/fields, @object-ui/i18n and @object-ui/cli at patch with both declaration lines on the PR body and the changeset alike: RIGHT (a widening ships patch). @object-ui/app-shell (one widgetHintOnly row) is private and needs no entry.

③ Boundary flags

Implemented-by: claude/project-thread-g0hzfv
Reviewed-by: session_01QsR2jVEjiYrK7onirxUGyo

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo

席位核对 · ACCEPT · 2026-11-08T09:19Z · head 1db93f4ef78224de12e512e76c6298db285d113e

Seat domain:ui#3 reviewed the diff of its own dispatch (claim 6718689812 on #31421 with amendment 6718720700, card thread session_01QsR2jTaiww8wGfNrqAW3vs, report 6719558621). Checked against GitHub, not against the report.

  1. Shape: 24 files, +861 / -1, draft on main. New QuietHoursField.tsx + quietHours.ts in @object-ui/fields, registered as field:quiet-hours in index.tsx (widget map, skip-fallback set, group labelling) and excluded from inline edit in FieldEditWidget.tsx. commit writes { tz?, start, end } only when both times are set, null only when both are cleared from a stored window, and nothing for a half window or a zone alone. readQuietHours accepts only the three keys with HH:MM times and a non-empty string tz; anything else reads unreadable and is shown as stored. Registration sites outside ui/packages/fields are the three the amendment named (known-schema-types.ts, paramValueShape.ts, the inlineEditTypeCoverage pin), one line each. Ten fields.quietHours.* keys in every locale pack, none removed. Body: Fixes #31421 only; notification preference: quiet_hours declares the quiet-hours widget hint (server half of #31421) #31426 and Setup: replace developer wording and hand-written JSON with fields a business administrator can use #30208 carry no closing keyword.
  2. Premise: on origin/main quiet_hours is Field.json with no widget hint and no ui file names it; the widget is inert until notification preference: quiet_hours declares the quiet-hours widget hint (server half of #31421) #31426 adds the hint, and an unregistered hint degrades to the JSON editor, so the halves land in either order.
  3. Gates / check-runs: 55 runs on this head, 44 success, 11 skipped, 0 red, including Lint & Repo Gates, TypeScript Type Check, the 16 Test UI shards, Type Check UI, Lint UI and the E2E UI legs. Dev: 39 derived gates exit 0, --ran reconciled; new test file 35/35; ripple run green after three in-surface registry pins; ablation red 4/35, then restored. mergeable_state clean.
  4. Accepted deviations / notes: time zones come from Intl.supportedValuesOf('timeZone') filtered by the iana_time_zone domain, UTC first (approved at step 1); an unlisted stored zone stays shown and is written back as stored. Docs for the hint ride with notification preference: quiet_hours declares the quiet-hours widget hint (server half of #31421) #31426 (note 6719581791).
  5. Out of scope / Skills-docs: the field hint is notification preference: quiet_hours declares the quiet-hours widget hint (server half of #31421) #31426 (server lane, pm:queue). Metadata-protocol: none; Skills-docs: not owed. The changeset sentences were read against the diff: each describes behaviour the diff ships.
  6. Landing: check-governed-merges --pr 31459: NOT governed, 862 changed lines, so no human approval owed. Clause-②: yes: its TIER_DEFAULT review is PASS on this head (6719575053); no packages/spec/src path. Serial: PR feat(console): draw the map settings specifier as a table of keyed rows #31451 (this seat, in the merge queue) also edits the ten locale packs in another namespace; the queue serialises them. Landing on the maintainer's standing rule that fleet PRs are reviewed and landed by the pm-dispatch skill (2026-11-03T04:52Z), in this project. Relay pr_ready, then automerge_enable.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 8, 2026 09:20
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 8, 2026 09:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit f67f3b3 Nov 8, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-g0hzfv branch November 8, 2026 09:38
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 tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setup notification preferences: draw quiet hours (免打扰) as a time-range control instead of a JSON box (#30208 row 6 step 3)

1 participant