@object-ui/core 17.6.0 → 17.7.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 +6610 -0
- package/README.md +101 -14
- package/dist/actions/ActionEngine.d.ts +4 -2
- package/dist/actions/ActionEngine.js +13 -8
- package/dist/actions/ActionRunner.d.ts +288 -27
- package/dist/actions/ActionRunner.js +443 -63
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/actionKeys.d.ts +9 -7
- package/dist/actions/actionKeys.js +57 -14
- package/dist/actions/actionResponse.js +2 -2
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/recordDelete.d.ts +118 -0
- package/dist/actions/recordDelete.js +112 -0
- package/dist/adapters/ValueDataSource.d.ts +6 -1
- package/dist/adapters/ValueDataSource.js +1090 -98
- package/dist/adapters/batchTransaction.d.ts +1 -1
- package/dist/adapters/batchTransaction.js +1 -1
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +1 -1
- package/dist/data-scope/ViewDataProvider.js +21 -1
- package/dist/data-scope/element-data-source.d.ts +261 -7
- package/dist/data-scope/element-data-source.js +342 -52
- package/dist/data-scope/index.d.ts +9 -4
- package/dist/data-scope/index.js +9 -4
- package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
- package/dist/evaluator/ExpressionEvaluator.js +184 -12
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +117 -16
- package/dist/evaluator/fieldRules.d.ts +88 -4
- package/dist/evaluator/fieldRules.js +203 -31
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +103 -34
- package/dist/evaluator/listConditional.js +127 -50
- package/dist/evaluator/optionRules.d.ts +77 -6
- package/dist/evaluator/optionRules.js +87 -10
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
- package/dist/evaluator/rowPredicateCanon.js +65 -115
- package/dist/evaluator/subjectPermissions.d.ts +76 -0
- package/dist/evaluator/subjectPermissions.js +44 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +80 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +358 -31
- package/dist/registry/Registry.js +263 -11
- package/dist/registry/WidgetRegistry.js +7 -3
- package/dist/registry/html-tier-intrinsics.d.ts +90 -0
- package/dist/registry/html-tier-intrinsics.js +110 -0
- package/dist/registry/public-blocks.d.ts +8 -0
- package/dist/registry/public-blocks.js +57 -0
- package/dist/theme/ThemeEngine.js +1 -1
- package/dist/utils/chart-category-key.d.ts +82 -0
- package/dist/utils/chart-category-key.js +84 -0
- package/dist/utils/chart-measure-key.d.ts +64 -0
- package/dist/utils/chart-measure-key.js +65 -0
- package/dist/utils/chart-presentation.d.ts +95 -112
- package/dist/utils/chart-presentation.js +169 -104
- package/dist/utils/chart-series.d.ts +131 -5
- package/dist/utils/chart-series.js +98 -16
- package/dist/utils/column-sortability.d.ts +157 -0
- package/dist/utils/column-sortability.js +217 -0
- package/dist/utils/dashboard-filters.d.ts +38 -6
- package/dist/utils/dashboard-filters.js +107 -61
- package/dist/utils/dataset-format.d.ts +211 -30
- package/dist/utils/dataset-format.js +389 -46
- package/dist/utils/date-display.d.ts +354 -0
- package/dist/utils/date-display.js +463 -0
- package/dist/utils/date-macros.js +7 -0
- package/dist/utils/drill-down.d.ts +78 -0
- package/dist/utils/drill-down.js +93 -0
- package/dist/utils/emptiness.d.ts +96 -0
- package/dist/utils/emptiness.js +101 -0
- package/dist/utils/expand-fields.d.ts +76 -6
- package/dist/utils/expand-fields.js +76 -6
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +152 -4
- package/dist/utils/filter-converter.js +1092 -22
- package/dist/utils/filter-tokens.d.ts +49 -5
- package/dist/utils/filter-tokens.js +105 -33
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/managedBy.d.ts +3 -3
- package/dist/utils/managedBy.js +5 -5
- package/dist/utils/native-date-value.d.ts +48 -0
- package/dist/utils/native-date-value.js +167 -0
- package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
- package/dist/utils/non-grid-row-ceiling.js +141 -0
- package/dist/utils/normalize-list-view.d.ts +121 -1
- package/dist/utils/normalize-list-view.js +186 -6
- package/dist/utils/number-display.d.ts +63 -14
- package/dist/utils/number-display.js +34 -12
- package/dist/utils/object-aggregate-query.d.ts +113 -0
- package/dist/utils/object-aggregate-query.js +50 -0
- package/dist/utils/parent-scope.d.ts +123 -0
- package/dist/utils/parent-scope.js +140 -0
- package/dist/utils/predicate-fields.d.ts +45 -12
- package/dist/utils/predicate-fields.js +137 -12
- package/dist/utils/record-color.d.ts +75 -0
- package/dist/utils/record-color.js +65 -0
- package/dist/utils/record-source.d.ts +298 -0
- package/dist/utils/record-source.js +331 -0
- package/dist/utils/record-title.d.ts +148 -8
- package/dist/utils/record-title.js +220 -26
- package/dist/utils/reference-keys.d.ts +21 -22
- package/dist/utils/reference-keys.js +532 -29
- package/dist/utils/sort-query.d.ts +115 -15
- package/dist/utils/sort-query.js +240 -13
- package/dist/utils/text-comparand.d.ts +92 -0
- package/dist/utils/text-comparand.js +99 -0
- package/dist/utils/without-denied-fields.d.ts +49 -0
- package/dist/utils/without-denied-fields.js +52 -0
- package/dist/validation/index.d.ts +8 -4
- package/dist/validation/index.js +8 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +200 -82
- package/dist/validation/validators/object-validation-engine.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -3
- package/package.json +8 -9
- package/dist/data-scope/DataScopeManager.d.ts +0 -127
- package/dist/data-scope/DataScopeManager.js +0 -229
- package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
- package/dist/protocols/ResponsiveProtocol.js +0 -158
- package/dist/validation/validation-engine.d.ts +0 -88
- package/dist/validation/validation-engine.js +0 -434
|
@@ -6,12 +6,39 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import { liftLegacyGlobalFilterDefault } from '@object-ui/types';
|
|
9
|
-
import { DATE_RANGE_PRESETS } from '@objectstack/spec/ui';
|
|
9
|
+
import { DATE_RANGE_PRESETS, DashboardSchema as SpecDashboardSchema } from '@objectstack/spec/ui';
|
|
10
10
|
import { resolveDateMacros } from './date-macros.js';
|
|
11
11
|
/** Reserved filter name for the dashboard's built-in date range. */
|
|
12
12
|
export const DATE_RANGE_FILTER_NAME = 'dateRange';
|
|
13
13
|
/** Default target field for the built-in date range filter. */
|
|
14
14
|
const DATE_RANGE_DEFAULT_FIELD = 'created_at';
|
|
15
|
+
/**
|
|
16
|
+
* The preset an authored `dateRange` resolves to when it omits `defaultRange`
|
|
17
|
+
* (objectui#10339) — READ FROM THE SPEC, never restated here.
|
|
18
|
+
*
|
|
19
|
+
* `@objectstack/spec` declares `DashboardSchema.dateRange.defaultRange` with a
|
|
20
|
+
* `.default(...)`, so the platform's parse of `dateRange: { field: 'created_at' }`
|
|
21
|
+
* carries that preset, and a renderer that treated the omission as "no filter"
|
|
22
|
+
* showed an UNFILTERED dashboard for a document the protocol says is filtered
|
|
23
|
+
* (#7759 ruling 5617465269, rule 1: the read site implements every arm the spec
|
|
24
|
+
* declares). The value is derived by parsing an EMPTY `dateRange` element
|
|
25
|
+
* through the spec's own schema: the only keys that come back are the ones the
|
|
26
|
+
* spec supplies by default, so this reads the spec's answer rather than a
|
|
27
|
+
* hand-copied string that could drift from it. `allowCustomRange` is the
|
|
28
|
+
* sibling default and needs no such read — it is consumed as `!== false`,
|
|
29
|
+
* which already is its spec default.
|
|
30
|
+
*
|
|
31
|
+
* Computed on first use and cached: `DashboardSchema` is a lazy schema, and
|
|
32
|
+
* forcing its construction at module load would cost every importer of
|
|
33
|
+
* `@object-ui/core` for a value only a dashboard with a bare `dateRange` needs.
|
|
34
|
+
*/
|
|
35
|
+
let specDateRangeDefault;
|
|
36
|
+
function specDefaultDateRangePreset() {
|
|
37
|
+
if (specDateRangeDefault === undefined) {
|
|
38
|
+
specDateRangeDefault = SpecDashboardSchema.shape.dateRange.parse({})?.defaultRange;
|
|
39
|
+
}
|
|
40
|
+
return specDateRangeDefault;
|
|
41
|
+
}
|
|
15
42
|
/**
|
|
16
43
|
* Date-range presets → date-macro token bounds. Tokens stay symbolic in the
|
|
17
44
|
* generated condition; every widget renderer resolves them at query time via
|
|
@@ -72,7 +99,7 @@ function presetBounds(preset) {
|
|
|
72
99
|
* protect.
|
|
73
100
|
*
|
|
74
101
|
* What stays local is the BOUNDS table above, which is the other half of
|
|
75
|
-
* #4614's design ("the two vocabularies live one import apart and neither
|
|
102
|
+
* objectstack-ai/objectstack#4614's design ("the two vocabularies live one import apart and neither
|
|
76
103
|
* restates the other's grammar"): the spec owns which presets exist, this
|
|
77
104
|
* module owns what each one resolves to in date-macro tokens.
|
|
78
105
|
*/
|
|
@@ -111,13 +138,13 @@ function warnDateFilter(message) {
|
|
|
111
138
|
console.warn(`[dashboard-filters] ${message}`);
|
|
112
139
|
}
|
|
113
140
|
/**
|
|
114
|
-
* Dev-mode gate, matching `actions/actionKeys.ts` — a
|
|
115
|
-
*
|
|
141
|
+
* Dev-mode gate, matching `actions/actionKeys.ts` — a warning that floods a
|
|
142
|
+
* production console is a warning that gets muted.
|
|
116
143
|
*/
|
|
117
144
|
const isDev = () => globalThis.process?.env?.NODE_ENV !==
|
|
118
145
|
'production';
|
|
119
146
|
/**
|
|
120
|
-
* Warn-once memo for the bare-string `options`
|
|
147
|
+
* Warn-once memo for the DROPPED bare-string `options` members (objectui#4356).
|
|
121
148
|
*
|
|
122
149
|
* Keyed by filter NAME **and** the offending values, deliberately — the same
|
|
123
150
|
* reasoning `warnOnUnknownActionKeys` records for its own memo. Keying on the
|
|
@@ -132,7 +159,13 @@ const isDev = () => globalThis.process?.env?.NODE_ENV !==
|
|
|
132
159
|
* flood the dedupe exists to prevent.
|
|
133
160
|
*/
|
|
134
161
|
const warnedShorthandOptions = new Set();
|
|
135
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* Reset the dropped-shorthand warn-once memo. Exported for tests.
|
|
164
|
+
*
|
|
165
|
+
* Kept through the objectui#4356 retirement: the memo it clears outlived the
|
|
166
|
+
* deprecation warning, because the warning that replaced it (a member was
|
|
167
|
+
* DROPPED, not lifted) dedupes on the same key for the same render-path reason.
|
|
168
|
+
*/
|
|
136
169
|
export function resetDashboardFilterWarnings() {
|
|
137
170
|
warnedShorthandOptions.clear();
|
|
138
171
|
}
|
|
@@ -252,45 +285,40 @@ function normalizeDateDefault(type, defaultValue) {
|
|
|
252
285
|
}
|
|
253
286
|
/**
|
|
254
287
|
* Normalize a filter's static `options` declaration to `{ value, label }`
|
|
255
|
-
* pairs
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* un-normalized option crashes React — this is the single place both shapes
|
|
259
|
-
* converge.
|
|
288
|
+
* pairs — the @objectstack/spec `GlobalFilterSchema.options` form, and the
|
|
289
|
+
* only form this function emits. Rendering an un-normalized option crashes
|
|
290
|
+
* React, so every member that reaches a renderer passes through here.
|
|
260
291
|
*
|
|
261
|
-
* ## The shorthand
|
|
292
|
+
* ## The bare-string shorthand is RETIRED (objectui#4356, objectstack#7917)
|
|
262
293
|
*
|
|
263
294
|
* Maintainer ruling of 2026-08-12 on objectstack#7917, verbatim 「7917 ②」:
|
|
264
|
-
* option ② — **the spec stays strict; the runtime bare-string lift retires
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* The
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* stored population is no longer growing while this warning asks authors to
|
|
286
|
-
* migrate. Warning authors while the docs still taught the form would have been
|
|
287
|
-
* a contradiction users report as a bug.
|
|
295
|
+
* option ② — **the spec stays strict; the runtime bare-string lift retires.**
|
|
296
|
+
* The window closed on the maintainer's 2026-09-02 ruling, verbatim
|
|
297
|
+
* 「objectstack#7917 不考虑现有数据」: Phase 2 proceeds on release cadence
|
|
298
|
+
* alone, with no live-tenant survey and no migration entry.
|
|
299
|
+
*
|
|
300
|
+
* So `options: ['EMEA']` — which the Phase 1 build (PR #4601) lifted to
|
|
301
|
+
* `{ value: 'EMEA', label: 'EMEA' }` under a deprecation warning — now yields
|
|
302
|
+
* NO option. A member that is not an object is skipped, exactly as a nullish
|
|
303
|
+
* member or an object with no `value` already was: a shorthand-only filter
|
|
304
|
+
* resolves with no `options`, and a mixed array keeps only its object members.
|
|
305
|
+
* The runtime reads the document the way the spec does, which is the end
|
|
306
|
+
* state option ② named — one strict contract, no renderer dialect beside it
|
|
307
|
+
* (AGENTS.md #0.1). ⛔ No lift, alias, fallback or migration returns here.
|
|
308
|
+
*
|
|
309
|
+
* The skip is not silent. `warnDroppedShorthandOptions` names the filter, the
|
|
310
|
+
* dropped members and the rewrite, once per filter per session — ADR-0078 §4:
|
|
311
|
+
* where the runtime skips an instance it emits a dev-mode diagnostic rather
|
|
312
|
+
* than swallowing it, and a STORED dashboard is the one document no author-time
|
|
313
|
+
* gate ever re-reads, so an empty select with nothing in the console would read
|
|
314
|
+
* as "no data". What that warning must NOT do is promise a lift: the Phase 1
|
|
315
|
+
* text said "still lifted here", and that sentence retired with the arm.
|
|
288
316
|
*
|
|
289
317
|
* ## What is normalized, and what is deliberately NOT (objectui#4032 / #4163)
|
|
290
318
|
*
|
|
291
|
-
* The PAIR SHAPE is normalized (`value` stringified
|
|
292
|
-
*
|
|
293
|
-
*
|
|
319
|
+
* The PAIR SHAPE is normalized (`value` stringified). The LABEL's authoring
|
|
320
|
+
* vocabulary is carried through untouched, because `label` is `I18nLabel` — a
|
|
321
|
+
* string OR an inline per-locale map.
|
|
294
322
|
*
|
|
295
323
|
* This line used to read:
|
|
296
324
|
*
|
|
@@ -312,8 +340,8 @@ function normalizeFilterOptions(options, filterName) {
|
|
|
312
340
|
if (!Array.isArray(options) || options.length === 0)
|
|
313
341
|
return undefined;
|
|
314
342
|
const normalized = [];
|
|
315
|
-
/** Every
|
|
316
|
-
const
|
|
343
|
+
/** Every non-object member, in authored order — one warning names them all. */
|
|
344
|
+
const dropped = [];
|
|
317
345
|
for (const o of options) {
|
|
318
346
|
if (o === null || o === undefined)
|
|
319
347
|
continue;
|
|
@@ -329,39 +357,48 @@ function normalizeFilterOptions(options, filterName) {
|
|
|
329
357
|
});
|
|
330
358
|
}
|
|
331
359
|
else {
|
|
332
|
-
shorthand
|
|
333
|
-
|
|
360
|
+
// The retired shorthand arm (objectui#4356): nothing is pushed for this
|
|
361
|
+
// member. It is recorded for the warning and yields no option.
|
|
362
|
+
dropped.push(o);
|
|
334
363
|
}
|
|
335
364
|
}
|
|
336
|
-
if (
|
|
337
|
-
|
|
365
|
+
if (dropped.length > 0)
|
|
366
|
+
warnDroppedShorthandOptions(filterName, dropped);
|
|
338
367
|
return normalized.length > 0 ? normalized : undefined;
|
|
339
368
|
}
|
|
340
369
|
/**
|
|
341
|
-
* Say the
|
|
342
|
-
*
|
|
370
|
+
* Say the dropped members out loud — once per offending filter per session,
|
|
371
|
+
* naming the filter, the members that yielded no option, and the rewrite.
|
|
343
372
|
*
|
|
344
|
-
* Collected per FILTER rather than per
|
|
373
|
+
* Collected per FILTER rather than per member: a filter declaring
|
|
345
374
|
* `['EMEA', 'APAC', 'AMER']` is one authoring mistake in one place, so it earns
|
|
346
375
|
* one warning carrying all three values, not three warnings the author has to
|
|
347
376
|
* reassemble. A MIXED array (`[{ value: 'won', … }, 'lost']`) names only the
|
|
348
|
-
*
|
|
349
|
-
*
|
|
377
|
+
* dropped members — the object members rendered, and re-reporting them is noise.
|
|
378
|
+
*
|
|
379
|
+
* Dev-mode only and memoised, exactly as the Phase 1 deprecation warning was:
|
|
380
|
+
* `resolveDashboardFilterDefs` runs on every dashboard render, and a warning
|
|
381
|
+
* that floods a console is a warning that gets muted. What changed is the
|
|
382
|
+
* sentence — this one reports a member that was DROPPED and promises no lift.
|
|
350
383
|
*/
|
|
351
|
-
function
|
|
384
|
+
function warnDroppedShorthandOptions(filterName, dropped) {
|
|
352
385
|
if (!isDev())
|
|
353
386
|
return;
|
|
354
|
-
|
|
387
|
+
// A string is quoted so `"42"` and `42` stay distinguishable; anything else is
|
|
388
|
+
// spelled as itself — `String()` rather than `JSON.stringify()`, because a
|
|
389
|
+
// programmatic caller can hand this a value JSON cannot carry.
|
|
390
|
+
const spelled = dropped.map((v) => (typeof v === 'string' ? JSON.stringify(v) : String(v)));
|
|
391
|
+
const memo = `${filterName}:${spelled.join(',')}`;
|
|
355
392
|
if (warnedShorthandOptions.has(memo))
|
|
356
393
|
return;
|
|
357
394
|
warnedShorthandOptions.add(memo);
|
|
358
|
-
const canonical =
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
`and
|
|
363
|
-
`
|
|
364
|
-
`
|
|
395
|
+
const canonical = dropped
|
|
396
|
+
.map((v) => `{ value: ${JSON.stringify(String(v))}, label: ${JSON.stringify(String(v))} }`)
|
|
397
|
+
.join(', ');
|
|
398
|
+
warnDateFilter(`filter "${filterName}": \`options\` members ${spelled.join(', ')} are not \`{ value, label }\` ` +
|
|
399
|
+
`objects and were DROPPED — the bare-string shorthand is no longer lifted (objectui#4356), so the ` +
|
|
400
|
+
`filter renders without them. Rewrite the stored dashboard to @objectstack/spec's pair form: ` +
|
|
401
|
+
`[${canonical}].`);
|
|
365
402
|
}
|
|
366
403
|
/**
|
|
367
404
|
* Normalize a dashboard schema's filter declarations into a flat list of
|
|
@@ -372,7 +409,11 @@ function warnShorthandOptions(filterName, shorthand) {
|
|
|
372
409
|
export function resolveDashboardFilterDefs(schema) {
|
|
373
410
|
const byName = new Map();
|
|
374
411
|
if (schema.dateRange) {
|
|
375
|
-
|
|
412
|
+
// An omitted `defaultRange` takes the spec's declared default (#10339); an
|
|
413
|
+
// explicit value — `'custom'` included — is used exactly as authored.
|
|
414
|
+
const preset = schema.dateRange.defaultRange === undefined
|
|
415
|
+
? specDefaultDateRangePreset()
|
|
416
|
+
: schema.dateRange.defaultRange;
|
|
376
417
|
byName.set(DATE_RANGE_FILTER_NAME, {
|
|
377
418
|
name: DATE_RANGE_FILTER_NAME,
|
|
378
419
|
field: schema.dateRange.field || DATE_RANGE_DEFAULT_FIELD,
|
|
@@ -398,9 +439,14 @@ export function resolveDashboardFilterDefs(schema) {
|
|
|
398
439
|
byName.set(name, {
|
|
399
440
|
name,
|
|
400
441
|
field: f.field,
|
|
442
|
+
// Spread rather than assigned (`061f5e829`): every filter authored
|
|
443
|
+
// before this key existed keeps the exact key set it resolved to, so
|
|
444
|
+
// `'object' in def` stays the discriminator the render side branches on
|
|
445
|
+
// and no consumer sees a new own-property carrying `undefined`.
|
|
446
|
+
...(typeof f.object === 'string' && f.object ? { object: f.object } : {}),
|
|
401
447
|
label: f.label,
|
|
402
448
|
type,
|
|
403
|
-
// `name` is the identifying context the
|
|
449
|
+
// `name` is the identifying context the dropped-member warning needs, and
|
|
404
450
|
// the local above already resolved it — nothing new is threaded through a
|
|
405
451
|
// public signature for it. `normalizeFilterOptions` is module-private, so
|
|
406
452
|
// widening ITS parameter list is not a contract move.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { AnalyticsResult } from '@objectstack/spec/contracts';
|
|
19
19
|
import type { PercentScale } from '@objectstack/spec/data';
|
|
20
|
+
import { type BuiltinAggregateLabels } from './chart-series.js';
|
|
20
21
|
/**
|
|
21
22
|
* Column metadata the analytics server returns alongside the rows — the spec's
|
|
22
23
|
* `AnalyticsResult.fields[]` element BY REFERENCE, never a local restatement of
|
|
@@ -59,28 +60,35 @@ export type DatasetResultField = AnalyticsResult['fields'][number];
|
|
|
59
60
|
*/
|
|
60
61
|
export type { PercentScale } from '@objectstack/spec/data';
|
|
61
62
|
/**
|
|
62
|
-
* Scale a stored
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* `
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* `1
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
63
|
+
* Scale a stored percentage to its DISPLAY magnitude (percentage points), at
|
|
64
|
+
* the storage the caller STATES.
|
|
65
|
+
*
|
|
66
|
+
* `percentScale` is the spec's `PercentScale`: `fraction` is a 0–1 ratio
|
|
67
|
+
* (`0.75` ⇒ 75, `1` ⇒ 100), `whole` is already in percentage points (`75` ⇒
|
|
68
|
+
* 75, `1` ⇒ 1). A caller holding a field definition reads it with the spec's
|
|
69
|
+
* `percentScaleOf` (`@objectstack/spec/data`): a `percent` field stores a
|
|
70
|
+
* fraction unless it declares a `max` above 1. A caller with no field (a
|
|
71
|
+
* computed ratio, a server measure column) states the storage it knows: a
|
|
72
|
+
* `derived: { op: 'ratio' }` measure is a fraction by definition, and the
|
|
73
|
+
* server annotates a measure column with its `percentScale`.
|
|
74
|
+
*
|
|
75
|
+
* ⛔ No storage is inferred from the VALUE any more (objectui#11475). This
|
|
76
|
+
* function used to be `value > -1 && value < 1 ? value * 100 : value`, a guess
|
|
77
|
+
* from the magnitude that read neither the field nor its `max`. A fraction-
|
|
78
|
+
* stored `1` (100%) therefore read `1%` in the list cell while the read-only
|
|
79
|
+
* form, which reads the declaration, read `100%`; a whole-stored `0.5` read
|
|
80
|
+
* `50%`. objectui#3136 was the same guess on a dashboard measure. The
|
|
81
|
+
* parameter is REQUIRED, so every caller says which storage it holds, and a
|
|
82
|
+
* value outside the union throws rather than falling to a branch silently.
|
|
83
|
+
*
|
|
84
|
+
* ⭐ The contract a third percent surface still owes is BOTH halves: this
|
|
85
|
+
* scaling, at the stored scale, AND the locale's percent convention
|
|
86
|
+
* (`style: 'percentPoints'`, see {@link formatMeasure} and `formatPercent` in
|
|
87
|
+
* `@object-ui/fields`). Between objectui#4553 and objectui#4576 the two ends
|
|
88
|
+
* shared the scaling and not the convention, so a German session read
|
|
89
|
+
* `1.234,5 %` in a list cell and `1.234,5%` in a dashboard measure.
|
|
82
90
|
*/
|
|
83
|
-
export declare function percentDisplayValue(value: number): number;
|
|
91
|
+
export declare function percentDisplayValue(value: number, percentScale: PercentScale): number;
|
|
84
92
|
/**
|
|
85
93
|
* Format a MEASURE value. Currency comes from the field's declared `currency`
|
|
86
94
|
* (locale-correct symbol via `Intl`), NOT from a "$" baked into the format
|
|
@@ -118,8 +126,54 @@ export declare function percentDisplayValue(value: number): number;
|
|
|
118
126
|
* is pure, so it moved DOWN into this package (`./number-display.ts`) and
|
|
119
127
|
* `@object-ui/i18n` re-exports it. The duplicate implementation this file used
|
|
120
128
|
* to carry is gone, and with it the drift that duplicate produced — see the
|
|
121
|
-
* percent note in the body, and
|
|
122
|
-
*
|
|
129
|
+
* percent note in the body, and the both-halves contract
|
|
130
|
+
* {@link percentDisplayValue}'s doc comment states.
|
|
131
|
+
*
|
|
132
|
+
* ── Date-valued measures (objectui#7178) ──
|
|
133
|
+
* `min` / `max` over a date or datetime field is a legitimate measure, and it
|
|
134
|
+
* used to render its stored value verbatim: the guard above returned
|
|
135
|
+
* `String(v)` for anything non-numeric BEFORE `format` was read, so a KPI tile
|
|
136
|
+
* showed `2026-07-04T07:00:00.000Z` in `text-2xl` and wrapped it over two
|
|
137
|
+
* lines. A date-shaped value now routes through {@link formatMeasureDate} to
|
|
138
|
+
* the display path list cells use; nothing about the numeric path moved.
|
|
139
|
+
*
|
|
140
|
+
* ⚠️ What `format` can and cannot say for those values, measured rather than
|
|
141
|
+
* assumed, and stated PER ARM — a date-shaped measure takes one of two arms in
|
|
142
|
+
* {@link formatMeasureDate}, and this paragraph used to describe only one of
|
|
143
|
+
* them while reading as though it described both. That cost a card: an
|
|
144
|
+
* objectui triage pass read the undifferentiated claim, concluded the defect
|
|
145
|
+
* was already fixed, and downgraded objectui#8352, which a driven browser run
|
|
146
|
+
* then refuted. The two arms agree TODAY, but only because #8352 made them; a
|
|
147
|
+
* universal sentence here is what hid the disagreement before, so it is not
|
|
148
|
+
* coming back even now that it would be true.
|
|
149
|
+
*
|
|
150
|
+
* The shared date path takes a named STYLE, not a date pattern. On BOTH arms
|
|
151
|
+
* the honoured vocabulary is `'short'` and `'relative'`:
|
|
152
|
+
*
|
|
153
|
+
* - `Field.date` (`2026-09-08`) — honoured by threading `format` into
|
|
154
|
+
* `formatDate`'s style parameter. The same words `DateCellRenderer`
|
|
155
|
+
* honours from `field.format` for the same field.
|
|
156
|
+
* - `Field.datetime` (`2026-09-01T00:00:00.000Z`) — honoured by SELECTING a
|
|
157
|
+
* formatter, because `formatDateTime` has no style parameter to thread
|
|
158
|
+
* into (objectui#8352). Before that card this arm honoured NOTHING: it
|
|
159
|
+
* dropped `format` entirely, so `'relative'` rendered the absolute face.
|
|
160
|
+
*
|
|
161
|
+
* Every other string falls to that arm's default face — a PATTERN like
|
|
162
|
+
* `format: 'YYYY-MM-DD'` is accepted by the schema, reaches this function, and
|
|
163
|
+
* renders the locale default, and so does `'compact'`, which is
|
|
164
|
+
* `formatDateTime`'s own vocabulary but not this path's. The pattern part is
|
|
165
|
+
* not new behaviour introduced here: `plugin-dashboard`'s `recordFields`
|
|
166
|
+
* already routes a date-shaped `format` into the same style slot and gets the
|
|
167
|
+
* same locale default. Closing it would mean teaching the shared path a
|
|
168
|
+
* pattern grammar, which is a change to the path itself and to every list cell
|
|
169
|
+
* that reads it — not a measure concern, and not either card.
|
|
170
|
+
*
|
|
171
|
+
* ⚠️ `'relative'` renders the ABSOLUTE face for a value more than ±7 days from
|
|
172
|
+
* today, on both arms, by `formatRelativeDate`'s own design. That is correct
|
|
173
|
+
* output, not a regression — but it also means an out-of-window value is
|
|
174
|
+
* useless for testing this path, because it renders identically whether
|
|
175
|
+
* `format` is honoured or dropped. Both readings that missed #8352 were taken
|
|
176
|
+
* on 40–60-day-old data.
|
|
123
177
|
*/
|
|
124
178
|
export declare function formatMeasure(v: unknown, format?: string, currency?: string, percentScale?: PercentScale, locale?: string): string;
|
|
125
179
|
/**
|
|
@@ -140,13 +194,31 @@ export declare function formatDimensionValue(v: unknown, locale?: string): strin
|
|
|
140
194
|
*
|
|
141
195
|
* `fieldLabel` is injected (rather than imported) so this stays React/i18n-free;
|
|
142
196
|
* callers pass `useSafeFieldLabel().fieldLabel`.
|
|
197
|
+
*
|
|
198
|
+
* ## `builtinAggregateLabels` (objectui#7534, sibling of #7258)
|
|
199
|
+
*
|
|
200
|
+
* A result field the analytics service minted as a BUILT-IN default measure
|
|
201
|
+
* carries `builtinAggregate` (objectstack#14492) and a hard-coded English
|
|
202
|
+
* `label` ('Count'). {@link resolveMeasureLabel} is the ONE resolver for that —
|
|
203
|
+
* #7258 wired it into `buildChartSeries()`, so a chart legend already read
|
|
204
|
+
* `计数` on a zh console while the table underneath it, the KPI caption, the
|
|
205
|
+
* pivot header and the dataset preview all still said `Count`, because they
|
|
206
|
+
* resolve their titles through here instead.
|
|
207
|
+
*
|
|
208
|
+
* Passing the caller's resolved labels closes that gap without a second
|
|
209
|
+
* resolution order: the discriminator is consulted FIRST (exactly as on the
|
|
210
|
+
* chart seam), the existing `fieldLabel` convention still applies on top, and
|
|
211
|
+
* the raw name is still the floor. Omitting the argument reproduces the
|
|
212
|
+
* previous output byte for byte — an author-declared measure carries no
|
|
213
|
+
* discriminator and keeps its wire `label` verbatim (objectui#4106), and a
|
|
214
|
+
* provider-less host resolves no labels and so changes nothing.
|
|
143
215
|
*/
|
|
144
|
-
export declare function buildDatasetFieldHelpers(fields: DatasetResultField[] | undefined, object: string | undefined, fieldLabel?: (objectName: string, fieldName: string, fallback: string) => string): {
|
|
216
|
+
export declare function buildDatasetFieldHelpers(fields: DatasetResultField[] | undefined, object: string | undefined, fieldLabel?: (objectName: string, fieldName: string, fallback: string) => string, builtinAggregateLabels?: BuiltinAggregateLabels): {
|
|
145
217
|
measureField: (name: string) => DatasetResultField | undefined;
|
|
146
218
|
headerLabel: (name: string) => string;
|
|
147
219
|
};
|
|
148
220
|
/**
|
|
149
|
-
* A half-open date-range drill scope for one time-bucketed dimension (#1752):
|
|
221
|
+
* A half-open date-range drill scope for one time-bucketed dimension (objectstack-ai/objectstack#1752):
|
|
150
222
|
* the object FIELD to filter and its inclusive `gte` / exclusive `lt` bounds
|
|
151
223
|
* (the server's `drillRanges` sidecar entry).
|
|
152
224
|
*/
|
|
@@ -161,18 +233,127 @@ export interface DatasetDrillRange {
|
|
|
161
233
|
* Each drillable dimension maps to its underlying object field, filtered by the
|
|
162
234
|
* dimension's RAW grouped value (from the server's parallel `drillRawRows`, NOT
|
|
163
235
|
* the visible row which carries the display LABEL — a select/lookup label would
|
|
164
|
-
* mis-filter).
|
|
165
|
-
*
|
|
166
|
-
*
|
|
236
|
+
* mis-filter). The render-time `runtimeFilter` is ANDed in so the drilled list
|
|
237
|
+
* stays within the same slice the aggregate was computed over.
|
|
238
|
+
*
|
|
239
|
+
* ## What the EMPTY bucket means, and why it is spelled `{ $null: true }`
|
|
167
240
|
*
|
|
168
|
-
* A
|
|
241
|
+
* A drill must return exactly the rows the clicked bucket COUNTED. The empty
|
|
242
|
+
* bucket is the one the aggregate grouped rows with NO VALUE for that dimension
|
|
243
|
+
* into, so the drill means "this dimension has no value" — NOT "this dimension
|
|
244
|
+
* holds the literal value null". Those are different row sets, and the
|
|
245
|
+
* difference is measurable rather than theoretical: against `ValueDataSource`'s
|
|
246
|
+
* matcher over rows `{owner:'alice'}` / `{owner:null}` / `{}` (no `owner` key),
|
|
247
|
+
* `['owner','is_null',true]` selects the explicit-null row AND the missing-key
|
|
248
|
+
* row, while an equality test against `null` selects only the explicit-null one
|
|
249
|
+
* and drops the row that has no `owner` at all. The bucket counted both, so
|
|
250
|
+
* is-null is the one that agrees with the number the user clicked.
|
|
251
|
+
*
|
|
252
|
+
* ⇒ every empty authoring — `''`, `null` and `undefined` alike — becomes ONE
|
|
253
|
+
* spelling, `{ [field]: { $null: true } }`, which `convertFiltersToAST` lowers
|
|
254
|
+
* to `[field, 'is_null', true]`. The three are not told apart because the
|
|
255
|
+
* server already merged them into the single bucket the user clicked; there is
|
|
256
|
+
* no second bucket here for a second spelling to address.
|
|
257
|
+
*
|
|
258
|
+
* ⚠️ This value used to be a bare `null`, and that is the defect objectui#9085
|
|
259
|
+
* records: `convertFiltersToAST`'s OLDEST pinned behaviour is to SKIP a key
|
|
260
|
+
* whose value is `null` / `undefined` (pinned as "should skip null and
|
|
261
|
+
* undefined values"), so the constraint was dropped on the way to the wire and
|
|
262
|
+
* the empty-bucket drill answered with a SUPERSET — every row, silently. The
|
|
263
|
+
* repair is here at the PRODUCER and ⛔ not at that converter: the skip is
|
|
264
|
+
* relied on by other producers, and changing it would move every caller's
|
|
265
|
+
* meaning at once (objectui#9020 ruled it stays). `$null` is not a new
|
|
266
|
+
* vocabulary either — it is a declared spec operator this dialect already
|
|
267
|
+
* lowers, and the one spelling that says the same thing on BOTH of
|
|
268
|
+
* `ValueDataSource`'s routes, where a bare `null` said "only an explicit null"
|
|
269
|
+
* on the plain-object route and "no constraint at all" on the AST route.
|
|
270
|
+
*
|
|
271
|
+
* ⚠️ Known BOUNDARY, unchanged by this and deliberately not widened: the drill
|
|
272
|
+
* "escape hatch" (the host's `openRecordList`, which serializes a drill filter
|
|
273
|
+
* into `filter[...]` URL params) has NO spelling for is-null — its operator
|
|
274
|
+
* vocabulary is equality plus four range bounds — so it drops this condition
|
|
275
|
+
* exactly as it already dropped the bare `null`, byte-for-byte the same query
|
|
276
|
+
* string. That surface is a superset today and stays one; closing it needs a
|
|
277
|
+
* URL-dialect operator on both the write and the read side, which is its own
|
|
278
|
+
* card.
|
|
279
|
+
*
|
|
280
|
+
* A time-bucketed date dimension (objectstack-ai/objectstack#1752) drills by RANGE, not equality — a
|
|
169
281
|
* humanized bucket ("2026-Q2") can't be exact-matched, so the server sends a
|
|
170
282
|
* half-open `[gte, lt)` per date dim in `rawRanges` instead of a raw value.
|
|
171
283
|
* Each becomes an ObjectQL range operator object (`{ $gte, $lt }`) so the drill
|
|
172
284
|
* scopes the list to the clicked time bucket instead of every bucket (which the
|
|
173
285
|
* old date-dim skip degraded to — a superset).
|
|
174
286
|
*
|
|
287
|
+
* ## The widget filter is CONJOINED, not spread (objectui#9137)
|
|
288
|
+
*
|
|
289
|
+
* ⛔ `runtimeFilter` is composed through {@link composeDrillFilter}, never by
|
|
290
|
+
* spreading it into the object literal below. Spreading is correct only for the
|
|
291
|
+
* OBJECT arm; this parameter's declared producer sends the ARRAY arm. The
|
|
292
|
+
* dashboard's `DashboardWidgetSchema.filter` docblock states it outright —
|
|
293
|
+
* "objectui passes an ObjectQL FilterNode array here, not the spec's
|
|
294
|
+
* `FilterCondition` envelope" — and `DatasetWidget`'s guard admits it
|
|
295
|
+
* (`typeof [] === 'object'` and `Object.keys(['x']).length === 1` are both
|
|
296
|
+
* true), so the array arm reached this line unfiltered. Spreading an ARRAY
|
|
297
|
+
* yields INDEX keys: `[['region','=','emea']]` became
|
|
298
|
+
* `{ '0': ['region','=','emea'] }`, which is not a condition anyone can honour.
|
|
299
|
+
*
|
|
300
|
+
* ⚠️ MEASURED, and it is NOT the silent superset the sibling sites had. An
|
|
301
|
+
* index key is not dropped on the way to the wire — every sink in circulation
|
|
302
|
+
* refuses it LOUDLY, because a bare array is not a legal equality comparand
|
|
303
|
+
* (objectui#8530 / objectui#8514): `convertFiltersToAST` THROWS
|
|
304
|
+
* `FilterOperatorError`, `driver-sql` answers `400 INVALID_FILTER`, and
|
|
305
|
+
* `ValueDataSource`'s matcher selects NOTHING. Against rows scoped
|
|
306
|
+
* `region = 'emea'`, drilling the `stage = 'won'` bucket returned `(none)`
|
|
307
|
+
* where the widget alone returned three rows and the conjunction returns the
|
|
308
|
+
* right two. So the widget's conditions were not merely lost — the drill was
|
|
309
|
+
* dead in the array arm. The failure is fail-CLOSED here, and this is the third
|
|
310
|
+
* site of the class objectui#8944 (chart) and objectui#9024 (pivot) removed.
|
|
311
|
+
*
|
|
312
|
+
* Three properties of THIS site, each confirmed rather than assumed — it is not
|
|
313
|
+
* a copy-paste of the other two:
|
|
314
|
+
*
|
|
315
|
+
* 1. The return stays a NON-optional `Record<string, unknown>`, because callers
|
|
316
|
+
* do not expect `undefined`. `composeDrillFilter` answers `undefined` when
|
|
317
|
+
* both sources are empty, so that one case is spelled `?? {}` — the same
|
|
318
|
+
* empty object this function already returned for a bucket with no dims.
|
|
319
|
+
* 2. Range operators SURVIVE. A time-bucketed dim contributes
|
|
320
|
+
* `{ $gte, $lt }`, and the composition re-spells it as two `$and` children
|
|
321
|
+
* on the same field (`[{d:{$gte}},{d:{$lt}}]`) rather than losing a bound;
|
|
322
|
+
* the selected rows are identical, and `serializeDrillFilterParams` walks
|
|
323
|
+
* `$and` (objectui#8944) so both bounds still reach `filter[d][gte|lt]`.
|
|
324
|
+
* 3. `runtimeFilter` is declared `unknown`, which is what the two consumers
|
|
325
|
+
* actually hand it — `any` from the dashboard, `Record<string, unknown>`
|
|
326
|
+
* from `DatasetReportRenderer`. Widening the PARAMETER is what admits both
|
|
327
|
+
* without a cast; ⛔ neither consumer's own type is changed by this.
|
|
328
|
+
*
|
|
329
|
+
* ⚠️ Conjunction changes one behaviour deliberately: a field named by BOTH the
|
|
330
|
+
* widget filter and the clicked bucket used to be overwritten by the bucket,
|
|
331
|
+
* and now both conditions apply. That is the point — a drill may narrow the
|
|
332
|
+
* widget's scope and may never widen it — and it is the posture the chart and
|
|
333
|
+
* pivot sites already ship.
|
|
334
|
+
*
|
|
335
|
+
* ## ⭐ Only the COMPOSING leg moves; the identity leg is left byte-identical
|
|
336
|
+
*
|
|
337
|
+
* The sink is asked only when there IS a second source. That is this site's
|
|
338
|
+
* one departure from `ObjectChart` / `ObjectPivotTable`, which call it
|
|
339
|
+
* unconditionally, and it is deliberate rather than a shortcut: the spread
|
|
340
|
+
* this replaces only ever ran when `runtimeFilter` was present, so the
|
|
341
|
+
* no-widget-filter path carries no defect to repair.
|
|
342
|
+
*
|
|
343
|
+
* ⚠️ Routing it anyway was MEASURED and rejected. `composeDrillFilter`'s own
|
|
344
|
+
* note — "a lone surviving source lowers back to exactly the flat object the
|
|
345
|
+
* spread produced" — holds only for a source of ONE condition; with two it
|
|
346
|
+
* lowers to `$and`, because `convertFiltersToAST` emits one node per condition
|
|
347
|
+
* and `parseFilterAST` cannot fold two nodes back onto one key. A `{ $gte,
|
|
348
|
+
* $lt }` range is two conditions on its own, so even a single date bucket
|
|
349
|
+
* re-spells. Sending the identity leg through the sink therefore re-shaped
|
|
350
|
+
* every multi-condition dataset drill — including the controls objectui#9085,
|
|
351
|
+
* objectui#4056, objectstack#5473 and objectstack-ai/objectstack#1752 pin on paths this card does not
|
|
352
|
+
* touch — for ZERO change in the rows selected or the `filter[...]` params
|
|
353
|
+
* emitted. ⛔ Do not "simplify" this to an unconditional call without redoing
|
|
354
|
+
* that measurement.
|
|
355
|
+
*
|
|
175
356
|
* Shared by the dashboard `DatasetWidget` and the report renderer so a drill
|
|
176
357
|
* filters identically (and correctly, including lookups) on both surfaces.
|
|
177
358
|
*/
|
|
178
|
-
export declare function buildDatasetDrillFilter(rawRow: Record<string, unknown> | undefined, drillDims: string[], dimensionFields: Record<string, string>, runtimeFilter?:
|
|
359
|
+
export declare function buildDatasetDrillFilter(rawRow: Record<string, unknown> | undefined, drillDims: string[], dimensionFields: Record<string, string>, runtimeFilter?: unknown, rawRanges?: Record<string, DatasetDrillRange>): Record<string, unknown>;
|