@object-ui/core 17.7.0 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +650 -0
- package/README.md +127 -0
- package/dist/actions/ActionRunner.d.ts +192 -49
- package/dist/actions/ActionRunner.js +389 -102
- package/dist/actions/UndoManager.d.ts +8 -13
- package/dist/actions/actionKeys.d.ts +1 -1
- package/dist/actions/actionKeys.js +10 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/navigationTarget.d.ts +41 -0
- package/dist/actions/navigationTarget.js +20 -0
- package/dist/actions/recordDelete.d.ts +59 -23
- package/dist/actions/recordDelete.js +40 -42
- package/dist/adapters/ApiDataSource.d.ts +8 -4
- package/dist/adapters/ApiDataSource.js +19 -7
- package/dist/adapters/resolveDataSource.d.ts +9 -6
- package/dist/adapters/resolveDataSource.js +9 -12
- package/dist/data-scope/ViewDataProvider.d.ts +7 -24
- package/dist/data-scope/ViewDataProvider.js +6 -53
- package/dist/evaluator/deadlineSettled.d.ts +40 -0
- package/dist/evaluator/deadlineSettled.js +45 -0
- package/dist/evaluator/fieldRules.js +6 -1
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/optionRules.d.ts +1 -1
- package/dist/evaluator/optionRules.js +1 -1
- package/dist/evaluator/subjectPermissions.d.ts +30 -1
- package/dist/evaluator/subjectPermissions.js +42 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/registry/Registry.d.ts +34 -13
- package/dist/registry/Registry.js +5 -22
- package/dist/utils/affordanceGrants.d.ts +371 -0
- package/dist/utils/affordanceGrants.js +252 -0
- package/dist/utils/chart-series.d.ts +34 -0
- package/dist/utils/chart-series.js +25 -2
- package/dist/utils/date-display.d.ts +42 -50
- package/dist/utils/date-display.js +192 -11
- package/dist/utils/expand-fields.d.ts +6 -4
- package/dist/utils/expand-fields.js +6 -4
- package/dist/utils/filter-converter.d.ts +6 -2
- package/dist/utils/filter-converter.js +6 -2
- package/dist/utils/normalize-list-view.d.ts +16 -12
- package/dist/utils/normalize-list-view.js +66 -67
- package/dist/utils/predicate-fields.js +6 -2
- package/dist/utils/record-source.d.ts +1 -1
- package/dist/utils/record-source.js +1 -1
- package/dist/utils/reference-keys.js +1 -1
- package/dist/validation/schema-validator.js +29 -5
- package/dist/validation/server-owned-value.d.ts +5 -1
- package/dist/validation/server-owned-value.js +5 -1
- package/package.json +4 -4
|
@@ -53,6 +53,22 @@ import { pivotBucketId, pivotDimensionValue } from './dataset-pivot.js';
|
|
|
53
53
|
* and {@link resolveMeasureLabel} treats it as absent.
|
|
54
54
|
*/
|
|
55
55
|
export const BUILTIN_AGGREGATES = ['count', 'sum', 'avg', 'min', 'max', 'count_distinct'];
|
|
56
|
+
/**
|
|
57
|
+
* The aggregates whose every value is a whole number: a count of rows, or of
|
|
58
|
+
* distinct values (objectui#11681, triage ruling Q3 A on that card). `sum` /
|
|
59
|
+
* `min` / `max` over an integer field would qualify too, but the answer does
|
|
60
|
+
* not state the source field's scale, so they are not judged integer here.
|
|
61
|
+
*/
|
|
62
|
+
export const INTEGER_AGGREGATES = ['count', 'count_distinct'];
|
|
63
|
+
/**
|
|
64
|
+
* Whether a measure column's `aggregate` makes every value a whole number — a
|
|
65
|
+
* value axis that plots only such measures takes integer ticks
|
|
66
|
+
* (objectui#11681). Judged over an untrusted wire value; anything else,
|
|
67
|
+
* absence included, answers `false`, so the axis keeps its default.
|
|
68
|
+
*/
|
|
69
|
+
export function isIntegerAggregate(value) {
|
|
70
|
+
return typeof value === 'string' && INTEGER_AGGREGATES.includes(value);
|
|
71
|
+
}
|
|
56
72
|
/** Runtime membership test for {@link BUILTIN_AGGREGATES}, over an untrusted wire value. */
|
|
57
73
|
export function isBuiltinAggregate(value) {
|
|
58
74
|
return typeof value === 'string' && BUILTIN_AGGREGATES.includes(value);
|
|
@@ -344,6 +360,12 @@ export function buildChartSeries(rows, dimensions, values, fields, options) {
|
|
|
344
360
|
const field = (fields ?? []).find((f) => f.name === name);
|
|
345
361
|
return field ? resolveMeasureLabel(field, options?.builtinAggregateLabels) : name;
|
|
346
362
|
};
|
|
363
|
+
// The measure column's own `aggregate`, spread onto each series that plots
|
|
364
|
+
// it (objectui#11681) — present only when the column states a string one.
|
|
365
|
+
const aggregateOf = (name) => {
|
|
366
|
+
const aggregate = (fields ?? []).find((f) => f.name === name)?.aggregate;
|
|
367
|
+
return typeof aggregate === 'string' ? { aggregate } : {};
|
|
368
|
+
};
|
|
347
369
|
// Multi-dimension, single-measure → pivot the second dimension into series.
|
|
348
370
|
if (dims.length >= 2 && vals.length === 1) {
|
|
349
371
|
const xKey = dims[0];
|
|
@@ -449,7 +471,8 @@ export function buildChartSeries(rows, dimensions, values, fields, options) {
|
|
|
449
471
|
// server-resolved to display labels by queryDataset), except the null
|
|
450
472
|
// group, which says so with the bucket label the renderer supplied. The
|
|
451
473
|
// key is a separate question from the text — see `pivotSeriesBuckets`.
|
|
452
|
-
|
|
474
|
+
// Every group plots the one measure, so each carries its aggregate.
|
|
475
|
+
series: groups.map(({ dataKey, label }) => ({ dataKey, label, ...aggregateOf(measure) })),
|
|
453
476
|
};
|
|
454
477
|
}
|
|
455
478
|
// Default: first dimension on the x-axis, one series per measure.
|
|
@@ -472,7 +495,7 @@ export function buildChartSeries(rows, dimensions, values, fields, options) {
|
|
|
472
495
|
? bucketNullCategories(safeRows, xKey, options?.nullCategoryLabel ?? NULL_CATEGORY_LABEL)
|
|
473
496
|
: safeRows,
|
|
474
497
|
xAxisKey: xKey,
|
|
475
|
-
series: vals.map((v) => ({ dataKey: v, label: labelOf(v) })),
|
|
498
|
+
series: vals.map((v) => ({ dataKey: v, label: labelOf(v), ...aggregateOf(v) })),
|
|
476
499
|
};
|
|
477
500
|
}
|
|
478
501
|
/**
|
|
@@ -6,56 +6,37 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `@object-ui/fields` re-exports the formatting symbols below under their
|
|
31
|
-
* original names, so `formatDate` / `formatDateTime` /
|
|
32
|
-
* `formatDateTimeCompactParts` / `formatRelativeDate` / `DateDisplayOptions`
|
|
33
|
-
* keep working unchanged for `ObjectGrid`, `ObjectGantt`,
|
|
34
|
-
* `plugin-dashboard`'s `recordFields` and the `date` cell renderer.
|
|
35
|
-
* `toDisplayDate` is exported from `@object-ui/core` alone (objectui#10183),
|
|
36
|
-
* and so are `toDisplayEndDate` / `toInclusiveEndDay`, the end read and its
|
|
37
|
-
* inverse both gantt surfaces share (objectui#11141).
|
|
38
|
-
*
|
|
39
|
-
* The `datetime` CELL face joined this file in objectui#7443. It used to be a
|
|
40
|
-
* second convention inlined in `DateTimeCellRenderer`: two `Intl` option bags
|
|
41
|
-
* for one field type, kept in step by nothing, while `date` had exactly one.
|
|
42
|
-
* It is `formatDateTime`'s `'compact'` style now, byte-identical to what the
|
|
43
|
-
* cell rendered before.
|
|
44
|
-
*
|
|
45
|
-
* Pure by construction (no React, no i18n): the only ambient inputs are `Intl`
|
|
46
|
-
* and the clock, and the one phrase `Intl` cannot produce ("Overdue Nd") comes
|
|
47
|
-
* in through the INJECTED `options.t`, the same way `buildDatasetFieldHelpers`
|
|
48
|
-
* in `dataset-format.ts` takes `fieldLabel`.
|
|
49
|
-
*
|
|
50
|
-
* ⚠️ There is a third ambient input, and it is the one this module has to
|
|
51
|
-
* decide about rather than pass on: the VIEWER's timezone. A value carrying a
|
|
52
|
-
* time is an instant and renders in that zone; a DATE-ONLY value names a
|
|
53
|
-
* calendar day, carries no instant, and must render as that day everywhere.
|
|
54
|
-
* `toDisplayDate` below is the single parse step that tells the two apart —
|
|
55
|
-
* every function here goes through it (objectui#10110), and so does every
|
|
56
|
-
* caller elsewhere that needs a face or a day comparison none of these
|
|
57
|
-
* functions produce (objectui#10183).
|
|
9
|
+
* Declare the zone instants render in — the host's half of objectui#11693.
|
|
10
|
+
*
|
|
11
|
+
* ## Why the zone enters HERE and nowhere else
|
|
12
|
+
*
|
|
13
|
+
* Every date and datetime face in the console is a function of this module
|
|
14
|
+
* (see the header), and each caller threads only its `locale`. The zone the
|
|
15
|
+
* signed-in workspace configures (`localization.timezone`, served by
|
|
16
|
+
* `GET /api/v1/auth/me/localization`) is the same for every face on the
|
|
17
|
+
* page, so it is set once, here, and every face reads it — rather than
|
|
18
|
+
* threaded through each call site, where one forgotten caller renders the
|
|
19
|
+
* same instant in a second zone beside the first (objectui#4272 is that
|
|
20
|
+
* defect for the locale). `LocalizationProvider` (`@object-ui/i18n`) is the
|
|
21
|
+
* host that calls this; a renderer never does.
|
|
22
|
+
*
|
|
23
|
+
* `undefined` (or an empty string) clears it: instants render in the
|
|
24
|
+
* viewer's zone again. A name this runtime's `Intl` does not know — a zone
|
|
25
|
+
* newer than the browser's time-zone data, say — clears it too, with a
|
|
26
|
+
* warning, rather than make every face throw the `RangeError` `Intl` raises
|
|
27
|
+
* for it.
|
|
28
|
+
*
|
|
29
|
+
* ⛔ A date-only value never takes the zone; see {@link toDisplayDate}.
|
|
58
30
|
*/
|
|
31
|
+
export declare function setDisplayTimeZone(timeZone: string | undefined): void;
|
|
32
|
+
/** The zone {@link setDisplayTimeZone} declared, or `undefined` for the viewer's own. */
|
|
33
|
+
export declare function getDisplayTimeZone(): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Be told when the display zone changes; returns the unsubscribe. The shape is
|
|
36
|
+
* React's `useSyncExternalStore` contract, which is how `LocalizationProvider`
|
|
37
|
+
* re-renders its consumers once the zone they format in has moved.
|
|
38
|
+
*/
|
|
39
|
+
export declare function subscribeDisplayTimeZone(listener: () => void): () => void;
|
|
59
40
|
/**
|
|
60
41
|
* Options shared by {@link formatDate} / {@link formatRelativeDate} /
|
|
61
42
|
* {@link formatDateTime}. One bag, and each function reads the keys it needs:
|
|
@@ -207,7 +188,9 @@ export declare function isRealCalendarDate(dateOnly: string): boolean;
|
|
|
207
188
|
* ⛔ Read the result with LOCAL getters or local-zone `Intl` formatting only.
|
|
208
189
|
* For a date-only value it is local midnight of the named day, so its
|
|
209
190
|
* `toISOString()` / UTC getters name the previous day east of UTC — hand
|
|
210
|
-
* those the stored value, never this.
|
|
191
|
+
* those the stored value, never this. The faces in this module know such a
|
|
192
|
+
* `Date` when they are handed one and keep it out of the display zone
|
|
193
|
+
* (objectui#11693, see `calendarDays`).
|
|
211
194
|
*/
|
|
212
195
|
export declare function toDisplayDate(value: string | Date | number): Date;
|
|
213
196
|
/**
|
|
@@ -269,6 +252,11 @@ export declare function toInclusiveEndDay(end: Date): Date;
|
|
|
269
252
|
* past dates render as plain "N days ago" instead. The overdue phrase has no
|
|
270
253
|
* `Intl` equivalent, so it resolves through `options.t` (key
|
|
271
254
|
* `fields.relativeDate.overdue`) with an English fallback.
|
|
255
|
+
*
|
|
256
|
+
* Days are counted in the display zone (objectui#11693): "today" is the
|
|
257
|
+
* display zone's today, and an instant falls on its day there. A date-only
|
|
258
|
+
* value keeps the day it names and is counted from that same today, so an
|
|
259
|
+
* instant and a date-only value on one day read one phrase.
|
|
272
260
|
*/
|
|
273
261
|
export declare function formatRelativeDate(value: string | Date | number, options?: DateDisplayOptions): string;
|
|
274
262
|
/**
|
|
@@ -350,5 +338,9 @@ export declare function formatDateTimeCompactParts(value: string | Date | number
|
|
|
350
338
|
* hard it tried — it always handed `Intl` an `undefined` tag, i.e. the
|
|
351
339
|
* MACHINE's locale, which is neither of the repo's two locale channels.
|
|
352
340
|
* Callers should pass the tag from `useDisplayLocale()`.
|
|
341
|
+
*
|
|
342
|
+
* The zone is NOT an option: an instant renders in the display zone the host
|
|
343
|
+
* declared with {@link setDisplayTimeZone}, else the viewer's, on every face
|
|
344
|
+
* this module produces (objectui#11693).
|
|
353
345
|
*/
|
|
354
346
|
export declare function formatDateTime(value: string | Date | number, options?: DateDisplayOptions): string;
|
|
@@ -5,6 +5,169 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* date-display - the ONE date/datetime display path behind every field cell,
|
|
10
|
+
* grid card, gantt tooltip and dataset measure in the console.
|
|
11
|
+
*
|
|
12
|
+
* These functions are unchanged; what changed is where they live. They were
|
|
13
|
+
* written in `@object-ui/fields`' barrel (`packages/fields/src/index.tsx`),
|
|
14
|
+
* which is a React package, so `@object-ui/core` could not reach them - and
|
|
15
|
+
* `core`'s `utils/dataset-format.ts` is exactly the caller that needed them:
|
|
16
|
+
* `formatMeasure` returned `String(v)` for any non-numeric value, so a `min` /
|
|
17
|
+
* `max` measure over a date field rendered its raw 24-character ISO string on
|
|
18
|
+
* the metric tile, in chart values, in dataset table cells and in the
|
|
19
|
+
* metadata-admin dataset preview (objectui#7178).
|
|
20
|
+
*
|
|
21
|
+
* The fix is the move, NOT a second formatter here. That choice is the whole
|
|
22
|
+
* point, and it is the lesson of objectui#4576: when the percent convention
|
|
23
|
+
* was duplicated across this same package boundary, a list cell and a
|
|
24
|
+
* dashboard measure drifted apart (`1.234,5 %` beside `1.234,5%` in a German
|
|
25
|
+
* session) while both were "correct". `dataset-format.ts`'s own header records
|
|
26
|
+
* that history, and `number-display.ts` next door is the same remedy applied
|
|
27
|
+
* to numbers: the pure function moves DOWN into the React-free engine and the
|
|
28
|
+
* upper package re-exports it, so there is one home and nothing to drift.
|
|
29
|
+
*
|
|
30
|
+
* `@object-ui/fields` re-exports the formatting symbols below under their
|
|
31
|
+
* original names, so `formatDate` / `formatDateTime` /
|
|
32
|
+
* `formatDateTimeCompactParts` / `formatRelativeDate` / `DateDisplayOptions`
|
|
33
|
+
* keep working unchanged for `ObjectGrid`, `ObjectGantt`,
|
|
34
|
+
* `plugin-dashboard`'s `recordFields` and the `date` cell renderer.
|
|
35
|
+
* `toDisplayDate` is exported from `@object-ui/core` alone (objectui#10183),
|
|
36
|
+
* and so are `toDisplayEndDate` / `toInclusiveEndDay`, the end read and its
|
|
37
|
+
* inverse both gantt surfaces share (objectui#11141).
|
|
38
|
+
*
|
|
39
|
+
* The `datetime` CELL face joined this file in objectui#7443. It used to be a
|
|
40
|
+
* second convention inlined in `DateTimeCellRenderer`: two `Intl` option bags
|
|
41
|
+
* for one field type, kept in step by nothing, while `date` had exactly one.
|
|
42
|
+
* It is `formatDateTime`'s `'compact'` style now, byte-identical to what the
|
|
43
|
+
* cell rendered before.
|
|
44
|
+
*
|
|
45
|
+
* No React, no i18n: the ambient inputs are `Intl`, the clock and the display
|
|
46
|
+
* zone below, and the one phrase `Intl` cannot produce ("Overdue Nd") comes
|
|
47
|
+
* in through the INJECTED `options.t`, the same way `buildDatasetFieldHelpers`
|
|
48
|
+
* in `dataset-format.ts` takes `fieldLabel`.
|
|
49
|
+
*
|
|
50
|
+
* ⚠️ There is a third ambient input, and it is the one this module has to
|
|
51
|
+
* decide about rather than pass on: the zone an instant renders in. A value
|
|
52
|
+
* carrying a time is an instant and renders in the DISPLAY ZONE — the zone the
|
|
53
|
+
* host declared through {@link setDisplayTimeZone} (objectui#11693), else the
|
|
54
|
+
* viewer's own; a DATE-ONLY value names a calendar day, carries no instant,
|
|
55
|
+
* and must render as that day everywhere, whatever the display zone.
|
|
56
|
+
* `toDisplayDate` below is the single parse step that tells the two apart —
|
|
57
|
+
* every function here goes through it (objectui#10110), and so does every
|
|
58
|
+
* caller elsewhere that needs a face or a day comparison none of these
|
|
59
|
+
* functions produce (objectui#10183).
|
|
60
|
+
*/
|
|
61
|
+
// ── The display zone (objectui#11693) ──────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* The zone every instant face below renders in, when the host has declared
|
|
64
|
+
* one; `undefined` means the viewer's own zone (the runtime default), which is
|
|
65
|
+
* what every face rendered in before objectui#11693.
|
|
66
|
+
*/
|
|
67
|
+
let displayTimeZone;
|
|
68
|
+
const displayTimeZoneListeners = new Set();
|
|
69
|
+
/** `true` when this runtime's `Intl` knows `timeZone`. */
|
|
70
|
+
function isKnownTimeZone(timeZone) {
|
|
71
|
+
try {
|
|
72
|
+
new Intl.DateTimeFormat('en-US', { timeZone });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Declare the zone instants render in — the host's half of objectui#11693.
|
|
81
|
+
*
|
|
82
|
+
* ## Why the zone enters HERE and nowhere else
|
|
83
|
+
*
|
|
84
|
+
* Every date and datetime face in the console is a function of this module
|
|
85
|
+
* (see the header), and each caller threads only its `locale`. The zone the
|
|
86
|
+
* signed-in workspace configures (`localization.timezone`, served by
|
|
87
|
+
* `GET /api/v1/auth/me/localization`) is the same for every face on the
|
|
88
|
+
* page, so it is set once, here, and every face reads it — rather than
|
|
89
|
+
* threaded through each call site, where one forgotten caller renders the
|
|
90
|
+
* same instant in a second zone beside the first (objectui#4272 is that
|
|
91
|
+
* defect for the locale). `LocalizationProvider` (`@object-ui/i18n`) is the
|
|
92
|
+
* host that calls this; a renderer never does.
|
|
93
|
+
*
|
|
94
|
+
* `undefined` (or an empty string) clears it: instants render in the
|
|
95
|
+
* viewer's zone again. A name this runtime's `Intl` does not know — a zone
|
|
96
|
+
* newer than the browser's time-zone data, say — clears it too, with a
|
|
97
|
+
* warning, rather than make every face throw the `RangeError` `Intl` raises
|
|
98
|
+
* for it.
|
|
99
|
+
*
|
|
100
|
+
* ⛔ A date-only value never takes the zone; see {@link toDisplayDate}.
|
|
101
|
+
*/
|
|
102
|
+
export function setDisplayTimeZone(timeZone) {
|
|
103
|
+
let next;
|
|
104
|
+
if (timeZone) {
|
|
105
|
+
if (isKnownTimeZone(timeZone))
|
|
106
|
+
next = timeZone;
|
|
107
|
+
else
|
|
108
|
+
console.warn(`[date-display] unknown time zone "${timeZone}"; instants render in the viewer's zone`);
|
|
109
|
+
}
|
|
110
|
+
if (next === displayTimeZone)
|
|
111
|
+
return;
|
|
112
|
+
displayTimeZone = next;
|
|
113
|
+
for (const listener of [...displayTimeZoneListeners])
|
|
114
|
+
listener();
|
|
115
|
+
}
|
|
116
|
+
/** The zone {@link setDisplayTimeZone} declared, or `undefined` for the viewer's own. */
|
|
117
|
+
export function getDisplayTimeZone() {
|
|
118
|
+
return displayTimeZone;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Be told when the display zone changes; returns the unsubscribe. The shape is
|
|
122
|
+
* React's `useSyncExternalStore` contract, which is how `LocalizationProvider`
|
|
123
|
+
* re-renders its consumers once the zone they format in has moved.
|
|
124
|
+
*/
|
|
125
|
+
export function subscribeDisplayTimeZone(listener) {
|
|
126
|
+
displayTimeZoneListeners.add(listener);
|
|
127
|
+
return () => {
|
|
128
|
+
displayTimeZoneListeners.delete(listener);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The `Date`s this module built to NAME A CALENDAR DAY — {@link toDisplayDate}'s
|
|
133
|
+
* local midnight for a date-only value, and {@link toInclusiveEndDay}'s day.
|
|
134
|
+
*
|
|
135
|
+
* The faces below must read such a `Date` in the local zone (its own docblock
|
|
136
|
+
* says so), never in the display zone: west of the viewer it would name the
|
|
137
|
+
* day before. A string still says what it is, but callers hand these `Date`s
|
|
138
|
+
* on — `GridField`'s date cell formats the one `toDisplayDate` returned, and
|
|
139
|
+
* the functions here hand theirs to each other — so the fact travels with the
|
|
140
|
+
* object rather than with the string it came from.
|
|
141
|
+
*/
|
|
142
|
+
const calendarDays = new WeakSet();
|
|
143
|
+
/** The zone a parsed value renders in: the display zone for an instant, the viewer's for a calendar day. */
|
|
144
|
+
function zoneFor(date) {
|
|
145
|
+
return calendarDays.has(date) ? undefined : displayTimeZone;
|
|
146
|
+
}
|
|
147
|
+
/** `Intl` options naming `timeZone`, or none, so a face with no zone keeps its exact options bag. */
|
|
148
|
+
function inZone(timeZone) {
|
|
149
|
+
return timeZone ? { timeZone } : {};
|
|
150
|
+
}
|
|
151
|
+
const dayPartsFormats = new Map();
|
|
152
|
+
/** The calendar day `date` falls on in `timeZone` (the viewer's when `undefined`); `month` is 0-based. */
|
|
153
|
+
function dayIn(date, timeZone) {
|
|
154
|
+
if (!timeZone)
|
|
155
|
+
return { year: date.getFullYear(), month: date.getMonth(), day: date.getDate() };
|
|
156
|
+
let format = dayPartsFormats.get(timeZone);
|
|
157
|
+
if (!format) {
|
|
158
|
+
format = new Intl.DateTimeFormat('en-US', { timeZone, year: 'numeric', month: 'numeric', day: 'numeric' });
|
|
159
|
+
dayPartsFormats.set(timeZone, format);
|
|
160
|
+
}
|
|
161
|
+
const parts = format.formatToParts(date);
|
|
162
|
+
const part = (type) => Number(parts.find((p) => p.type === type)?.value);
|
|
163
|
+
return { year: part('year'), month: part('month') - 1, day: part('day') };
|
|
164
|
+
}
|
|
165
|
+
/** A day as a whole number of days, so two days subtract to their distance with no DST hour in it. */
|
|
166
|
+
function dayNumber({ year, month, day }) {
|
|
167
|
+
const probe = new Date(0);
|
|
168
|
+
probe.setUTCFullYear(year, month, day);
|
|
169
|
+
return Math.round(probe.getTime() / 86400000);
|
|
170
|
+
}
|
|
8
171
|
/**
|
|
9
172
|
* The date-only ISO spelling — `2026-08-01`, and nothing else.
|
|
10
173
|
*
|
|
@@ -147,7 +310,9 @@ export function isRealCalendarDate(dateOnly) {
|
|
|
147
310
|
* ⛔ Read the result with LOCAL getters or local-zone `Intl` formatting only.
|
|
148
311
|
* For a date-only value it is local midnight of the named day, so its
|
|
149
312
|
* `toISOString()` / UTC getters name the previous day east of UTC — hand
|
|
150
|
-
* those the stored value, never this.
|
|
313
|
+
* those the stored value, never this. The faces in this module know such a
|
|
314
|
+
* `Date` when they are handed one and keep it out of the display zone
|
|
315
|
+
* (objectui#11693, see `calendarDays`).
|
|
151
316
|
*/
|
|
152
317
|
export function toDisplayDate(value) {
|
|
153
318
|
const parsed = value instanceof Date ? value : new Date(value);
|
|
@@ -169,6 +334,7 @@ export function toDisplayDate(value) {
|
|
|
169
334
|
// Setting the year back is co-extensive with that legacy mapping and a
|
|
170
335
|
// no-op on every other year.
|
|
171
336
|
local.setFullYear(year);
|
|
337
|
+
calendarDays.add(local);
|
|
172
338
|
return local;
|
|
173
339
|
}
|
|
174
340
|
/**
|
|
@@ -233,6 +399,7 @@ export function toDisplayEndDate(value) {
|
|
|
233
399
|
export function toInclusiveEndDay(end) {
|
|
234
400
|
const day = new Date(end.getTime() - 1);
|
|
235
401
|
day.setHours(0, 0, 0, 0);
|
|
402
|
+
calendarDays.add(day);
|
|
236
403
|
return day;
|
|
237
404
|
}
|
|
238
405
|
/**
|
|
@@ -289,6 +456,11 @@ function absoluteFallbackOptions(options) {
|
|
|
289
456
|
* past dates render as plain "N days ago" instead. The overdue phrase has no
|
|
290
457
|
* `Intl` equivalent, so it resolves through `options.t` (key
|
|
291
458
|
* `fields.relativeDate.overdue`) with an English fallback.
|
|
459
|
+
*
|
|
460
|
+
* Days are counted in the display zone (objectui#11693): "today" is the
|
|
461
|
+
* display zone's today, and an instant falls on its day there. A date-only
|
|
462
|
+
* value keeps the day it names and is counted from that same today, so an
|
|
463
|
+
* instant and a date-only value on one day read one phrase.
|
|
292
464
|
*/
|
|
293
465
|
export function formatRelativeDate(value, options) {
|
|
294
466
|
if (value === null || value === undefined || value === '')
|
|
@@ -296,11 +468,7 @@ export function formatRelativeDate(value, options) {
|
|
|
296
468
|
const date = toDisplayDate(value);
|
|
297
469
|
if (!(date instanceof Date) || isNaN(date.getTime()))
|
|
298
470
|
return '—';
|
|
299
|
-
const
|
|
300
|
-
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
301
|
-
const startOfDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
302
|
-
const diffMs = startOfDate.getTime() - startOfToday.getTime();
|
|
303
|
-
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
471
|
+
const diffDays = dayNumber(dayIn(date, zoneFor(date))) - dayNumber(dayIn(new Date(), displayTimeZone));
|
|
304
472
|
// Beyond the ±7-day window, fall back to the absolute (already localized) form.
|
|
305
473
|
if (diffDays < -7 || diffDays > 7)
|
|
306
474
|
return formatDate(date, undefined, absoluteFallbackOptions(options));
|
|
@@ -354,6 +522,9 @@ export function formatDate(value, style, options) {
|
|
|
354
522
|
if (!(date instanceof Date) || isNaN(date.getTime()))
|
|
355
523
|
return '—';
|
|
356
524
|
const effectiveStyle = style ?? options?.style;
|
|
525
|
+
// An instant names its day in the display zone; a calendar day is its own
|
|
526
|
+
// day in every zone (objectui#11693).
|
|
527
|
+
const zone = zoneFor(date);
|
|
357
528
|
if (effectiveStyle === 'short') {
|
|
358
529
|
// Compact format for mobile: "Jan 15, '24" / "1月 15, '24".
|
|
359
530
|
// Only the MONTH token is localized: the surrounding compact shape (day,
|
|
@@ -361,10 +532,9 @@ export function formatDate(value, style, options) {
|
|
|
361
532
|
// cards, not a locale-derived one. The tag comes from `options.locale`
|
|
362
533
|
// like the default branch below — hardcoding `'en-US'` here made this the
|
|
363
534
|
// one branch that ignored a locale its caller had threaded (objectui#4272).
|
|
364
|
-
const month = date.toLocaleDateString(options?.locale, { month: 'short' });
|
|
365
|
-
const day = date
|
|
366
|
-
|
|
367
|
-
return `${month} ${day}, '${year}`;
|
|
535
|
+
const month = date.toLocaleDateString(options?.locale, { month: 'short', ...inZone(zone) });
|
|
536
|
+
const { day, year } = dayIn(date, zone);
|
|
537
|
+
return `${month} ${day}, '${String(year).slice(-2)}`;
|
|
368
538
|
}
|
|
369
539
|
if (effectiveStyle === 'relative') {
|
|
370
540
|
return formatRelativeDate(date, options);
|
|
@@ -374,11 +544,12 @@ export function formatDate(value, style, options) {
|
|
|
374
544
|
// because the year is rarely useful for in-progress records and the
|
|
375
545
|
// verbose "2026年7月21日" form crowds cards and table cells. Past- /
|
|
376
546
|
// future-year dates keep the year so users can disambiguate.
|
|
377
|
-
const isCurrentYear = date.
|
|
547
|
+
const isCurrentYear = dayIn(date, zone).year === dayIn(new Date(), displayTimeZone).year;
|
|
378
548
|
return date.toLocaleDateString(options?.locale, {
|
|
379
549
|
year: isCurrentYear ? undefined : 'numeric',
|
|
380
550
|
month: 'short',
|
|
381
551
|
day: 'numeric',
|
|
552
|
+
...inZone(zone),
|
|
382
553
|
});
|
|
383
554
|
}
|
|
384
555
|
/**
|
|
@@ -402,11 +573,15 @@ export function formatDateTimeCompactParts(value, options) {
|
|
|
402
573
|
const date = toDisplayDate(value);
|
|
403
574
|
if (!(date instanceof Date) || isNaN(date.getTime()))
|
|
404
575
|
return null;
|
|
576
|
+
// Both halves in the display zone, so the day and the time of one instant
|
|
577
|
+
// never come from two zones (objectui#11693).
|
|
578
|
+
const zone = zoneFor(date);
|
|
405
579
|
return {
|
|
406
580
|
date: date.toLocaleDateString(options?.locale, {
|
|
407
581
|
month: 'numeric',
|
|
408
582
|
day: 'numeric',
|
|
409
583
|
year: 'numeric',
|
|
584
|
+
...inZone(zone),
|
|
410
585
|
}),
|
|
411
586
|
// `hour12` stays declared: this is the compact Airtable-style cell, and
|
|
412
587
|
// the 12-hour face is its design, not a locale artefact. Locales that
|
|
@@ -415,6 +590,7 @@ export function formatDateTimeCompactParts(value, options) {
|
|
|
415
590
|
hour: 'numeric',
|
|
416
591
|
minute: '2-digit',
|
|
417
592
|
hour12: true,
|
|
593
|
+
...inZone(zone),
|
|
418
594
|
}).toLowerCase(),
|
|
419
595
|
};
|
|
420
596
|
}
|
|
@@ -442,6 +618,10 @@ export function formatDateTimeCompactParts(value, options) {
|
|
|
442
618
|
* hard it tried — it always handed `Intl` an `undefined` tag, i.e. the
|
|
443
619
|
* MACHINE's locale, which is neither of the repo's two locale channels.
|
|
444
620
|
* Callers should pass the tag from `useDisplayLocale()`.
|
|
621
|
+
*
|
|
622
|
+
* The zone is NOT an option: an instant renders in the display zone the host
|
|
623
|
+
* declared with {@link setDisplayTimeZone}, else the viewer's, on every face
|
|
624
|
+
* this module produces (objectui#11693).
|
|
445
625
|
*/
|
|
446
626
|
export function formatDateTime(value, options) {
|
|
447
627
|
if (value === null || value === undefined || value === '')
|
|
@@ -459,5 +639,6 @@ export function formatDateTime(value, options) {
|
|
|
459
639
|
day: 'numeric',
|
|
460
640
|
hour: '2-digit',
|
|
461
641
|
minute: '2-digit',
|
|
642
|
+
...inZone(zoneFor(date)),
|
|
462
643
|
});
|
|
463
644
|
}
|
|
@@ -98,7 +98,8 @@
|
|
|
98
98
|
* - the dashboard's relation/link test — `isLookupType` in
|
|
99
99
|
* `packages/plugin-dashboard/src/recordFields.tsx` (objectui#5692);
|
|
100
100
|
* - the action-param RESOLVER (one step before `paramToField`) —
|
|
101
|
-
* `packages/
|
|
101
|
+
* `packages/fields/src/resolveActionParams.ts` (objectui#5874; moved out of
|
|
102
|
+
* app-shell by objectui#12105 so the bulk dialog shares it);
|
|
102
103
|
* - the detail header's relation test — `HeaderHighlight.tsx` in
|
|
103
104
|
* `packages/plugin-detail` (objectui#5874);
|
|
104
105
|
* - the detail drawer's relation test — `RecordDetailDrawer.tsx` in
|
|
@@ -134,10 +135,11 @@
|
|
|
134
135
|
* (objectui#4790). The form now DERIVES from this set instead of restating it:
|
|
135
136
|
*
|
|
136
137
|
* form's data-source rule == EXPANDABLE_FIELD_TYPES
|
|
137
|
-
* +
|
|
138
|
+
* + the widget-hint-only picker names
|
|
138
139
|
*
|
|
139
140
|
* The extension is one-directional and cannot fold back into this set:
|
|
140
|
-
* `object-ref` / `filter-condition` / `recipient-picker`
|
|
141
|
+
* `object-ref` / `filter-condition` / `recipient-picker` (and the registry-name
|
|
142
|
+
* pickers `position-ref` / `permission-ref`, objectui#7611) are widget HINTS, never
|
|
141
143
|
* declarable field types (`widgetHintOnly: true` in
|
|
142
144
|
* `app-shell/src/utils/paramValueShape.ts`), so no object schema can produce a
|
|
143
145
|
* field whose `type` is one of them.
|
|
@@ -146,7 +148,7 @@
|
|
|
146
148
|
* of the above (`lookup` / `master_detail` / `user` — no `tree`, no pickers, and
|
|
147
149
|
* for a while a fifth spelling, `owner`, that objectui#4814 retired). It now
|
|
148
150
|
* derives from this set with NO extension, because the bulk dialog's params
|
|
149
|
-
* cannot produce the
|
|
151
|
+
* cannot produce the widget-hint names (objectui#4815):
|
|
150
152
|
*
|
|
151
153
|
* bulk dialog's data-source rule == EXPANDABLE_FIELD_TYPES
|
|
152
154
|
*
|
|
@@ -99,7 +99,8 @@ import { columnIdentity } from './column-identity.js';
|
|
|
99
99
|
* - the dashboard's relation/link test — `isLookupType` in
|
|
100
100
|
* `packages/plugin-dashboard/src/recordFields.tsx` (objectui#5692);
|
|
101
101
|
* - the action-param RESOLVER (one step before `paramToField`) —
|
|
102
|
-
* `packages/
|
|
102
|
+
* `packages/fields/src/resolveActionParams.ts` (objectui#5874; moved out of
|
|
103
|
+
* app-shell by objectui#12105 so the bulk dialog shares it);
|
|
103
104
|
* - the detail header's relation test — `HeaderHighlight.tsx` in
|
|
104
105
|
* `packages/plugin-detail` (objectui#5874);
|
|
105
106
|
* - the detail drawer's relation test — `RecordDetailDrawer.tsx` in
|
|
@@ -135,10 +136,11 @@ import { columnIdentity } from './column-identity.js';
|
|
|
135
136
|
* (objectui#4790). The form now DERIVES from this set instead of restating it:
|
|
136
137
|
*
|
|
137
138
|
* form's data-source rule == EXPANDABLE_FIELD_TYPES
|
|
138
|
-
* +
|
|
139
|
+
* + the widget-hint-only picker names
|
|
139
140
|
*
|
|
140
141
|
* The extension is one-directional and cannot fold back into this set:
|
|
141
|
-
* `object-ref` / `filter-condition` / `recipient-picker`
|
|
142
|
+
* `object-ref` / `filter-condition` / `recipient-picker` (and the registry-name
|
|
143
|
+
* pickers `position-ref` / `permission-ref`, objectui#7611) are widget HINTS, never
|
|
142
144
|
* declarable field types (`widgetHintOnly: true` in
|
|
143
145
|
* `app-shell/src/utils/paramValueShape.ts`), so no object schema can produce a
|
|
144
146
|
* field whose `type` is one of them.
|
|
@@ -147,7 +149,7 @@ import { columnIdentity } from './column-identity.js';
|
|
|
147
149
|
* of the above (`lookup` / `master_detail` / `user` — no `tree`, no pickers, and
|
|
148
150
|
* for a while a fifth spelling, `owner`, that objectui#4814 retired). It now
|
|
149
151
|
* derives from this set with NO extension, because the bulk dialog's params
|
|
150
|
-
* cannot produce the
|
|
152
|
+
* cannot produce the widget-hint names (objectui#4815):
|
|
151
153
|
*
|
|
152
154
|
* bulk dialog's data-source rule == EXPANDABLE_FIELD_TYPES
|
|
153
155
|
*
|
|
@@ -174,8 +174,12 @@ export declare function convertFiltersToAST(filter: Record<string, any>): Filter
|
|
|
174
174
|
*
|
|
175
175
|
* The third is the one that kept getting lost. Renderers tested `source.length
|
|
176
176
|
* > 0` before using it, which is `undefined > 0` for an object — so a
|
|
177
|
-
* `table.defaultFilters` (declared `Record<string, any>`
|
|
178
|
-
*
|
|
177
|
+
* `table.defaultFilters` (declared `Record<string, any>` until objectui#6152
|
|
178
|
+
* round 10; the protocol's `ViewFilterRule` array since, and the record no
|
|
179
|
+
* longer validates there) was DROPPED and the view returned every record.
|
|
180
|
+
* Silently: no error, just a wider answer. That narrowing moved what an author
|
|
181
|
+
* may write, not this sink: the object arm below still lowers a record whatever
|
|
182
|
+
* caller hands it one.
|
|
179
183
|
*
|
|
180
184
|
* The FIRST never worked at all (objectui#3431). The array branch returned
|
|
181
185
|
* every array VERBATIM, so a saved view's `ViewFilterRule[]` travelled to
|
|
@@ -1256,8 +1256,12 @@ function viewFilterRuleToNode(rule) {
|
|
|
1256
1256
|
*
|
|
1257
1257
|
* The third is the one that kept getting lost. Renderers tested `source.length
|
|
1258
1258
|
* > 0` before using it, which is `undefined > 0` for an object — so a
|
|
1259
|
-
* `table.defaultFilters` (declared `Record<string, any>`
|
|
1260
|
-
*
|
|
1259
|
+
* `table.defaultFilters` (declared `Record<string, any>` until objectui#6152
|
|
1260
|
+
* round 10; the protocol's `ViewFilterRule` array since, and the record no
|
|
1261
|
+
* longer validates there) was DROPPED and the view returned every record.
|
|
1262
|
+
* Silently: no error, just a wider answer. That narrowing moved what an author
|
|
1263
|
+
* may write, not this sink: the object arm below still lowers a record whatever
|
|
1264
|
+
* caller hands it one.
|
|
1261
1265
|
*
|
|
1262
1266
|
* The FIRST never worked at all (objectui#3431). The array branch returned
|
|
1263
1267
|
* every array VERBATIM, so a saved view's `ViewFilterRule[]` travelled to
|
|
@@ -199,6 +199,12 @@ export declare function isListViewVisualization(value: unknown): value is ListVi
|
|
|
199
199
|
* stays absent, because the defaults are per-toggle (search/sort/filter/
|
|
200
200
|
* rowHeight/group default ON, hideFields/rowColor default OFF) and belong to
|
|
201
201
|
* the renderer, not to the vocabulary bridge.
|
|
202
|
+
* - the view's `inlineEdit` → `userActions.editInline` (objectui#5144), with
|
|
203
|
+
* the value carried over and an explicit `editInline` winning. `inlineEdit`
|
|
204
|
+
* stays on the result, because `ListView` seeds the grid's edit mode from
|
|
205
|
+
* it. See {@link foldsInlineEditIntoEditInline}. An absent pair stays
|
|
206
|
+
* absent here as well; `ListView` reads that as on, the spec's default
|
|
207
|
+
* (objectstack#22605).
|
|
202
208
|
* - `aria: { label, describedBy }` → the spec's `AriaProps`
|
|
203
209
|
* (`{ ariaLabel, ariaDescribedBy }`), and `sharing: { visibility, enabled }`
|
|
204
210
|
* → the spec's `ViewSharing` (`{ type }`) — #2890 scope A step 5. `aria.live`
|
|
@@ -221,8 +227,8 @@ export declare function isListViewVisualization(value: unknown): value is ListVi
|
|
|
221
227
|
* · an already-present `objectName` WINS — this fold only fills a gap, it
|
|
222
228
|
* never re-points a binding that already resolves;
|
|
223
229
|
* · `data` is NOT deleted. Every other fold deletes because the legacy key
|
|
224
|
-
* has one meaning and one home; `data` has
|
|
225
|
-
*
|
|
230
|
+
* has one meaning and one home; `data` has more providers than
|
|
231
|
+
* `object`, `value` is read live in `ListView`, and the whole block is FORWARDED to child
|
|
226
232
|
* views (the gantt branch), whose own `getDataConfig` reads `data` before
|
|
227
233
|
* `objectName`. Deleting it for one provider would rewrite what a child
|
|
228
234
|
* resolves. Nothing in `ListView` reads `data.provider === 'object'`, so
|
|
@@ -245,15 +251,13 @@ export declare function isListViewVisualization(value: unknown): value is ListVi
|
|
|
245
251
|
* breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
|
|
246
252
|
* above, so a view that spells its column list the legacy way still gets its
|
|
247
253
|
* entries canonicalized.
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* `columns` reaches it. Every other reader of these four was already
|
|
257
|
-
* canonical-first and is unaffected.
|
|
254
|
+
*
|
|
255
|
+
* No longer folded: the four PER-VIEW-TYPE config aliases phase 3 carried over
|
|
256
|
+
* (`kanban.groupField` → `groupByField`, `kanban.cardFields` → `columns`,
|
|
257
|
+
* `gallery.imageField` → `coverField`, `timeline.dateField` →
|
|
258
|
+
* `startDateField`). objectui#6152 round 14 retired that fold with every
|
|
259
|
+
* renderer-side reader of the four: each list-view door refuses them by name
|
|
260
|
+
* (rounds 11 and 12), and a stored block that still carries one passes through
|
|
261
|
+
* here untouched and binds nothing downstream.
|
|
258
262
|
*/
|
|
259
263
|
export declare function normalizeListViewSchema<T>(schema: T): T;
|