@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
|
@@ -73,8 +73,16 @@ const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
|
73
73
|
* available to users in the view toolbar", and already carries `rowHeight` —
|
|
74
74
|
* objectui's `showDensity` under its spec name. `group` / `hideFields` /
|
|
75
75
|
* `rowColor` are the same kind of toggle and are named after the config key
|
|
76
|
-
* they gate (`grouping`, `hiddenFields`, `rowColor`)
|
|
77
|
-
*
|
|
76
|
+
* they gate (`grouping`, `hiddenFields`, `rowColor`).
|
|
77
|
+
*
|
|
78
|
+
* That promotion has LANDED (objectui#5435, ruled Option A 2026-08-22):
|
|
79
|
+
* `UserActionsConfigSchema` declares all three from `@objectstack/spec@17.3.0`,
|
|
80
|
+
* and the spec's declaring docblock names this card. Every key this table emits
|
|
81
|
+
* is therefore authorable, which
|
|
82
|
+
* `__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts` pins with a
|
|
83
|
+
* firing control. ⛔ Do not read the three as legacy-only spellings awaiting
|
|
84
|
+
* retirement — the protocol declares them, so removing them here would leave
|
|
85
|
+
* objectui narrower than the protocol.
|
|
78
86
|
*/
|
|
79
87
|
const SHOW_FLAG_TO_USER_ACTION = {
|
|
80
88
|
showSearch: 'search',
|
|
@@ -101,6 +109,137 @@ const ARIA_KEY_ALIASES = {
|
|
|
101
109
|
label: 'ariaLabel',
|
|
102
110
|
describedBy: 'ariaDescribedBy',
|
|
103
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* The view kinds `ListView` actually draws.
|
|
114
|
+
*
|
|
115
|
+
* A total `Record<…, true>` over {@link ListViewVisualization}, for the same
|
|
116
|
+
* reason the density maps above are `Record<RowHeight, …>`: a kind added to the
|
|
117
|
+
* union fails the build HERE instead of silently staying unreadable authored
|
|
118
|
+
* input.
|
|
119
|
+
*
|
|
120
|
+
* ⚠️ That promise was FALSE between `@objectstack/spec@17.3.0` and `ca3942729`.
|
|
121
|
+
* `ViewType` was a hand-written copy of the spec's list, so this map was total
|
|
122
|
+
* over the copy and compiled green while the spec's `page` had nowhere to land.
|
|
123
|
+
* Both faces are derived now, and the two totality structures below close the
|
|
124
|
+
* other half of the union, so every member of the spec's list is accounted for
|
|
125
|
+
* by a structure that fails the build rather than by a reader's memory.
|
|
126
|
+
*
|
|
127
|
+
* `hasOwnProperty`, not `in` — same trap as {@link rowHeightToDensityMode}:
|
|
128
|
+
* `in` walks the prototype chain, so `'toString'` would read as a view kind.
|
|
129
|
+
*/
|
|
130
|
+
const LIST_VIEW_KINDS = {
|
|
131
|
+
grid: true,
|
|
132
|
+
kanban: true,
|
|
133
|
+
gallery: true,
|
|
134
|
+
calendar: true,
|
|
135
|
+
timeline: true,
|
|
136
|
+
gantt: true,
|
|
137
|
+
map: true,
|
|
138
|
+
chart: true,
|
|
139
|
+
tree: true,
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Every member of {@link ViewType} that {@link LIST_VIEW_KINDS} does NOT carry,
|
|
143
|
+
* with the reason — and, where the answer is "the author authored something
|
|
144
|
+
* valid and will get a grid", the sentence to say out loud.
|
|
145
|
+
*
|
|
146
|
+
* The two tables together still partition the derived union — a member the spec
|
|
147
|
+
* ADDS must be classed as drawable or explained HERE, and until it is the build
|
|
148
|
+
* is red — but the red lands on {@link LIST_VIEW_KINDS} alone, because a kind
|
|
149
|
+
* neither table classes is not extracted into {@link UndrawableViewKind} and so
|
|
150
|
+
* stays inside {@link ListViewVisualization}, whose record is total. A key
|
|
151
|
+
* MISSPELLED here fails the same way, for the same reason: the kind it was
|
|
152
|
+
* meant to class is not extracted either. Both directions are pinned in
|
|
153
|
+
* `__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts` rather than
|
|
154
|
+
* asserted in this sentence.
|
|
155
|
+
*
|
|
156
|
+
* ⛔ NOT annotated `Record<Exclude<ViewType, ListViewVisualization>, …>`, and
|
|
157
|
+
* the `satisfies` is not a style choice (objectui#9880). An annotation makes
|
|
158
|
+
* the literal EXACT in BOTH directions: a kind the spec adds is a missing key
|
|
159
|
+
* (wanted) and a kind the spec RETIRES is an EXCESS PROPERTY — a TS2353 that
|
|
160
|
+
* demands a deletion this repository cannot make while it resolves a published
|
|
161
|
+
* spec which still carries the kind (as it did with `page` through 17.4.0).
|
|
162
|
+
* `satisfies` keeps the value constraint
|
|
163
|
+
* (every reason is a `string` or `null`) and drops the exactness, which is what
|
|
164
|
+
* lets one spelling compile against both the pinned published spec and one
|
|
165
|
+
* built from objectstack `main`.
|
|
166
|
+
*
|
|
167
|
+
* `null` means silence is correct — these are objectui view CATEGORIES, never
|
|
168
|
+
* authored as a list-view kind:
|
|
169
|
+
* - `list` is the view CATEGORY, not a kind — it already folds to `grid`.
|
|
170
|
+
* - `detail` is a different renderer (`plugin-detail`), never a ListView case.
|
|
171
|
+
*
|
|
172
|
+
* `page` had a row here until objectui#11073. objectstack#17063 removed
|
|
173
|
+
* `type: 'page'` from the list-view enum, and the row answered for it while this
|
|
174
|
+
* repository still resolved a spec that published the kind. `@objectstack/spec`
|
|
175
|
+
* 17.5.0 carries the removal: both published `@object-ui/types` faces refuse
|
|
176
|
+
* `page`, `Extract` no longer extracted the row, and the residual pins that held
|
|
177
|
+
* it (`normalize-list-view.pageResidual-8429.test.ts`) were converted, so it was
|
|
178
|
+
* deleted as it said it would be. A stored `page` view now degrades like any
|
|
179
|
+
* kind outside the vocabulary.
|
|
180
|
+
*/
|
|
181
|
+
const UNDRAWABLE_VIEW_KINDS = {
|
|
182
|
+
list: null,
|
|
183
|
+
detail: null,
|
|
184
|
+
};
|
|
185
|
+
/** One warning per undrawable kind per process — see {@link warnUndrawableViewKind}. */
|
|
186
|
+
const warnedUndrawableKinds = new Set();
|
|
187
|
+
/**
|
|
188
|
+
* Fail-open is **loud**, not silent — the convention this repo already applies
|
|
189
|
+
* to unevaluable predicates (`../evaluator/fieldRules.ts`, objectstack#5149).
|
|
190
|
+
*
|
|
191
|
+
* The bug `ca3942729` records is not only that `type: 'page'` degrades to a
|
|
192
|
+
* grid; it is that it degrades IDENTICALLY to a typo. Measured on `5505aec`,
|
|
193
|
+
* `specType: 'page'` and `specType: 'nonsense'` both left `viewType: 'grid'`
|
|
194
|
+
* with no error, no warning and no console line — and `@object-ui/types`'
|
|
195
|
+
* published validator ACCEPTS `viewType: 'page'` while refusing the typo. An
|
|
196
|
+
* author who writes a spec-valid view and is silently handed a different one
|
|
197
|
+
* has no way to tell that from a working grid, so the degrade says so once.
|
|
198
|
+
*
|
|
199
|
+
* Once per kind per process, and never for the two objectui categories: this
|
|
200
|
+
* runs inside a normalizer that a render can call on every keystroke, and the
|
|
201
|
+
* point is one line, not a scrolling wall.
|
|
202
|
+
*/
|
|
203
|
+
function warnUndrawableViewKind(kind) {
|
|
204
|
+
const reason = Object.prototype.hasOwnProperty.call(UNDRAWABLE_VIEW_KINDS, kind)
|
|
205
|
+
? UNDRAWABLE_VIEW_KINDS[kind]
|
|
206
|
+
: null;
|
|
207
|
+
if (reason === null || warnedUndrawableKinds.has(kind))
|
|
208
|
+
return;
|
|
209
|
+
warnedUndrawableKinds.add(kind);
|
|
210
|
+
console.warn(`[object-ui] This build accepts a ${JSON.stringify(kind)} list view but cannot draw one: ` +
|
|
211
|
+
`${reason}. The view falls back to a grid. ` +
|
|
212
|
+
'Author a visualization this renderer draws, or render the page through the surface that owns it.');
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Whether `value` names a visualization `ListView` draws.
|
|
216
|
+
*
|
|
217
|
+
* Exported so a read site cannot restate the membership question as its own
|
|
218
|
+
* literal array: the gate and the seam must answer one question, which is the
|
|
219
|
+
* rule this file's own `availableViews` notes already argue for (objectui#5042,
|
|
220
|
+
* objectui#7544, and now `ca3942729`).
|
|
221
|
+
*/
|
|
222
|
+
export function isListViewVisualization(value) {
|
|
223
|
+
return typeof value === 'string' && Object.prototype.hasOwnProperty.call(LIST_VIEW_KINDS, value);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The author's view kind, or `undefined` when the value names no kind ListView
|
|
227
|
+
* draws. An unrecognized kind is deliberately left unresolved rather than
|
|
228
|
+
* written through to `viewType`: the caller's own `'grid'` default is a more
|
|
229
|
+
* honest answer than a `viewType` no branch matches (the same call
|
|
230
|
+
* `normalizeChartSchema` makes for a chart family it cannot draw).
|
|
231
|
+
*
|
|
232
|
+
* A kind the SPEC allows but this renderer cannot draw takes the same fallback
|
|
233
|
+
* and additionally warns — see {@link warnUndrawableViewKind}.
|
|
234
|
+
*/
|
|
235
|
+
function readListViewKind(value) {
|
|
236
|
+
if (typeof value !== 'string')
|
|
237
|
+
return undefined;
|
|
238
|
+
if (isListViewVisualization(value))
|
|
239
|
+
return value;
|
|
240
|
+
warnUndrawableViewKind(value);
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
104
243
|
/**
|
|
105
244
|
* Per-view-type config aliases → the spec key each one aliases (#2890, the
|
|
106
245
|
* phase-3 carry-over).
|
|
@@ -179,10 +318,34 @@ const PER_VIEW_CONFIG_ALIASES = {
|
|
|
179
318
|
* so objectui's field is typed from the spec but used as something else.
|
|
180
319
|
* That mismatch is real and out of scope here; converting formats inside a
|
|
181
320
|
* vocabulary fold would change what reaches the data source.
|
|
321
|
+
* - `data: { provider: 'object', object }` → `objectName` (#7477, step 6 of
|
|
322
|
+
* #2890). This is the spelling the published `react-blocks` contract
|
|
323
|
+
* recommends and the one `@objectstack/spec`'s `ViewDataSchema` declares;
|
|
324
|
+
* ListView read it at ZERO sites, so a page bound that way validated green
|
|
325
|
+
* upstream and rendered an empty list here with no diagnostic. The `object`
|
|
326
|
+
* provider is a `strictObject` carrying exactly `{ provider, object }`, so
|
|
327
|
+
* `objectName` captures all of it. Two deliberate departures from the folds
|
|
328
|
+
* above, both narrowing:
|
|
329
|
+
* · an already-present `objectName` WINS — this fold only fills a gap, it
|
|
330
|
+
* never re-points a binding that already resolves;
|
|
331
|
+
* · `data` is NOT deleted. Every other fold deletes because the legacy key
|
|
332
|
+
* has one meaning and one home; `data` has four providers, `api`/`value`
|
|
333
|
+
* are read live in `ListView`, and the whole block is FORWARDED to child
|
|
334
|
+
* views (the gantt branch), whose own `getDataConfig` reads `data` before
|
|
335
|
+
* `objectName`. Deleting it for one provider would rewrite what a child
|
|
336
|
+
* resolves. Nothing in `ListView` reads `data.provider === 'object'`, so
|
|
337
|
+
* keeping it cannot create a second de-facto contract inside the renderer.
|
|
182
338
|
* - `viewType`: a missing kind, or the view CATEGORY `'list'` that AI-authored
|
|
183
339
|
* metadata stores and hosts forward verbatim, becomes the renderable `'grid'`
|
|
184
340
|
* — otherwise it reaches the renderer's typeless default branch and shows as
|
|
185
|
-
* a red "Unknown component type" box.
|
|
341
|
+
* a red "Unknown component type" box. Before that default applies, the
|
|
342
|
+
* AUTHOR's kind is read (#7477): `specType` — the slot
|
|
343
|
+
* `components/renderers/layout/react-page.tsx` parks a react-tier `type` in
|
|
344
|
+
* when the SDUI envelope claims the `type` key (ADR-0078) — and then a bare
|
|
345
|
+
* `type` when it names a kind ListView draws, which the component
|
|
346
|
+
* discriminator (`'list-view'`) never does. Same two legs, same order, as
|
|
347
|
+
* `normalizeChartSchema`'s chart-family read. An explicit `viewType` still
|
|
348
|
+
* wins: this leg only fills the gap that used to resolve to `'grid'`.
|
|
186
349
|
* - each `columns` entry's IDENTITY — `name` / `fieldName` → the spec's `field`
|
|
187
350
|
* (#3104). This one MIRRORS instead of deleting, for the reason given on
|
|
188
351
|
* {@link normalizeColumnIdentities}: `columns` entries cross the package
|
|
@@ -208,7 +371,12 @@ export function normalizeListViewSchema(schema) {
|
|
|
208
371
|
const legacyFields = s.fields;
|
|
209
372
|
const foldColumns = Array.isArray(legacyFields);
|
|
210
373
|
const legacyDensity = s.densityMode;
|
|
211
|
-
|
|
374
|
+
// `hasOwnProperty`, not `in` — same trap as {@link rowHeightToDensityMode},
|
|
375
|
+
// in the fold direction: `in` walks the prototype chain, so `'toString'` used
|
|
376
|
+
// to fold into `rowHeight` as `Object.prototype.toString`, a FUNCTION, and
|
|
377
|
+
// drop the key (objectui#10868). An inherited key is an unrecognized density.
|
|
378
|
+
const foldRowHeight = typeof legacyDensity === 'string' &&
|
|
379
|
+
Object.prototype.hasOwnProperty.call(DENSITY_MODE_TO_ROW_HEIGHT, legacyDensity);
|
|
212
380
|
const legacyFilters = s.filters;
|
|
213
381
|
const foldFilter = Array.isArray(legacyFilters);
|
|
214
382
|
const legacyFlags = Object.keys(SHOW_FLAG_TO_USER_ACTION).filter((k) => typeof s[k] === 'boolean');
|
|
@@ -217,7 +385,17 @@ export function normalizeListViewSchema(schema) {
|
|
|
217
385
|
const foldAria = !!aria && Object.keys(ARIA_KEY_ALIASES).some((k) => aria[k] !== undefined);
|
|
218
386
|
const sharing = isRecord(s.sharing) ? s.sharing : undefined;
|
|
219
387
|
const foldSharing = !!sharing && (sharing.visibility !== undefined || sharing.enabled !== undefined);
|
|
388
|
+
// `data: { provider: 'object', object }` → `objectName` (#7477). Gap-fill
|
|
389
|
+
// only: a non-empty `objectName` already on the schema wins, so this can
|
|
390
|
+
// never re-point a binding that resolves today.
|
|
391
|
+
const dataConfig = isRecord(s.data) ? s.data : undefined;
|
|
392
|
+
const dataObjectName = dataConfig?.provider === 'object' && typeof dataConfig.object === 'string' && dataConfig.object
|
|
393
|
+
? dataConfig.object
|
|
394
|
+
: undefined;
|
|
395
|
+
const foldObjectName = dataObjectName !== undefined && !(typeof s.objectName === 'string' && s.objectName);
|
|
220
396
|
const viewType = s.viewType;
|
|
397
|
+
// The author's kind, read before the `'grid'` default applies (#7477).
|
|
398
|
+
const authoredViewKind = readListViewKind(s.specType) ?? readListViewKind(s.type);
|
|
221
399
|
const defaultViewKind = !viewType || viewType === 'list';
|
|
222
400
|
// The columns array the identity fold will see — mirroring the `foldColumns`
|
|
223
401
|
// precedence below (canonical `columns` wins; otherwise the legacy `fields`
|
|
@@ -246,7 +424,7 @@ export function normalizeListViewSchema(schema) {
|
|
|
246
424
|
.filter((entry) => entry !== undefined);
|
|
247
425
|
if (!foldColumns && !foldRowHeight && !foldFilter && !legacyFlags.length &&
|
|
248
426
|
!foldDescription && !foldAria && !foldSharing && !defaultViewKind &&
|
|
249
|
-
!foldColumnIdentity && !perViewFolds.length) {
|
|
427
|
+
!foldColumnIdentity && !perViewFolds.length && !foldObjectName) {
|
|
250
428
|
return schema;
|
|
251
429
|
}
|
|
252
430
|
const next = { ...s };
|
|
@@ -333,7 +511,9 @@ export function normalizeListViewSchema(schema) {
|
|
|
333
511
|
}
|
|
334
512
|
next[viewKey] = nextCfg;
|
|
335
513
|
}
|
|
514
|
+
if (foldObjectName)
|
|
515
|
+
next.objectName = dataObjectName;
|
|
336
516
|
if (defaultViewKind)
|
|
337
|
-
next.viewType = 'grid';
|
|
517
|
+
next.viewType = authoredViewKind ?? 'grid';
|
|
338
518
|
return next;
|
|
339
519
|
}
|
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
* grouped and pointed decimals the US way; and
|
|
21
21
|
* 2. `useGrouping` was never set, so a four-digit YEAR stored as
|
|
22
22
|
* `Field.number({ scale: 0 })` rendered as `2,026` — in every locale, with
|
|
23
|
-
* no field property able to turn it off.
|
|
23
|
+
* no field property able to turn it off. (That property now exists: the
|
|
24
|
+
* spec's `FieldSchema.useGrouping`, read here since objectui#11026.)
|
|
24
25
|
*
|
|
25
|
-
* Both policies now live here and nowhere else. Call sites bring the value
|
|
26
|
-
*
|
|
27
|
-
* grouping.
|
|
26
|
+
* Both policies now live here and nowhere else. Call sites bring the value, the
|
|
27
|
+
* display width and whatever the FIELD declared (`scale`, `useGrouping`); they
|
|
28
|
+
* do not bring a locale default and they do not decide grouping.
|
|
28
29
|
*
|
|
29
30
|
* ── Why this module lives in `@object-ui/core` (objectui#4576) ──
|
|
30
31
|
* It used to live in `@object-ui/i18n`, and that home cost the repo a real
|
|
@@ -57,6 +58,22 @@ export interface DisplayNumberFormatOptions {
|
|
|
57
58
|
* for a non-React caller that has no locale in hand. It never means `en-US`:
|
|
58
59
|
* assuming US conventions for the whole world is the defect this module was
|
|
59
60
|
* created to remove.
|
|
61
|
+
*
|
|
62
|
+
* ── The caller this rule governs (objectui#10098) ──
|
|
63
|
+
* The `undefined` rule above is written for a NON-REACT display caller with
|
|
64
|
+
* no tag in hand: a plain formatter, an exporter, engine code — anything that
|
|
65
|
+
* cannot call a hook. For that caller the runtime default is the viewer's
|
|
66
|
+
* own environment, and the viewer's own environment is the honest locale for
|
|
67
|
+
* a user-facing display. A malformed tag ends in the same place, whoever the
|
|
68
|
+
* caller: {@link formatDisplayNumber} retries without it rather than
|
|
69
|
+
* throwing, so the number renders in the runtime default instead of taking
|
|
70
|
+
* the cell down.
|
|
71
|
+
*
|
|
72
|
+
* A React renderer is governed by a different rule, and the two do not
|
|
73
|
+
* contradict each other. It passes what `useDisplayLocale` returns, and that
|
|
74
|
+
* hook — `@object-ui/i18n`, `packages/i18n/src/useDisplayLocale.ts` — ends
|
|
75
|
+
* its provider chain on the concrete `'en'`, for determinism, when the chain
|
|
76
|
+
* yields no tag at all. That hook's docblock names this one in return.
|
|
60
77
|
*/
|
|
61
78
|
locale?: string;
|
|
62
79
|
/**
|
|
@@ -88,14 +105,40 @@ export interface DisplayNumberFormatOptions {
|
|
|
88
105
|
*
|
|
89
106
|
* ⚠️ INTERIM DEFAULT (objectui#4033, PM ruling 2026-08-11). Suppressing
|
|
90
107
|
* grouping for every scale-0 number is a transitional policy with a known,
|
|
91
|
-
* accepted cost: a large scale-0 COUNT loses its separators too.
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* declare nothing.
|
|
108
|
+
* accepted cost: a large scale-0 COUNT loses its separators too. The
|
|
109
|
+
* authorable hint it was waiting for has landed as
|
|
110
|
+
* {@link DisplayNumberFormatOptions.useGrouping} (objectui#11026): an
|
|
111
|
+
* authored boolean OVERRIDES this default, and the heuristic is now only the
|
|
112
|
+
* fallback for a field that declares nothing.
|
|
97
113
|
*/
|
|
98
114
|
scale?: number;
|
|
115
|
+
/**
|
|
116
|
+
* The FIELD's authored `useGrouping` — `FieldSchema.useGrouping` in
|
|
117
|
+
* `@objectstack/spec`, the author's digit-grouping hint — and nothing else.
|
|
118
|
+
* Like `scale` it is a POLICY input: pass it only when a field declaration
|
|
119
|
+
* actually said so (objectui#11026).
|
|
120
|
+
*
|
|
121
|
+
* Three-valued, and the spec's own reading of each value is the one this
|
|
122
|
+
* option implements:
|
|
123
|
+
*
|
|
124
|
+
* - `undefined` — the author made no judgement, so the renderer decides:
|
|
125
|
+
* the `scale`/`currency` heuristic in {@link shouldGroupDisplayNumber},
|
|
126
|
+
* unchanged.
|
|
127
|
+
* - `false` — the author's explicit opt-out (a year, an ID, a zip code).
|
|
128
|
+
* Never grouped, whatever `scale` or `currency` would have suggested.
|
|
129
|
+
* - `true` — the author pins grouping ON, overriding the heuristic the
|
|
130
|
+
* other way (a scale-0 count that should read `2,026`).
|
|
131
|
+
*
|
|
132
|
+
* The spec maps the key 1:1 onto `Intl.NumberFormat`'s `useGrouping`, so an
|
|
133
|
+
* authored `true` reaches `Intl` as `true`, which means "always". That is
|
|
134
|
+
* deliberately NOT what the heuristic's own "group" answer does: it omits
|
|
135
|
+
* the key and keeps the locale's "auto" (see the `Intl` note inside
|
|
136
|
+
* {@link formatDisplayNumber}). The two differ where a locale leaves a
|
|
137
|
+
* four-digit number alone under "auto" (es-ES and pl-PL render `1234`); an
|
|
138
|
+
* author who pinned grouping on gets `1.234` / `1 234` there, because that
|
|
139
|
+
* is what they declared.
|
|
140
|
+
*/
|
|
141
|
+
useGrouping?: boolean;
|
|
99
142
|
/**
|
|
100
143
|
* How the number relates to a percentage — the ONE place that distinction is
|
|
101
144
|
* expressed, because getting it wrong is the whole of objectui#4576.
|
|
@@ -137,11 +180,17 @@ export interface DisplayNumberFormatOptions {
|
|
|
137
180
|
* The grouping policy, alone and testable: does this number get thousands
|
|
138
181
|
* separators?
|
|
139
182
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
183
|
+
* This is the ONE place the decision is made. An authored `useGrouping`
|
|
184
|
+
* answers it outright; only when the field declared nothing does the
|
|
185
|
+
* `scale`/`currency` heuristic below get a say (objectui#11026).
|
|
186
|
+
*
|
|
187
|
+
* @param scale the field's declared `scale`, or `undefined` when the
|
|
188
|
+
* caller has no field declaration behind it
|
|
189
|
+
* @param currency ISO 4217 code when the number is money
|
|
190
|
+
* @param useGrouping the field's authored `useGrouping`, or `undefined` when
|
|
191
|
+
* the author declared none
|
|
143
192
|
*/
|
|
144
|
-
export declare function shouldGroupDisplayNumber(scale?: number, currency?: string): boolean;
|
|
193
|
+
export declare function shouldGroupDisplayNumber(scale?: number, currency?: string, useGrouping?: boolean): boolean;
|
|
145
194
|
/**
|
|
146
195
|
* Format a number for DISPLAY, in the active locale, under the grouping policy
|
|
147
196
|
* above.
|
|
@@ -9,11 +9,23 @@
|
|
|
9
9
|
* The grouping policy, alone and testable: does this number get thousands
|
|
10
10
|
* separators?
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* This is the ONE place the decision is made. An authored `useGrouping`
|
|
13
|
+
* answers it outright; only when the field declared nothing does the
|
|
14
|
+
* `scale`/`currency` heuristic below get a say (objectui#11026).
|
|
15
|
+
*
|
|
16
|
+
* @param scale the field's declared `scale`, or `undefined` when the
|
|
17
|
+
* caller has no field declaration behind it
|
|
18
|
+
* @param currency ISO 4217 code when the number is money
|
|
19
|
+
* @param useGrouping the field's authored `useGrouping`, or `undefined` when
|
|
20
|
+
* the author declared none
|
|
15
21
|
*/
|
|
16
|
-
export function shouldGroupDisplayNumber(scale, currency) {
|
|
22
|
+
export function shouldGroupDisplayNumber(scale, currency, useGrouping) {
|
|
23
|
+
// The author's explicit declaration beats every guess below, money included:
|
|
24
|
+
// the spec reads `false` as "never grouped, regardless of what the
|
|
25
|
+
// renderer's heuristic would have guessed", and `true` as the same override
|
|
26
|
+
// in the other direction.
|
|
27
|
+
if (typeof useGrouping === 'boolean')
|
|
28
|
+
return useGrouping;
|
|
17
29
|
// Money always groups — including money whose currency code could not be
|
|
18
30
|
// resolved, which still renders as an amount (just without a symbol).
|
|
19
31
|
if (currency)
|
|
@@ -32,7 +44,7 @@ export function shouldGroupDisplayNumber(scale, currency) {
|
|
|
32
44
|
* malformed tag from a tenant config must never take a grid cell down.
|
|
33
45
|
*/
|
|
34
46
|
export function formatDisplayNumber(value, options = {}) {
|
|
35
|
-
const { locale, currency, scale, style, ...passthrough } = options;
|
|
47
|
+
const { locale, currency, scale, useGrouping, style, ...passthrough } = options;
|
|
36
48
|
const intlOptions = { ...passthrough };
|
|
37
49
|
if (style === 'percentPoints') {
|
|
38
50
|
// The locale's percent convention WITHOUT `style: 'percent'`'s ×100 — see
|
|
@@ -50,15 +62,25 @@ export function formatDisplayNumber(value, options = {}) {
|
|
|
50
62
|
delete intlOptions.unit;
|
|
51
63
|
delete intlOptions.unitDisplay;
|
|
52
64
|
}
|
|
53
|
-
// ⚠️
|
|
54
|
-
// as omitting the key: `true` means
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// "auto" → `1234` but "always" → `1
|
|
58
|
-
//
|
|
59
|
-
|
|
65
|
+
// ⚠️ Never write `useGrouping: true` on the renderer's own behalf.
|
|
66
|
+
// `useGrouping: true` is NOT the same as omitting the key: `true` means
|
|
67
|
+
// "always", while omitting it means "auto" (and "min2" under compact
|
|
68
|
+
// notation), which is the locale's own preference. Measured — for 1234:
|
|
69
|
+
// es-ES "auto" → `1234` but "always" → `1.234`; pl-PL "auto" → `1234` but
|
|
70
|
+
// "always" → `1 234`. Writing `true` for a field that declared nothing would
|
|
71
|
+
// silently override those locales' conventions in the name of preserving
|
|
72
|
+
// en-US output.
|
|
73
|
+
//
|
|
74
|
+
// The ONE `true` this function writes is the author's own (objectui#11026):
|
|
75
|
+
// `FieldSchema.useGrouping` maps 1:1 onto this `Intl` option, and an author
|
|
76
|
+
// who pinned grouping on asked for "always". The decision itself is made
|
|
77
|
+
// once, above, by `shouldGroupDisplayNumber`; this only spells it for `Intl`.
|
|
78
|
+
if (!shouldGroupDisplayNumber(scale, currency, useGrouping)) {
|
|
60
79
|
intlOptions.useGrouping = false;
|
|
61
80
|
}
|
|
81
|
+
else if (useGrouping === true) {
|
|
82
|
+
intlOptions.useGrouping = true;
|
|
83
|
+
}
|
|
62
84
|
try {
|
|
63
85
|
return new Intl.NumberFormat(locale, intlOptions).format(value);
|
|
64
86
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* object-aggregate-query — the ONE answer to "which CALL does an object-bound
|
|
10
|
+
* `aggregate` make when its authored `groupBy` is the structured node?"
|
|
11
|
+
* (objectui#8613).
|
|
12
|
+
*
|
|
13
|
+
* ## The two wires, and why the choice is not cosmetic
|
|
14
|
+
*
|
|
15
|
+
* An authored `aggregate.groupBy` is a union: a bare field name, or the
|
|
16
|
+
* structured date-bucketing node `{ field, dateGranularity?, alias? }` — the
|
|
17
|
+
* union `ChartGroupBySchema` declares and `GroupByNodeSchema` (the query AST)
|
|
18
|
+
* mirrors. The adapter routes on the SHAPE of the params it is handed, not on
|
|
19
|
+
* the shape of the authored value:
|
|
20
|
+
*
|
|
21
|
+
* - `{ groupBy: GroupByNode[], aggregations, where }` — the spec-shape query,
|
|
22
|
+
* which reaches `engine.aggregate` and runs the server-side date-bucket
|
|
23
|
+
* engine;
|
|
24
|
+
* - `{ field, function, groupBy, filter }` — the legacy cube/analytics query,
|
|
25
|
+
* whose `dimensions` the contract declares as an array of dimension NAMES
|
|
26
|
+
* and which does NOT honour `dateGranularity` at all.
|
|
27
|
+
*
|
|
28
|
+
* So a structured node forwarded on the legacy wire is posted as a dimension
|
|
29
|
+
* OBJECT where a name is declared: the author asks for monthly buckets and the
|
|
30
|
+
* platform answers a different question, with no diagnostic on the way. That is
|
|
31
|
+
* the defect objectui#8613 records on the metric path, and it was reachable
|
|
32
|
+
* there only because this payload was spelled INSIDE one renderer
|
|
33
|
+
* (`ObjectChart.runAggregate`) rather than anywhere a second renderer could
|
|
34
|
+
* reuse it.
|
|
35
|
+
*
|
|
36
|
+
* ## Why a shared module rather than a second copy
|
|
37
|
+
*
|
|
38
|
+
* The alternative was to transcribe the eight lines into the metric widget.
|
|
39
|
+
* That is the objectui#5042 / #7544 / #8193 / #8266 / #8269 drift shape — a
|
|
40
|
+
* second opinion of one question, with nothing keeping the two in agreement —
|
|
41
|
+
* and it is the same shape those cards' fixes (`chartMeasureKey`,
|
|
42
|
+
* `chartCategoryKey`) exist to end, one column at a time. This module is that
|
|
43
|
+
* move applied to the REQUEST rather than to a result column.
|
|
44
|
+
*
|
|
45
|
+
* ## What is NOT here
|
|
46
|
+
*
|
|
47
|
+
* The legacy bag itself. The two renderers do not agree on it and should not be
|
|
48
|
+
* made to: a chart passes the authored `groupBy` through untouched, while a
|
|
49
|
+
* metric floors an absent one at `'_all'` (one bucket) because it paints a
|
|
50
|
+
* single number. Folding that difference into a shared builder would put a
|
|
51
|
+
* renderer's own default into a module that has no business holding one, so the
|
|
52
|
+
* shared part stops exactly where the agreement stops.
|
|
53
|
+
*/
|
|
54
|
+
import type { ChartGroupBy } from '@objectstack/spec/ui';
|
|
55
|
+
import { type ChartAggregateLike } from './chart-measure-key.js';
|
|
56
|
+
/**
|
|
57
|
+
* The STRUCTURED arm of the contract's `groupBy` union — the date-bucketing
|
|
58
|
+
* node, as distinct from the bare field name the same union admits.
|
|
59
|
+
*
|
|
60
|
+
* Extracted from `ChartGroupBy` rather than restated, so a member added to the
|
|
61
|
+
* node upstream (as `alias` once was) arrives here without an edit.
|
|
62
|
+
*/
|
|
63
|
+
export type StructuredGroupBy = Extract<ChartGroupBy, {
|
|
64
|
+
field: string;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* The spec-shape aggregate query's `aggregations[]` entry, as
|
|
68
|
+
* `AggregationNodeSchema` declares it: `alias` is REQUIRED (it names the column
|
|
69
|
+
* the measure is projected under), `field` is optional because a `count` counts
|
|
70
|
+
* rows rather than a column.
|
|
71
|
+
*/
|
|
72
|
+
export interface ObjectAggregationNode {
|
|
73
|
+
function?: string;
|
|
74
|
+
alias: string;
|
|
75
|
+
field?: string;
|
|
76
|
+
}
|
|
77
|
+
/** The spec-shape query body — `{ groupBy: GroupByNode[], aggregations, where }`. */
|
|
78
|
+
export interface ObjectAggregateSpecQuery {
|
|
79
|
+
groupBy: StructuredGroupBy[];
|
|
80
|
+
aggregations: ObjectAggregationNode[];
|
|
81
|
+
where: unknown;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Is this authored `aggregate.groupBy` the structured node rather than a bare
|
|
85
|
+
* field name?
|
|
86
|
+
*
|
|
87
|
+
* ⚠️ An ARRAY answers `false`. It is an object, but it is not this union's
|
|
88
|
+
* object arm — neither `ChartGroupBySchema` nor `ObjectMetricPropsSchema`'s
|
|
89
|
+
* `aggregate` admits one at this position, and `data-objectstack`'s adapter
|
|
90
|
+
* already refuses it at the wire (`503cd8b89`). Letting it through here would
|
|
91
|
+
* wrap it into `groupBy: [[…]]` and turn a producer-side refusal into a
|
|
92
|
+
* malformed query.
|
|
93
|
+
*/
|
|
94
|
+
export declare function isStructuredGroupBy(groupBy: unknown): groupBy is StructuredGroupBy;
|
|
95
|
+
/**
|
|
96
|
+
* The spec-shape aggregate query for an object-bound `aggregate` whose
|
|
97
|
+
* `groupBy` is the structured node.
|
|
98
|
+
*
|
|
99
|
+
* The measure is projected under {@link chartMeasureKey}'s answer — the raw
|
|
100
|
+
* field name, or the literal `'count'` for a fieldless count — so a caller's
|
|
101
|
+
* existing result readback finds the value under the key it already looks for,
|
|
102
|
+
* and a chart's series binding names the column the rows actually carry.
|
|
103
|
+
*
|
|
104
|
+
* `field` is omitted for `count` so the engine emits `count(*)`: the dashboard
|
|
105
|
+
* relays default `field: 'value'` for a widget with no explicit measure column,
|
|
106
|
+
* and `count(value)` crashes SQL drivers with "no such column: value" on the
|
|
107
|
+
* count-the-rows case that is the common one.
|
|
108
|
+
*
|
|
109
|
+
* @param aggregate the authored inline aggregate
|
|
110
|
+
* @param groupBy its `groupBy`, already narrowed by {@link isStructuredGroupBy}
|
|
111
|
+
* @param where the resolved filter, in the spec Query DSL's `where` position
|
|
112
|
+
*/
|
|
113
|
+
export declare function objectAggregateSpecQuery(aggregate: ChartAggregateLike, groupBy: StructuredGroupBy, where: unknown): ObjectAggregateSpecQuery;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
import { chartMeasureKey } from './chart-measure-key.js';
|
|
9
|
+
/**
|
|
10
|
+
* Is this authored `aggregate.groupBy` the structured node rather than a bare
|
|
11
|
+
* field name?
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ An ARRAY answers `false`. It is an object, but it is not this union's
|
|
14
|
+
* object arm — neither `ChartGroupBySchema` nor `ObjectMetricPropsSchema`'s
|
|
15
|
+
* `aggregate` admits one at this position, and `data-objectstack`'s adapter
|
|
16
|
+
* already refuses it at the wire (`503cd8b89`). Letting it through here would
|
|
17
|
+
* wrap it into `groupBy: [[…]]` and turn a producer-side refusal into a
|
|
18
|
+
* malformed query.
|
|
19
|
+
*/
|
|
20
|
+
export function isStructuredGroupBy(groupBy) {
|
|
21
|
+
return !!groupBy && typeof groupBy === 'object' && !Array.isArray(groupBy);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The spec-shape aggregate query for an object-bound `aggregate` whose
|
|
25
|
+
* `groupBy` is the structured node.
|
|
26
|
+
*
|
|
27
|
+
* The measure is projected under {@link chartMeasureKey}'s answer — the raw
|
|
28
|
+
* field name, or the literal `'count'` for a fieldless count — so a caller's
|
|
29
|
+
* existing result readback finds the value under the key it already looks for,
|
|
30
|
+
* and a chart's series binding names the column the rows actually carry.
|
|
31
|
+
*
|
|
32
|
+
* `field` is omitted for `count` so the engine emits `count(*)`: the dashboard
|
|
33
|
+
* relays default `field: 'value'` for a widget with no explicit measure column,
|
|
34
|
+
* and `count(value)` crashes SQL drivers with "no such column: value" on the
|
|
35
|
+
* count-the-rows case that is the common one.
|
|
36
|
+
*
|
|
37
|
+
* @param aggregate the authored inline aggregate
|
|
38
|
+
* @param groupBy its `groupBy`, already narrowed by {@link isStructuredGroupBy}
|
|
39
|
+
* @param where the resolved filter, in the spec Query DSL's `where` position
|
|
40
|
+
*/
|
|
41
|
+
export function objectAggregateSpecQuery(aggregate, groupBy, where) {
|
|
42
|
+
const aggFn = aggregate.function;
|
|
43
|
+
const node = {
|
|
44
|
+
function: aggFn,
|
|
45
|
+
alias: chartMeasureKey(aggregate, aggFn || 'count'),
|
|
46
|
+
};
|
|
47
|
+
if (aggFn !== 'count' && aggregate.field)
|
|
48
|
+
node.field = aggregate.field;
|
|
49
|
+
return { groupBy: [groupBy], aggregations: [node], where };
|
|
50
|
+
}
|