Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/31421-quiet-hours-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@object-ui/fields': patch
'@object-ui/i18n': patch
'@object-ui/cli': patch
---

feat(fields): a notification preference's quiet hours are drawn as a time-zone picker and two times, not a JSON box

Clause-②: yes
Metadata-protocol: none

`@object-ui/fields` registers `field:quiet-hours` for a field that declares `widget: 'quiet-hours'`: a daily window when notifications are held, stored as `{ tz?, start: 'HH:MM', end: 'HH:MM' }`. The stored shape does not change. The control appears in Setup once `sys_notification_preference.quiet_hours` declares the hint; until then the field keeps the JSON editor.

- **Writes.** Exactly `{ tz?, start, end }` once both times are set, and `null` once both are cleared. One time without the other writes nothing and marks the missing time invalid, so the form does not submit. Choosing a zone alone writes nothing.
- **Reads.** A stored window is read back into the control with each part kept as stored, so a part nobody edits is written back unchanged. A window without `tz` reads as UTC and stays without `tz` until a zone is chosen. A window whose start is later than its end runs past midnight, and the control says so; a window whose start and end are the same minute holds nothing, and the control says that too.
- **Time zones.** UTC first, then every zone `Intl.supportedValuesOf('timeZone')` lists that the `iana_time_zone` value domain accepts. A stored zone outside the list stays on screen, marked "not in the list", and is written back as stored.
- **Unreadable values.** A stored value of any other shape is shown as stored with a note, and is replaced only when the person chooses to set a new range and fills both times.
- **Strings.** Ten new `fields.quietHours.*` keys in every built-in language pack (`zh-Hant` regenerated from `zh-Hans`).
- `@object-ui/cli`'s known component types gain `field:quiet-hours`.
1 change: 1 addition & 0 deletions ui/packages/app-shell/src/utils/paramValueShape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export const PARAM_VALUE_SHAPES: Readonly<Record<string, ParamValueShapeSpec>> =
'permission-ref': { base: 'string', cardinality: 'scalar', widgetHintOnly: true, note: 'Permission-set name string. Widget-hint only — not a declarable param type.' },
'notification-topic': { base: 'string', cardinality: 'scalar', widgetHintOnly: true, note: 'Notification topic string. Widget-hint only — not a declarable param type.' },
'notification-channel': { base: 'string', cardinality: 'scalar', widgetHintOnly: true, note: 'Notification channel string. Widget-hint only — not a declarable param type.' },
'quiet-hours': { base: 'object', cardinality: 'scalar', widgetHintOnly: true, note: 'Quiet-hours window { tz?, start, end } object (null when empty). Widget-hint only — not a declarable param type.' },
});

/** Concrete `base[]` tag for a base shape. */
Expand Down
1 change: 1 addition & 0 deletions ui/packages/cli/src/utils/known-schema-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [
'field:position-ref',
'field:progress',
'field:qrcode',
'field:quiet-hours',
'field:radio',
'field:rating',
'field:recipient-picker',
Expand Down
2 changes: 1 addition & 1 deletion ui/packages/fields/src/FieldEditWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export const INLINE_EXCLUDED_FIELD_TYPES = new Set<string>([
// sibling fields / a loaded object catalog), not inline in a grid cell.
'object-ref', 'shareable-object-ref', 'filter-condition', 'recipient-picker',
'position-ref', 'permission-ref',
'notification-topic', 'notification-channel',
'notification-topic', 'notification-channel', 'quiet-hours',
]);

/** Field types whose value is chosen in one discrete gesture (no free typing). */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ const GROUP_LABELLED = [
'slider',
'signature',
'grid',
// A time-zone picker and two time inputs under one label (objectstack#31421).
'quiet-hours',
] as const;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ import {
NotificationTopicField,
NotificationChannelField,
} from '../widgets/RegistryRefField';
import { QuietHoursField } from '../widgets/QuietHoursField';

/** Same widget map as the #3291 sweep, so the parity check covers the registry. */
const WIDGETS: Record<string, ComponentType<any>> = {
Expand Down Expand Up @@ -197,6 +198,7 @@ const WIDGETS: Record<string, ComponentType<any>> = {
'permission-ref': PermissionRefField,
'notification-topic': NotificationTopicField,
'notification-channel': NotificationChannelField,
'quiet-hours': QuietHoursField,
};

/**
Expand Down
2 changes: 2 additions & 0 deletions ui/packages/fields/src/__tests__/widget-dom-leak-e2e.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ import {
NotificationTopicField,
NotificationChannelField,
} from '../widgets/RegistryRefField';
import { QuietHoursField } from '../widgets/QuietHoursField';

/* ════════════════════════════════════════════════════════════════════════════
* Every registered field widget, both hosts
Expand Down Expand Up @@ -216,6 +217,7 @@ const WIDGETS: Record<string, ComponentType<any>> = {
'permission-ref': PermissionRefField,
'notification-topic': NotificationTopicField,
'notification-channel': NotificationChannelField,
'quiet-hours': QuietHoursField,
};

/** Option widgets render an "unfillable" placeholder unless offered a list. */
Expand Down
6 changes: 6 additions & 0 deletions ui/packages/fields/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4728,6 +4728,10 @@ const fieldWidgetMap = {
// notification-channel → a notification channel, `*` for all
'notification-topic': () => import('./widgets/RegistryRefField.js').then(m => ({ default: m.NotificationTopicField })),
'notification-channel': () => import('./widgets/RegistryRefField.js').then(m => ({ default: m.NotificationChannelField })),
// A notification preference's quiet hours: a daily window stored as
// `{ tz?, start: 'HH:MM', end: 'HH:MM' }`, drawn as a time-zone picker and
// two times.
'quiet-hours': () => import('./widgets/QuietHoursField.js').then(m => ({ default: m.QuietHoursField })),
} satisfies Record<string, FieldWidgetLoader>;

/** The registered field widget keys, as a literal union (objectui#4857). */
Expand Down Expand Up @@ -4913,6 +4917,7 @@ const FIELD_TYPES_SKIP_FALLBACK = new Set([
'permission-ref',
'notification-topic',
'notification-channel',
'quiet-hours',
// ── Retired bare node keys (objectui#10859 batch 8) ───────────────────────
// Each was registered bare only as this loop's fallback: no other package
// owns the name, no form path reads it, and no document authors it as a
Expand Down Expand Up @@ -5064,6 +5069,7 @@ export const FIELD_WIDGET_LABELLING: Record<
'permission-ref': 'control',
'notification-topic': 'control',
'notification-channel': 'control',
'quiet-hours': 'group',
};

export function registerField(fieldType: string): void {
Expand Down
Loading
Loading