@object-ui/core 17.3.0 → 17.5.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 +1224 -0
- package/README.md +2 -12
- package/dist/actions/ActionEngine.d.ts +42 -25
- package/dist/actions/ActionEngine.js +66 -52
- package/dist/actions/ActionRunner.d.ts +199 -21
- package/dist/actions/ActionRunner.js +332 -16
- package/dist/actions/actionKeys.d.ts +96 -20
- package/dist/actions/actionKeys.js +157 -18
- package/dist/data-scope/ViewDataProvider.d.ts +37 -12
- package/dist/data-scope/ViewDataProvider.js +83 -5
- package/dist/data-scope/element-data-source.d.ts +113 -0
- package/dist/data-scope/element-data-source.js +216 -0
- package/dist/data-scope/index.d.ts +2 -1
- package/dist/data-scope/index.js +1 -0
- package/dist/evaluator/declaredPredicate.d.ts +119 -0
- package/dist/evaluator/declaredPredicate.js +152 -0
- package/dist/evaluator/fieldRules.d.ts +16 -0
- package/dist/evaluator/fieldRules.js +51 -16
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +5 -2
- package/dist/evaluator/listConditional.js +58 -16
- package/dist/evaluator/predicateInput.d.ts +51 -3
- package/dist/evaluator/predicateInput.js +62 -5
- package/dist/index.d.ts +4 -1
- package/dist/index.js +14 -1
- package/dist/protocols/DndProtocol.d.ts +42 -1
- package/dist/protocols/KeyboardProtocol.d.ts +31 -1
- package/dist/protocols/SharingProtocol.d.ts +11 -1
- package/dist/registry/Registry.d.ts +24 -0
- package/dist/theme/ThemeEngine.d.ts +0 -8
- package/dist/theme/ThemeEngine.js +22 -103
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +7 -1
- package/dist/types/index.d.ts +42 -8
- package/dist/utils/chart-series.d.ts +258 -5
- package/dist/utils/chart-series.js +423 -20
- package/dist/utils/compare-to.d.ts +40 -13
- package/dist/utils/compare-to.js +4 -32
- package/dist/utils/dashboard-filters.d.ts +54 -8
- package/dist/utils/dashboard-filters.js +261 -12
- package/dist/utils/dataset-format.d.ts +73 -14
- package/dist/utils/dataset-format.js +130 -9
- package/dist/utils/dataset-pivot.d.ts +28 -7
- package/dist/utils/dataset-pivot.js +27 -6
- package/dist/utils/dom-props.d.ts +137 -0
- package/dist/utils/dom-props.js +153 -0
- package/dist/utils/normalize-list-view.d.ts +26 -6
- package/dist/utils/normalize-list-view.js +30 -8
- package/dist/utils/number-display.d.ts +151 -0
- package/dist/utils/number-display.js +71 -0
- package/dist/utils/resolve-view-id.d.ts +31 -0
- package/dist/utils/resolve-view-id.js +45 -0
- package/dist/utils/sort-query.d.ts +55 -0
- package/dist/utils/sort-query.js +35 -0
- package/package.json +5 -5
- package/dist/utils/merge-views-into-objects.d.ts +0 -19
- package/dist/utils/merge-views-into-objects.js +0 -39
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* Everything in this module is pure and synchronous so the binding rules
|
|
19
19
|
* are unit-testable in isolation from React and the data layer.
|
|
20
20
|
*/
|
|
21
|
-
import type { DashboardComponentSchema, DashboardWidgetSchema, PageVariable } from '@object-ui/types';
|
|
21
|
+
import type { DashboardComponentSchema, DashboardWidgetSchema, I18nLabel, PageVariable } from '@object-ui/types';
|
|
22
|
+
import { DATE_RANGE_PRESETS } from '@objectstack/spec/ui';
|
|
22
23
|
/** Reserved filter name for the dashboard's built-in date range. */
|
|
23
24
|
export declare const DATE_RANGE_FILTER_NAME = "dateRange";
|
|
24
25
|
export interface DashboardFilterDef {
|
|
@@ -26,17 +27,41 @@ export interface DashboardFilterDef {
|
|
|
26
27
|
name: string;
|
|
27
28
|
/** Default target field when a widget declares no explicit binding. */
|
|
28
29
|
field: string;
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Display label, in @objectstack/spec's `I18nLabel` vocabulary — a plain
|
|
32
|
+
* string, or an inline per-locale map (`{ en: 'Owner', 'zh-CN': '负责人' }`).
|
|
33
|
+
*
|
|
34
|
+
* Widened from `string` under the objectstack#5428 ruling of 2026-08-06
|
|
35
|
+
* (option A): `GlobalFilterSchema.label` has been `I18nLabel` on the spec
|
|
36
|
+
* side since 17.0.0-rc.6, so declaring it `string` here made this module
|
|
37
|
+
* NARROWER than the contract it implements — which is exactly why the filter
|
|
38
|
+
* bar's map reads were invisible to `tsc` (objectui#4163).
|
|
39
|
+
*
|
|
40
|
+
* This module is locale-free BY DESIGN (`@object-ui/core` is logic-only), so
|
|
41
|
+
* it carries the authored vocabulary through unresolved and the RENDER side
|
|
42
|
+
* collapses it to the active language. Resolving here would need a locale
|
|
43
|
+
* this layer has no business knowing.
|
|
44
|
+
*/
|
|
45
|
+
label?: string | I18nLabel;
|
|
30
46
|
type: 'text' | 'select' | 'date' | 'number' | 'lookup' | 'dateRange';
|
|
31
47
|
/**
|
|
32
48
|
* Static options, NORMALIZED to `{ value, label }` pairs by
|
|
33
|
-
* `resolveDashboardFilterDefs` —
|
|
34
|
-
*
|
|
35
|
-
*
|
|
49
|
+
* `resolveDashboardFilterDefs` — consumers always see the object form.
|
|
50
|
+
*
|
|
51
|
+
* The canonical authoring form is @objectstack/spec's `{ value, label }`
|
|
52
|
+
* pair, and it is the ONLY one the platform accepts at publish. A bare-string
|
|
53
|
+
* shorthand in a STORED document is still lifted here, with a deprecation
|
|
54
|
+
* warning, on the objectstack#7917 retirement schedule — see
|
|
55
|
+
* `normalizeFilterOptions`. Do not author a new one.
|
|
56
|
+
*
|
|
57
|
+
* The PAIR SHAPE is normalized; the label's own vocabulary is not. `label`
|
|
58
|
+
* is `I18nLabel` in `GlobalFilterSchema.options[]` too, and it reaches the
|
|
59
|
+
* renderer as authored — see `normalizeFilterOptions` for why collapsing it
|
|
60
|
+
* here was data loss rather than normalization.
|
|
36
61
|
*/
|
|
37
62
|
options?: Array<{
|
|
38
63
|
value: string;
|
|
39
|
-
label: string;
|
|
64
|
+
label: string | I18nLabel;
|
|
40
65
|
}>;
|
|
41
66
|
optionsFrom?: {
|
|
42
67
|
object: string;
|
|
@@ -58,8 +83,29 @@ export interface DateRangeValue {
|
|
|
58
83
|
from?: string;
|
|
59
84
|
to?: string;
|
|
60
85
|
}
|
|
61
|
-
/**
|
|
62
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Preset keys the filter bar offers, in display order — the spec's list,
|
|
88
|
+
* RE-EXPORTED since objectui#4167 rather than derived from the local bounds
|
|
89
|
+
* table (objectstack#4115).
|
|
90
|
+
*
|
|
91
|
+
* `@objectstack/spec` 17.0.0-rc.6 publishes `DATE_RANGE_PRESETS`, and its own
|
|
92
|
+
* doc comment names this module as one of the three copies the extraction
|
|
93
|
+
* (objectstack#4614) existed to collapse: "it used to exist three times —
|
|
94
|
+
* inline in `dateRange.defaultRange`, as `PRESET_RANGES` in objectui's
|
|
95
|
+
* `dashboard-filters`, and as a hand-written table in
|
|
96
|
+
* `content/docs/ui/dashboards.mdx`". So the burn-down direction here is not a
|
|
97
|
+
* judgement call, it is upstream's stated intent, and the two lists were
|
|
98
|
+
* already identical in content AND display order — the copy had nothing to
|
|
99
|
+
* protect.
|
|
100
|
+
*
|
|
101
|
+
* What stays local is the BOUNDS table above, which is the other half of
|
|
102
|
+
* #4614's design ("the two vocabularies live one import apart and neither
|
|
103
|
+
* restates the other's grammar"): the spec owns which presets exist, this
|
|
104
|
+
* module owns what each one resolves to in date-macro tokens.
|
|
105
|
+
*/
|
|
106
|
+
export { DATE_RANGE_PRESETS };
|
|
107
|
+
/** Reset the shorthand-options warn-once memo. Exported for tests. */
|
|
108
|
+
export declare function resetDashboardFilterWarnings(): void;
|
|
63
109
|
/**
|
|
64
110
|
* Normalize a dashboard schema's filter declarations into a flat list of
|
|
65
111
|
* filter definitions. The built-in `dateRange` (when declared) comes first
|
|
@@ -5,6 +5,8 @@
|
|
|
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
|
+
import { liftLegacyGlobalFilterDefault } from '@object-ui/types';
|
|
9
|
+
import { DATE_RANGE_PRESETS } from '@objectstack/spec/ui';
|
|
8
10
|
import { resolveDateMacros } from './date-macros.js';
|
|
9
11
|
/** Reserved filter name for the dashboard's built-in date range. */
|
|
10
12
|
export const DATE_RANGE_FILTER_NAME = 'dateRange';
|
|
@@ -14,6 +16,18 @@ const DATE_RANGE_DEFAULT_FIELD = 'created_at';
|
|
|
14
16
|
* Date-range presets → date-macro token bounds. Tokens stay symbolic in the
|
|
15
17
|
* generated condition; every widget renderer resolves them at query time via
|
|
16
18
|
* `resolveDateMacros`, exactly like hand-authored widget filters.
|
|
19
|
+
*
|
|
20
|
+
* `satisfies Record<DateRangePreset, …>` is the load-bearing half of
|
|
21
|
+
* objectui#4167, not decoration. `DATE_RANGE_PRESETS` below is now the spec's
|
|
22
|
+
* list rather than `Object.keys` of this table, so the two could otherwise
|
|
23
|
+
* drift in the one direction the spec's own comment on that const names as the
|
|
24
|
+
* failure mode: a preset the SCHEMA knows and this table has no bounds for
|
|
25
|
+
* "validates clean and then resolves to nothing" — it would reach the filter
|
|
26
|
+
* bar's dropdown, be selected, and produce no range. The `satisfies` makes that
|
|
27
|
+
* a compile error at the moment the spec adds a preset (missing key), and makes
|
|
28
|
+
* a local invention a compile error too (excess key). The annotation form
|
|
29
|
+
* `const PRESET_RANGES: Record<DateRangePreset, …>` would NOT do this: a string
|
|
30
|
+
* index signature satisfies every literal key, so the check passes vacuously.
|
|
17
31
|
*/
|
|
18
32
|
const PRESET_RANGES = {
|
|
19
33
|
today: { from: '{today}', to: '{today}' },
|
|
@@ -30,8 +44,39 @@ const PRESET_RANGES = {
|
|
|
30
44
|
last_30_days: { from: '{30_days_ago}', to: '{today}' },
|
|
31
45
|
last_90_days: { from: '{90_days_ago}', to: '{today}' },
|
|
32
46
|
};
|
|
33
|
-
/**
|
|
34
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Bounds for a preset NAME that may not be one.
|
|
49
|
+
*
|
|
50
|
+
* The runtime receives whatever a stored dashboard carries, so an unrecognised
|
|
51
|
+
* name must warn (see `buildFilterCondition`) rather than fail to compile. That
|
|
52
|
+
* read is confined here so the `satisfies` pin above stays the single place
|
|
53
|
+
* deciding which names exist — an unguarded `PRESET_RANGES[someString]` would
|
|
54
|
+
* have forced the table back to a `Record<string, …>` and taken the pin with it.
|
|
55
|
+
*/
|
|
56
|
+
function presetBounds(preset) {
|
|
57
|
+
return PRESET_RANGES[preset];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Preset keys the filter bar offers, in display order — the spec's list,
|
|
61
|
+
* RE-EXPORTED since objectui#4167 rather than derived from the local bounds
|
|
62
|
+
* table (objectstack#4115).
|
|
63
|
+
*
|
|
64
|
+
* `@objectstack/spec` 17.0.0-rc.6 publishes `DATE_RANGE_PRESETS`, and its own
|
|
65
|
+
* doc comment names this module as one of the three copies the extraction
|
|
66
|
+
* (objectstack#4614) existed to collapse: "it used to exist three times —
|
|
67
|
+
* inline in `dateRange.defaultRange`, as `PRESET_RANGES` in objectui's
|
|
68
|
+
* `dashboard-filters`, and as a hand-written table in
|
|
69
|
+
* `content/docs/ui/dashboards.mdx`". So the burn-down direction here is not a
|
|
70
|
+
* judgement call, it is upstream's stated intent, and the two lists were
|
|
71
|
+
* already identical in content AND display order — the copy had nothing to
|
|
72
|
+
* protect.
|
|
73
|
+
*
|
|
74
|
+
* 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
|
|
76
|
+
* restates the other's grammar"): the spec owns which presets exist, this
|
|
77
|
+
* module owns what each one resolves to in date-macro tokens.
|
|
78
|
+
*/
|
|
79
|
+
export { DATE_RANGE_PRESETS };
|
|
35
80
|
/**
|
|
36
81
|
* ISO calendar date, optionally carrying a time part — `2026-01-15`,
|
|
37
82
|
* `2026-01-15T08:30:00Z`. Deliberately narrower than `Date.parse`, which
|
|
@@ -65,10 +110,117 @@ function warnDateFilter(message) {
|
|
|
65
110
|
if (typeof console !== 'undefined')
|
|
66
111
|
console.warn(`[dashboard-filters] ${message}`);
|
|
67
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Dev-mode gate, matching `actions/actionKeys.ts` — a deprecation warning that
|
|
115
|
+
* floods a production console is a warning that gets muted.
|
|
116
|
+
*/
|
|
117
|
+
const isDev = () => globalThis.process?.env?.NODE_ENV !==
|
|
118
|
+
'production';
|
|
119
|
+
/**
|
|
120
|
+
* Warn-once memo for the bare-string `options` shorthand (objectui#4356).
|
|
121
|
+
*
|
|
122
|
+
* Keyed by filter NAME **and** the offending values, deliberately — the same
|
|
123
|
+
* reasoning `warnOnUnknownActionKeys` records for its own memo. Keying on the
|
|
124
|
+
* name alone would report the first dashboard carrying a shorthand `status`
|
|
125
|
+
* filter and stay silent about every other one, sending the author to fix one
|
|
126
|
+
* symptom; keying on the values alone would collapse two genuinely different
|
|
127
|
+
* filters that happen to share an option list. The memo is bounded by the
|
|
128
|
+
* number of authored filters either way.
|
|
129
|
+
*
|
|
130
|
+
* This lives at module scope because `resolveDashboardFilterDefs` runs on every
|
|
131
|
+
* dashboard render — per-call state would warn once per frame, which is the
|
|
132
|
+
* flood the dedupe exists to prevent.
|
|
133
|
+
*/
|
|
134
|
+
const warnedShorthandOptions = new Set();
|
|
135
|
+
/** Reset the shorthand-options warn-once memo. Exported for tests. */
|
|
136
|
+
export function resetDashboardFilterWarnings() {
|
|
137
|
+
warnedShorthandOptions.clear();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Apply the ADR-0089 legacy-alias lift to ONE stored `globalFilters` entry, and
|
|
141
|
+
* say so out loud when it fires (objectui#4165).
|
|
142
|
+
*
|
|
143
|
+
* ## The alias, at this read site
|
|
144
|
+
*
|
|
145
|
+
* **What it is.** `defaultValue: { preset: 'last_7_days' }` on a `type: 'date'`
|
|
146
|
+
* filter. The canonical spelling is the bare preset NAME,
|
|
147
|
+
* `defaultValue: 'last_7_days'` — one of the three the spec's rc.6 refinement
|
|
148
|
+
* accepts (preset name / ISO date / date-macro token).
|
|
149
|
+
*
|
|
150
|
+
* **Why it is lifted here rather than tolerated.** Maintainer ruling on
|
|
151
|
+
* objectui#4165 (2026-08-11): 「spec stays strict — no widening」. objectui's
|
|
152
|
+
* `GlobalFilterSchema` now carries that refinement, so a document holding the
|
|
153
|
+
* object form fails validation; lifting it BEFORE anything reads the entry
|
|
154
|
+
* makes the declaration canonical by construction. The lift itself lives in
|
|
155
|
+
* `@object-ui/types`' `dashboard-filter-alias.ts` (one implementation, shared
|
|
156
|
+
* with the designer's rewrite-on-save path); this is one of its two call sites.
|
|
157
|
+
*
|
|
158
|
+
* **What it does NOT buy, measured.** It does not rescue rendering. A legacy
|
|
159
|
+
* declaration already resolved correctly before #4165 and still does with this
|
|
160
|
+
* call deleted — reverse-verified, only the warning below changes. The reason
|
|
161
|
+
* is a coincidence worth knowing: `{ preset }` is also the runtime VALUE shape
|
|
162
|
+
* `normalizeDateDefault` produces for the canonical name, and that function
|
|
163
|
+
* passes non-strings through untouched. That coincidence is why the object form
|
|
164
|
+
* looked harmless for so long, and how the schema's own prose drifted into
|
|
165
|
+
* calling it "the on-disk form". Read this call as canonicalization plus
|
|
166
|
+
* observability, not as a repair — claiming more would be claiming coverage the
|
|
167
|
+
* tests in `__tests__/dashboard-filters.test.ts` do not have.
|
|
168
|
+
*
|
|
169
|
+
* **When it may be removed.** At the next MAJOR of `@object-ui/types` (18.0.0).
|
|
170
|
+
* By then every dashboard opened in the designer has been rewritten to the bare
|
|
171
|
+
* name, because `DashboardDesignPage` lifts into the editable draft and the
|
|
172
|
+
* next save persists it. Delete this function and its call below together with
|
|
173
|
+
* the lift itself; a document still carrying the object form then gets the
|
|
174
|
+
* spec's named rejection, which is the intended end state.
|
|
175
|
+
*
|
|
176
|
+
* The warning is not decoration: a silent lift can never be retired, because
|
|
177
|
+
* nothing would ever show that the last legacy document is gone (ADR-0078 —
|
|
178
|
+
* nothing silently inert).
|
|
179
|
+
*/
|
|
180
|
+
function liftLegacyFilterDeclaration(filter) {
|
|
181
|
+
const lifted = liftLegacyGlobalFilterDefault(filter);
|
|
182
|
+
if (lifted === filter)
|
|
183
|
+
return filter;
|
|
184
|
+
const name = filter?.name
|
|
185
|
+
?? filter?.field
|
|
186
|
+
?? '?';
|
|
187
|
+
const preset = lifted?.defaultValue;
|
|
188
|
+
warnDateFilter(`filter "${name}": \`defaultValue: { preset: ${JSON.stringify(preset)} }\` is a LEGACY ` +
|
|
189
|
+
`spelling (objectui#4165) and was lifted to the canonical bare preset name ` +
|
|
190
|
+
`${JSON.stringify(preset)}. Rewrite the stored dashboard — the object form is ` +
|
|
191
|
+
`refused by @objectstack/spec and its acceptance here ends with @object-ui/types 18.`);
|
|
192
|
+
return lifted;
|
|
193
|
+
}
|
|
68
194
|
/**
|
|
69
195
|
* Normalize a date filter's DECLARED default into the `DateRangeValue` shape
|
|
70
196
|
* every date consumer in this module reads (framework#4475).
|
|
71
197
|
*
|
|
198
|
+
* ## Declaration space vs value space — the distinction objectui#4165 turned on
|
|
199
|
+
*
|
|
200
|
+
* These are two different things that share the name `defaultValue`, and
|
|
201
|
+
* conflating them is what produced #4165:
|
|
202
|
+
*
|
|
203
|
+
* - the **declaration** is `globalFilters[].defaultValue` in a stored
|
|
204
|
+
* dashboard. `@objectstack/spec` owns it, it is `string | number | boolean`,
|
|
205
|
+
* and since rc.6 a refinement holds a `type: 'date'` one to a preset NAME,
|
|
206
|
+
* an ISO date or a date-macro token. A bare preset name is the canonical
|
|
207
|
+
* spelling and the object form is a retiring alias (see
|
|
208
|
+
* `liftLegacyFilterDeclaration` above);
|
|
209
|
+
* - the **value** is what this function RETURNS: `DashboardFilterDef
|
|
210
|
+
* .defaultValue`, which seeds the filter variable and is read by
|
|
211
|
+
* `DateRangeFilter` (`.preset`/`.from`/`.to`) and `buildFilterCondition`.
|
|
212
|
+
* That shape is `DateRangeValue`, it is objectui-internal, the spec has no
|
|
213
|
+
* opinion about it, and for a preset it is `{ preset }`.
|
|
214
|
+
*
|
|
215
|
+
* So this function converts declaration → value. It is NOT a producer of stored
|
|
216
|
+
* metadata: nothing writes a resolved `DashboardFilterDef` back into a
|
|
217
|
+
* dashboard document (measured in #4165 — `resolveDashboardFilterDefs`' only
|
|
218
|
+
* callers are `DashboardRenderer` and `DashboardWidgetInspector`, both read
|
|
219
|
+
* side). Making it emit the bare name instead would therefore not change one
|
|
220
|
+
* byte on disk; it would only hand `DateRangeFilter` a string it cannot read
|
|
221
|
+
* and `buildFilterCondition` a value it warns-and-skips — i.e. re-open
|
|
222
|
+
* framework#4475 exactly, which is why it keeps emitting `{ preset }`.
|
|
223
|
+
*
|
|
72
224
|
* The built-in `dateRange` declaration has always been normalized this way —
|
|
73
225
|
* `schema.dateRange.defaultRange` is a preset NAME and
|
|
74
226
|
* `resolveDashboardFilterDefs` lifts it to `{ preset }`. A `globalFilters`
|
|
@@ -101,15 +253,67 @@ function normalizeDateDefault(type, defaultValue) {
|
|
|
101
253
|
/**
|
|
102
254
|
* Normalize a filter's static `options` declaration to `{ value, label }`
|
|
103
255
|
* pairs. The @objectstack/spec `GlobalFilterSchema.options` form is
|
|
104
|
-
* `{ value, label }` objects
|
|
105
|
-
*
|
|
106
|
-
* un-normalized
|
|
107
|
-
*
|
|
256
|
+
* `{ value, label }` objects; the bare-string shorthand (`options: ['EMEA', …]`)
|
|
257
|
+
* is still lifted, but is DEPRECATED and now says so out loud. Rendering an
|
|
258
|
+
* un-normalized option crashes React — this is the single place both shapes
|
|
259
|
+
* converge.
|
|
260
|
+
*
|
|
261
|
+
* ## The shorthand's deprecation (objectui#4356, objectstack#7917)
|
|
262
|
+
*
|
|
263
|
+
* Maintainer ruling of 2026-08-12 on objectstack#7917, verbatim 「7917 ②」:
|
|
264
|
+
* option ② — **the spec stays strict; the runtime bare-string lift retires
|
|
265
|
+
* behind a deprecation window sized by a stored-dashboard survey.** So a
|
|
266
|
+
* document spelling `options: ['EMEA']` renders here and is refused the moment
|
|
267
|
+
* it reaches the platform's validation — the "one strict contract beats N
|
|
268
|
+
* dialects" divergence AGENTS.md #0.1 names, with the renderer's tolerance
|
|
269
|
+
* acting as a second de-facto contract.
|
|
270
|
+
*
|
|
271
|
+
* This is the WARN half of that window (Phase 1). The lift itself is unchanged
|
|
272
|
+
* and remains mechanically lossless (`'EMEA'` → `{ value: 'EMEA', label:
|
|
273
|
+
* 'EMEA' }`), because stored dashboards carry the shorthand and dropping it
|
|
274
|
+
* silently would turn a rendering filter into an empty one. Removal (Phase 2)
|
|
275
|
+
* is scheduled on objectstack#7917, earliest one minor release after this ships
|
|
276
|
+
* and not before the live-tenant channel has actually been queried.
|
|
277
|
+
*
|
|
278
|
+
* The warning is not decoration: a silent lift can never be retired, because
|
|
279
|
+
* nothing would ever show that the last shorthand document is gone (ADR-0078 —
|
|
280
|
+
* nothing silently inert). It is the same reasoning `liftLegacyFilterDeclaration`
|
|
281
|
+
* records above, for the sibling alias.
|
|
282
|
+
*
|
|
283
|
+
* Phase 0 shipped in the same PR: objectui's own docs, its `plugin-dashboard`
|
|
284
|
+
* README and its schema-catalog corpus stopped TEACHING the shorthand, so the
|
|
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.
|
|
288
|
+
*
|
|
289
|
+
* ## What is normalized, and what is deliberately NOT (objectui#4032 / #4163)
|
|
290
|
+
*
|
|
291
|
+
* The PAIR SHAPE is normalized (`value` stringified, a bare string lifted to a
|
|
292
|
+
* pair). The LABEL's authoring vocabulary is carried through untouched, because
|
|
293
|
+
* `label` is `I18nLabel` — a string OR an inline per-locale map.
|
|
294
|
+
*
|
|
295
|
+
* This line used to read:
|
|
296
|
+
*
|
|
297
|
+
* ```ts
|
|
298
|
+
* label: typeof label === 'string' && label ? label : String(value),
|
|
299
|
+
* ```
|
|
300
|
+
*
|
|
301
|
+
* which looks like defensive normalization and is data loss. An option authored
|
|
302
|
+
* `{ value: 'domestic', label: { en: 'Domestic', 'zh-CN': '国内' } }` normalized
|
|
303
|
+
* to `label: 'domestic'` — the raw STORED VALUE — so the control lost the
|
|
304
|
+
* authored text in *every* locale, English included. Nothing downstream could
|
|
305
|
+
* recover it: by the time a renderer saw the def, the map was gone.
|
|
306
|
+
*
|
|
307
|
+
* A map is therefore preserved and the render side resolves it against the
|
|
308
|
+
* active language. Anything that is neither a string nor an object is not a
|
|
309
|
+
* label in any vocabulary the spec admits, and still falls back to the value.
|
|
108
310
|
*/
|
|
109
|
-
function normalizeFilterOptions(options) {
|
|
311
|
+
function normalizeFilterOptions(options, filterName) {
|
|
110
312
|
if (!Array.isArray(options) || options.length === 0)
|
|
111
313
|
return undefined;
|
|
112
314
|
const normalized = [];
|
|
315
|
+
/** Every bare-string member, in authored order — one warning names them all. */
|
|
316
|
+
const shorthand = [];
|
|
113
317
|
for (const o of options) {
|
|
114
318
|
if (o === null || o === undefined)
|
|
115
319
|
continue;
|
|
@@ -118,17 +322,47 @@ function normalizeFilterOptions(options) {
|
|
|
118
322
|
if (value === undefined || value === null)
|
|
119
323
|
continue;
|
|
120
324
|
const label = o.label;
|
|
325
|
+
const isMap = label !== null && typeof label === 'object' && !Array.isArray(label);
|
|
121
326
|
normalized.push({
|
|
122
327
|
value: String(value),
|
|
123
|
-
label: typeof label === 'string' && label ? label : String(value),
|
|
328
|
+
label: (typeof label === 'string' && label) || isMap ? label : String(value),
|
|
124
329
|
});
|
|
125
330
|
}
|
|
126
331
|
else {
|
|
332
|
+
shorthand.push(String(o));
|
|
127
333
|
normalized.push({ value: String(o), label: String(o) });
|
|
128
334
|
}
|
|
129
335
|
}
|
|
336
|
+
if (shorthand.length > 0)
|
|
337
|
+
warnShorthandOptions(filterName, shorthand);
|
|
130
338
|
return normalized.length > 0 ? normalized : undefined;
|
|
131
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Say the deprecated shorthand out loud — once per offending filter per
|
|
342
|
+
* session, naming the filter and printing the canonical replacement.
|
|
343
|
+
*
|
|
344
|
+
* Collected per FILTER rather than per option: a filter declaring
|
|
345
|
+
* `['EMEA', 'APAC', 'AMER']` is one authoring mistake in one place, so it earns
|
|
346
|
+
* one warning carrying all three values, not three warnings the author has to
|
|
347
|
+
* reassemble. A MIXED array (`[{ value: 'won', … }, 'lost']`) names only the
|
|
348
|
+
* bare members, which are the ones that need rewriting — partial migrations
|
|
349
|
+
* happen and a warning that re-reports the already-canonical members is noise.
|
|
350
|
+
*/
|
|
351
|
+
function warnShorthandOptions(filterName, shorthand) {
|
|
352
|
+
if (!isDev())
|
|
353
|
+
return;
|
|
354
|
+
const memo = `${filterName}:${shorthand.join(',')}`;
|
|
355
|
+
if (warnedShorthandOptions.has(memo))
|
|
356
|
+
return;
|
|
357
|
+
warnedShorthandOptions.add(memo);
|
|
358
|
+
const canonical = shorthand.map((v) => `{ value: ${JSON.stringify(v)}, label: ${JSON.stringify(v)} }`).join(', ');
|
|
359
|
+
warnDateFilter(`filter "${filterName}": \`options\` carries the bare-string shorthand ` +
|
|
360
|
+
`(${shorthand.map((v) => JSON.stringify(v)).join(', ')}), which @objectstack/spec's ` +
|
|
361
|
+
`\`GlobalFilterSchema\` REFUSES at publish — a dashboard authored this way renders here ` +
|
|
362
|
+
`and is rejected the moment it reaches the platform (objectui#4356). Rewrite the stored ` +
|
|
363
|
+
`dashboard to the canonical pair form: [${canonical}]. Still lifted here for already-` +
|
|
364
|
+
`persisted dashboards; the lift is removed on the objectstack#7917 schedule.`);
|
|
365
|
+
}
|
|
132
366
|
/**
|
|
133
367
|
* Normalize a dashboard schema's filter declarations into a flat list of
|
|
134
368
|
* filter definitions. The built-in `dateRange` (when declared) comes first
|
|
@@ -148,9 +382,14 @@ export function resolveDashboardFilterDefs(schema) {
|
|
|
148
382
|
allowCustomRange: schema.dateRange.allowCustomRange,
|
|
149
383
|
});
|
|
150
384
|
}
|
|
151
|
-
for (const
|
|
152
|
-
if (!
|
|
385
|
+
for (const raw of schema.globalFilters ?? []) {
|
|
386
|
+
if (!raw?.field)
|
|
153
387
|
continue;
|
|
388
|
+
// ADR-0089 legacy-alias lift (#4165) — a stored `defaultValue: { preset }`
|
|
389
|
+
// becomes the canonical bare name BEFORE anything else reads the entry, so
|
|
390
|
+
// a legacy dashboard resolves to byte-identical defs. See
|
|
391
|
+
// `liftLegacyFilterDeclaration` for the retirement window.
|
|
392
|
+
const f = liftLegacyFilterDeclaration(raw);
|
|
154
393
|
const name = f.name || f.field;
|
|
155
394
|
if (byName.has(name) && typeof console !== 'undefined') {
|
|
156
395
|
console.warn(`[dashboard-filters] duplicate filter name "${name}" — the later definition wins`);
|
|
@@ -161,7 +400,17 @@ export function resolveDashboardFilterDefs(schema) {
|
|
|
161
400
|
field: f.field,
|
|
162
401
|
label: f.label,
|
|
163
402
|
type,
|
|
164
|
-
|
|
403
|
+
// `name` is the identifying context the deprecation warning needs, and
|
|
404
|
+
// the local above already resolved it — nothing new is threaded through a
|
|
405
|
+
// public signature for it. `normalizeFilterOptions` is module-private, so
|
|
406
|
+
// widening ITS parameter list is not a contract move.
|
|
407
|
+
//
|
|
408
|
+
// (Deliberately not restating that local's expression here: the
|
|
409
|
+
// column-identity ratchet in `__tests__/column-identity.ratchet.test.ts`
|
|
410
|
+
// is a LINE-LEVEL scanner, so a comment quoting it reads as a second
|
|
411
|
+
// dual read and fails the count — a false positive worth avoiding rather
|
|
412
|
+
// than absorbing into the inventory, which would mask a future real one.)
|
|
413
|
+
options: normalizeFilterOptions(f.options, name),
|
|
165
414
|
optionsFrom: f.optionsFrom,
|
|
166
415
|
// framework#4475 — same preset-name lifting the built-in `dateRange`
|
|
167
416
|
// above already does; see normalizeDateDefault for why a bare string is
|
|
@@ -222,7 +471,7 @@ export function buildFilterCondition(def, value) {
|
|
|
222
471
|
const v = value;
|
|
223
472
|
if (typeof v === 'object') {
|
|
224
473
|
const preset = typeof v.preset === 'string' && v.preset ? v.preset : undefined;
|
|
225
|
-
const range = preset ?
|
|
474
|
+
const range = preset ? presetBounds(preset) : undefined;
|
|
226
475
|
const from = range?.from ?? v.from;
|
|
227
476
|
const to = range?.to ?? v.to;
|
|
228
477
|
if (preset && !range) {
|
|
@@ -15,21 +15,34 @@
|
|
|
15
15
|
* Pure (no React / i18n) so it can live in `@object-ui/core` and be imported
|
|
16
16
|
* by both `@object-ui/plugin-dashboard` and `@object-ui/plugin-report`.
|
|
17
17
|
*/
|
|
18
|
+
import type { AnalyticsResult } from '@objectstack/spec/contracts';
|
|
18
19
|
import type { PercentScale } from '@objectstack/spec/data';
|
|
19
20
|
/**
|
|
20
|
-
* Column metadata the analytics server returns alongside the rows
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Column metadata the analytics server returns alongside the rows — the spec's
|
|
22
|
+
* `AnalyticsResult.fields[]` element BY REFERENCE, never a local restatement of
|
|
23
|
+
* it (objectui#3815). Read `@objectstack/spec` for what a column carries; this
|
|
24
|
+
* comment deliberately does not re-list the keys, because the enumeration it
|
|
25
|
+
* replaced was the latent bug: a hand-written interface stops at the contract
|
|
26
|
+
* of the day it was written and cannot grow with the spec, and here it would
|
|
27
|
+
* not even fail to compile — three surfaces (`plugin-dashboard`'s
|
|
28
|
+
* `DatasetWidget`, `plugin-report`'s `DatasetReportRenderer`, app-shell's
|
|
29
|
+
* `DatasetPreview`) consume this name AS the real thing, so a spec column key
|
|
30
|
+
* they never learn about is one they can never render. The same derive-don't-
|
|
31
|
+
* restate fix as the parameter side (#3613/#3753) and the adapter return side
|
|
32
|
+
* (#3752); this was the last surviving restatement of the family.
|
|
33
|
+
*
|
|
34
|
+
* `type` is REQUIRED, as the contract has it. The restatement had relaxed it to
|
|
35
|
+
* optional, which was a promise the server never asked for: nothing in this repo
|
|
36
|
+
* constructs a result column (every value originates in
|
|
37
|
+
* `ObjectStackAdapter.queryDataset`, which already declares the spec element),
|
|
38
|
+
* and nothing reads `.type` — so the widening bought no caller anything and only
|
|
39
|
+
* offered future readers a `string | undefined` the wire never produces.
|
|
40
|
+
*
|
|
41
|
+
* Still a superset of {@link ChartResultField} (which needs only
|
|
42
|
+
* name/label/format) — pinned, not merely asserted, in
|
|
43
|
+
* `__tests__/dataset-result-field-spec-parity.test.ts`.
|
|
24
44
|
*/
|
|
25
|
-
export
|
|
26
|
-
name: string;
|
|
27
|
-
type?: string;
|
|
28
|
-
label?: string;
|
|
29
|
-
format?: string;
|
|
30
|
-
currency?: string;
|
|
31
|
-
percentScale?: PercentScale;
|
|
32
|
-
}
|
|
45
|
+
export type DatasetResultField = AnalyticsResult['fields'][number];
|
|
33
46
|
/**
|
|
34
47
|
* How a percentage column's stored number relates to its displayed percentage —
|
|
35
48
|
* the server's answer to a question a `%` format string cannot express:
|
|
@@ -56,6 +69,16 @@ export type { PercentScale } from '@objectstack/spec/data';
|
|
|
56
69
|
* percent cell renderer (`formatPercent` in `@object-ui/fields`) and the dataset
|
|
57
70
|
* measure formatter ({@link formatMeasure}) so a percent renders identically as
|
|
58
71
|
* a row value and as an aggregated metric — the two surfaces can never drift.
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ That last sentence was briefly FALSE, and objectui#4576 is what made it
|
|
74
|
+
* true again. Sharing the SCALING was never enough on its own: between #4553
|
|
75
|
+
* and #4576 the cell renderer got the locale's percent CONVENTION from `Intl`
|
|
76
|
+
* while {@link formatMeasure} appended a literal '%', so a German session read
|
|
77
|
+
* `1.234,5 %` in a list cell and `1.234,5%` in a dashboard measure — the same
|
|
78
|
+
* number, scaled identically, rendered under two conventions. Both ends now go
|
|
79
|
+
* through the locale's own percent affix. If a third surface ever needs percent
|
|
80
|
+
* display, it takes BOTH halves from here — the scaling AND the convention —
|
|
81
|
+
* or this promise breaks again in the same place.
|
|
59
82
|
*/
|
|
60
83
|
export declare function percentDisplayValue(value: number): number;
|
|
61
84
|
/**
|
|
@@ -65,13 +88,49 @@ export declare function percentDisplayValue(value: number): number;
|
|
|
65
88
|
* never a misleading "$". The numeral `format` hint (e.g. "0,0", "0.0%")
|
|
66
89
|
* controls grouping / decimals / percent; it can't be baked into the row value
|
|
67
90
|
* server-side (the same number feeds charts), so it is applied here.
|
|
91
|
+
*
|
|
92
|
+
* `locale` is the BCP-47 tag of the active display locale — in React, whatever
|
|
93
|
+
* `useDisplayLocale()` returns. It is optional and LAST so every existing call
|
|
94
|
+
* keeps compiling, and omitting it reproduces the previous output byte for
|
|
95
|
+
* byte (the old code passed a literal `undefined` to `Intl`, which is exactly
|
|
96
|
+
* what an omitted argument passes now).
|
|
97
|
+
*
|
|
98
|
+
* Before objectui#4566 there was no way to pass one: all three formatting sites
|
|
99
|
+
* here hard-coded `undefined`, which is neither of the repo's two locale
|
|
100
|
+
* channels — it is the MACHINE's locale. A German session read a KPI as
|
|
101
|
+
* `1,234.5` beside a grid cell rendering the same number as `1.234,5`, and
|
|
102
|
+
* inverted separators read as a different number, not an unstyled one.
|
|
103
|
+
*
|
|
104
|
+
* ── This routes through `formatDisplayNumber` (objectui#4576) ──
|
|
105
|
+
* It did not always. #4566 measured the option mapping across 32,760
|
|
106
|
+
* value × format × currency × percentScale × locale combinations and found it
|
|
107
|
+
* LOSSLESS — routing changes no byte of output, because the policy layer
|
|
108
|
+
* `formatDisplayNumber` adds over plain `Intl` is grouping suppression keyed on
|
|
109
|
+
* a field's declared `scale`, and a measure has no `scale` to feed it (decimals
|
|
110
|
+
* here come from a numeral format PATTERN, so grouping stays on and the two
|
|
111
|
+
* agree everywhere). What blocked it was the PACKAGE BOUNDARY: the formatter
|
|
112
|
+
* lived in `@object-ui/i18n`, which depends on `i18next`/`react-i18next` and
|
|
113
|
+
* peer-depends on React, while this package is the React-free engine (AGENTS.md
|
|
114
|
+
* §3: "No UI-lib deps. Logic only.") and a runtime dependency of React-FREE
|
|
115
|
+
* consumers — the `object-ui` VS Code extension and `@object-ui/data-objectstack`.
|
|
116
|
+
*
|
|
117
|
+
* #4576 removed the boundary rather than working around it: `formatDisplayNumber`
|
|
118
|
+
* is pure, so it moved DOWN into this package (`./number-display.ts`) and
|
|
119
|
+
* `@object-ui/i18n` re-exports it. The duplicate implementation this file used
|
|
120
|
+
* to carry is gone, and with it the drift that duplicate produced — see the
|
|
121
|
+
* percent note in the body, and {@link percentDisplayValue}'s promise, which
|
|
122
|
+
* is true again.
|
|
68
123
|
*/
|
|
69
|
-
export declare function formatMeasure(v: unknown, format?: string, currency?: string, percentScale?: PercentScale): string;
|
|
124
|
+
export declare function formatMeasure(v: unknown, format?: string, currency?: string, percentScale?: PercentScale, locale?: string): string;
|
|
70
125
|
/**
|
|
71
126
|
* Format a non-measure (dimension / label) value — the server already resolves
|
|
72
127
|
* dimension display labels, so this only tidies numbers and nulls.
|
|
128
|
+
*
|
|
129
|
+
* `locale` follows {@link formatMeasure}: optional, last, and omitting it
|
|
130
|
+
* reproduces the previous machine-locale output byte for byte. Integers stay
|
|
131
|
+
* verbatim here for the same measured reason as there.
|
|
73
132
|
*/
|
|
74
|
-
export declare function formatDimensionValue(v: unknown): string;
|
|
133
|
+
export declare function formatDimensionValue(v: unknown, locale?: string): string;
|
|
75
134
|
/**
|
|
76
135
|
* Resolve a column header / measure helper pair for one result set.
|
|
77
136
|
* `headerLabel` maps a dimension/measure NAME to its display label — the
|