@object-ui/core 17.6.0 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7260 -0
- package/README.md +228 -14
- package/dist/actions/ActionEngine.d.ts +4 -2
- package/dist/actions/ActionEngine.js +13 -8
- package/dist/actions/ActionRunner.d.ts +473 -69
- package/dist/actions/ActionRunner.js +792 -125
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/UndoManager.d.ts +8 -13
- package/dist/actions/actionKeys.d.ts +9 -7
- package/dist/actions/actionKeys.js +67 -14
- package/dist/actions/actionResponse.js +2 -2
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/navigationTarget.d.ts +41 -0
- package/dist/actions/navigationTarget.js +20 -0
- package/dist/actions/recordDelete.d.ts +154 -0
- package/dist/actions/recordDelete.js +110 -0
- package/dist/adapters/ApiDataSource.d.ts +8 -4
- package/dist/adapters/ApiDataSource.js +19 -7
- 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/adapters/resolveDataSource.d.ts +9 -6
- package/dist/adapters/resolveDataSource.js +9 -12
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +8 -25
- package/dist/data-scope/ViewDataProvider.js +27 -54
- 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/deadlineSettled.d.ts +40 -0
- package/dist/evaluator/deadlineSettled.js +45 -0
- 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 +208 -31
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -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 +105 -0
- package/dist/evaluator/subjectPermissions.js +86 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +83 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +382 -34
- package/dist/registry/Registry.js +247 -12
- 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/affordanceGrants.d.ts +371 -0
- package/dist/utils/affordanceGrants.js +252 -0
- 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 +165 -5
- package/dist/utils/chart-series.js +123 -18
- 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 +346 -0
- package/dist/utils/date-display.js +644 -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 +81 -9
- package/dist/utils/expand-fields.js +81 -9
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +158 -6
- package/dist/utils/filter-converter.js +1098 -24
- 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 +135 -11
- package/dist/utils/normalize-list-view.js +243 -64
- 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 +141 -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 +225 -83
- package/dist/validation/server-owned-value.d.ts +5 -1
- package/dist/validation/server-owned-value.js +5 -1
- 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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — served per-column sortability projection (consumer side)
|
|
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
|
+
* The consumer half of objectstack#10235 (maintainer ruling A, 2026-08-23):
|
|
10
|
+
* the platform serves an explicit per-column sortability signal, and a grid
|
|
11
|
+
* reads THAT to decide whether a header offers a sort click — it never
|
|
12
|
+
* re-derives "virtual ⇒ unsortable" from the field's `type`.
|
|
13
|
+
*
|
|
14
|
+
* ## What the platform serves
|
|
15
|
+
*
|
|
16
|
+
* `GET /api/v1/meta/object/:name` answers `GetMetaItemResponseSchema`, whose
|
|
17
|
+
* ENVELOPE (never the document under `item` — `FieldSchema` is a
|
|
18
|
+
* `strictObject`, so the key must stay un-authorable) carries an optional
|
|
19
|
+
* `sortability` key when the served type is `object`, on every serving branch
|
|
20
|
+
* including the cached one:
|
|
21
|
+
*
|
|
22
|
+
* ```jsonc
|
|
23
|
+
* { "type": "object", "name": "crm_opportunity", "item": { … },
|
|
24
|
+
* "sortability": { "fields": {
|
|
25
|
+
* "amount": { "sortable": true },
|
|
26
|
+
* "expected_revenue": { "sortable": false, "reason": "virtual-type" },
|
|
27
|
+
* "remote_anchor": { "sortable": true, "caveat": "unprovisioned-anchor" }
|
|
28
|
+
* } } }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Shape verified against the MERGED upstream change, not against a description
|
|
32
|
+
* of it: `@objectstack/spec`'s `ObjectSortabilitySchema` /
|
|
33
|
+
* `FieldSortabilitySchema` and `GetMetaItemResponseSchema.sortability`.
|
|
34
|
+
*
|
|
35
|
+
* ## The contract, and the asymmetry that makes it easy to get backwards
|
|
36
|
+
*
|
|
37
|
+
* Offer a sort affordance on a column **iff the projection has an entry for
|
|
38
|
+
* the column's field name AND that entry says `sortable: true`**.
|
|
39
|
+
*
|
|
40
|
+
* ABSENCE OF AN ENTRY MEANS "no platform sort behind this name" — never
|
|
41
|
+
* "assume sortable". The projection's domain is exactly the served field map
|
|
42
|
+
* plus the always-provisioned `id`, so an unknown name, a dotted path
|
|
43
|
+
* (`account.name`) and an unprovisioned audit column all arrive as absence,
|
|
44
|
+
* and all three are refused by the runtime doors. A `!== false` test — the
|
|
45
|
+
* spelling every other optional flag in this repo uses — answers `true` for
|
|
46
|
+
* absence and so gets exactly that family backwards. {@link isPlatformSortableField}
|
|
47
|
+
* is the only spelling of this test; do not inline a comparison at a call site.
|
|
48
|
+
*
|
|
49
|
+
* ## Present-vs-absent PROJECTION is a different question
|
|
50
|
+
*
|
|
51
|
+
* `readObjectSortability` returns `undefined` when the served metadata carried
|
|
52
|
+
* no `sortability` key at all — a backend older than the upstream change, an
|
|
53
|
+
* inline/mock data source, a fixture. That is NOT "every column is unsortable":
|
|
54
|
+
* it is "no signal was served", and the caller decides what it did before the
|
|
55
|
+
* signal existed. The two are deliberately different types (`undefined` vs an
|
|
56
|
+
* empty `fields` map) so a caller cannot conflate them by accident.
|
|
57
|
+
*
|
|
58
|
+
* ## `caveat: 'unprovisioned-anchor'` is NOT a refusal
|
|
59
|
+
*
|
|
60
|
+
* Platform-injected anchors on ADR-0015 `external` objects come back
|
|
61
|
+
* `sortable: true, caveat: 'unprovisioned-anchor'`: the runtime ACCEPTS the
|
|
62
|
+
* sort, but silently drops it (`asc` === `desc` under a `200`) when the remote
|
|
63
|
+
* table carries no such column — a degradation the platform cannot prove
|
|
64
|
+
* either way at serve time. This module treats the caveat as advisory and the
|
|
65
|
+
* `sortable` verdict as the enforcement fact, so a caveated column keeps its
|
|
66
|
+
* click. Refusing what the platform does not refuse would recreate
|
|
67
|
+
* declared-≠-enforced drift in mirror image; whether to badge or withhold on
|
|
68
|
+
* the caveat is an open affordance question, not something to settle here.
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* The served shape is the SPEC's, re-exported rather than restated.
|
|
72
|
+
*
|
|
73
|
+
* `FIELD_UNSORTABLE_VIRTUAL_TYPE` / `FIELD_SORTABLE_UNPROVISIONED_ANCHOR` are
|
|
74
|
+
* the two string literals the projection can carry, and `FieldSortability` /
|
|
75
|
+
* `ObjectSortability` are the wire types themselves (`z.input` of the
|
|
76
|
+
* schemas the REST layer answers with). Re-exporting is the point of a
|
|
77
|
+
* contract-first consumer: a local declaration under a spec export's name is
|
|
78
|
+
* read by the next agent as the spec's own definition, and then drifts from
|
|
79
|
+
* it — which is what `check:spec-symbols` guards, and what it caught here.
|
|
80
|
+
*/
|
|
81
|
+
export { FIELD_UNSORTABLE_VIRTUAL_TYPE, FIELD_SORTABLE_UNPROVISIONED_ANCHOR, } from '@objectstack/spec/api';
|
|
82
|
+
/**
|
|
83
|
+
* Where the projection rides on the client-side object schema.
|
|
84
|
+
*
|
|
85
|
+
* A GLOBAL-REGISTRY SYMBOL, deliberately — the same carrier shape
|
|
86
|
+
* `INFLIGHT_GET_REGISTRY_KEY` uses in `@object-ui/types`. Three properties
|
|
87
|
+
* this signal needs and a string key would not give it:
|
|
88
|
+
*
|
|
89
|
+
* - `JSON.stringify` drops symbol-keyed properties, so a schema object that
|
|
90
|
+
* is ever round-tripped back at a metadata WRITE endpoint cannot carry the
|
|
91
|
+
* projection into a document the server parses `strict` and rejects by
|
|
92
|
+
* name. The upstream change kept the key off `item` for exactly that
|
|
93
|
+
* reason; stamping a string key here would undo it one repo away.
|
|
94
|
+
* - `Object.keys` / spread / `for…in` do not see it, so every existing reader
|
|
95
|
+
* that enumerates the schema (column generators, diffing, form builders)
|
|
96
|
+
* is unchanged by its presence.
|
|
97
|
+
* - `Symbol.for` is the cross-realm registry, so two copies of this module
|
|
98
|
+
* (a workspace build and a published one in the same graph) agree on the
|
|
99
|
+
* key rather than silently reading past each other.
|
|
100
|
+
*/
|
|
101
|
+
export const OBJECT_SORTABILITY_KEY = Symbol.for('objectui.objectSortability');
|
|
102
|
+
/** Narrow one served entry, tolerating an unparsed / hand-built record. */
|
|
103
|
+
function asFieldSortability(value) {
|
|
104
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
105
|
+
return undefined;
|
|
106
|
+
const sortable = value.sortable;
|
|
107
|
+
if (typeof sortable !== 'boolean')
|
|
108
|
+
return undefined;
|
|
109
|
+
const reason = value.reason;
|
|
110
|
+
const caveat = value.caveat;
|
|
111
|
+
// `reason` / `caveat` are typed as closed literal unions by the spec, and
|
|
112
|
+
// this is unparsed input off the wire. Preserve whatever string arrived
|
|
113
|
+
// rather than dropping an annotation a NEWER platform added — the verdict
|
|
114
|
+
// this module acts on is `sortable`, and an unrecognised advisory string is
|
|
115
|
+
// strictly better carried than silently erased. The assertion is the
|
|
116
|
+
// boundary cast that admission implies, and it is confined to this function.
|
|
117
|
+
return {
|
|
118
|
+
sortable,
|
|
119
|
+
...(typeof reason === 'string' ? { reason } : {}),
|
|
120
|
+
...(typeof caveat === 'string' ? { caveat } : {}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Normalize a served `sortability` envelope value into the projection, or
|
|
125
|
+
* `undefined` when the envelope carried nothing usable.
|
|
126
|
+
*
|
|
127
|
+
* Tolerant on purpose (the same contract the upstream resolver carries): the
|
|
128
|
+
* adapter hands this whatever came back over the wire, unparsed.
|
|
129
|
+
*/
|
|
130
|
+
export function normalizeObjectSortability(served) {
|
|
131
|
+
if (!served || typeof served !== 'object' || Array.isArray(served))
|
|
132
|
+
return undefined;
|
|
133
|
+
const rawFields = served.fields;
|
|
134
|
+
if (!rawFields || typeof rawFields !== 'object' || Array.isArray(rawFields))
|
|
135
|
+
return undefined;
|
|
136
|
+
const fields = {};
|
|
137
|
+
for (const [name, entry] of Object.entries(rawFields)) {
|
|
138
|
+
const verdict = asFieldSortability(entry);
|
|
139
|
+
if (verdict)
|
|
140
|
+
fields[name] = verdict;
|
|
141
|
+
}
|
|
142
|
+
return { fields };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Stamp the served projection onto the client-side object schema, under
|
|
146
|
+
* {@link OBJECT_SORTABILITY_KEY}.
|
|
147
|
+
*
|
|
148
|
+
* Non-enumerable and non-writable-by-accident: the property is defined rather
|
|
149
|
+
* than assigned so it never shows up in a spread of the schema and cannot be
|
|
150
|
+
* clobbered by one. Returns the same object it was handed (the adapter caches
|
|
151
|
+
* that instance), and is a no-op when there is nothing usable to attach — so
|
|
152
|
+
* an older backend leaves the schema exactly as it found it rather than
|
|
153
|
+
* stamping an empty projection that would read as "nothing is sortable".
|
|
154
|
+
*/
|
|
155
|
+
export function attachObjectSortability(schema, served) {
|
|
156
|
+
const projection = normalizeObjectSortability(served);
|
|
157
|
+
if (!projection || !schema || typeof schema !== 'object')
|
|
158
|
+
return schema;
|
|
159
|
+
Object.defineProperty(schema, OBJECT_SORTABILITY_KEY, {
|
|
160
|
+
value: projection,
|
|
161
|
+
enumerable: false,
|
|
162
|
+
configurable: true,
|
|
163
|
+
writable: true,
|
|
164
|
+
});
|
|
165
|
+
return schema;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Read the served projection off a client-side object schema.
|
|
169
|
+
*
|
|
170
|
+
* `undefined` means NO SIGNAL WAS SERVED — not "nothing is sortable". See the
|
|
171
|
+
* module docblock: callers must branch on this before applying the contract.
|
|
172
|
+
*/
|
|
173
|
+
export function readObjectSortability(schema) {
|
|
174
|
+
if (!schema || typeof schema !== 'object')
|
|
175
|
+
return undefined;
|
|
176
|
+
const carried = schema[OBJECT_SORTABILITY_KEY];
|
|
177
|
+
if (!carried || typeof carried !== 'object')
|
|
178
|
+
return undefined;
|
|
179
|
+
const fields = carried.fields;
|
|
180
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields))
|
|
181
|
+
return undefined;
|
|
182
|
+
return carried;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* THE contract test: does the platform offer a sort over this field name?
|
|
186
|
+
*
|
|
187
|
+
* `true` iff the projection has an entry for `fieldName` and that entry says
|
|
188
|
+
* `sortable: true`. Every other case — no entry, `sortable: false`, a
|
|
189
|
+
* malformed entry — answers `false`, because absence means "no platform sort
|
|
190
|
+
* behind this name".
|
|
191
|
+
*
|
|
192
|
+
* `sortability` is REQUIRED and non-optional on purpose: a caller holding
|
|
193
|
+
* `undefined` (no signal served) has a different question to answer and must
|
|
194
|
+
* not reach this function with it. See {@link readObjectSortability}.
|
|
195
|
+
*/
|
|
196
|
+
export function isPlatformSortableField(sortability, fieldName) {
|
|
197
|
+
if (typeof fieldName !== 'string' || fieldName.length === 0)
|
|
198
|
+
return false;
|
|
199
|
+
const entry = sortability.fields?.[fieldName];
|
|
200
|
+
return asFieldSortability(entry)?.sortable === true;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Drop from a sort list every entry the platform will not order by.
|
|
204
|
+
*
|
|
205
|
+
* The RESTORE leg of the same contract: withholding the header click stops a
|
|
206
|
+
* fresh unsortable sort from being created, but a sort persisted BEFORE the
|
|
207
|
+
* signal existed is already in stored view state, and replaying it would keep
|
|
208
|
+
* a refused `$orderby` alive and re-persist it on the next unrelated edit.
|
|
209
|
+
* Filtering the list at the seam the grid both renders from and emits through
|
|
210
|
+
* means an already-stored entry is inert and is dropped the first time
|
|
211
|
+
* anything writes the sort back.
|
|
212
|
+
*/
|
|
213
|
+
export function filterPlatformSortableSort(sort, sortability) {
|
|
214
|
+
if (!Array.isArray(sort))
|
|
215
|
+
return [];
|
|
216
|
+
return sort.filter((item) => isPlatformSortableField(sortability, item?.field));
|
|
217
|
+
}
|
|
@@ -27,6 +27,31 @@ export interface DashboardFilterDef {
|
|
|
27
27
|
name: string;
|
|
28
28
|
/** Default target field when a widget declares no explicit binding. */
|
|
29
29
|
field: string;
|
|
30
|
+
/**
|
|
31
|
+
* The object `field` lives on, carried through from
|
|
32
|
+
* @objectstack/spec's `GlobalFilterSchema.object` (`061f5e829`).
|
|
33
|
+
*
|
|
34
|
+
* The spec's own describe text for that key states the contract: "Object
|
|
35
|
+
* whose `fields.<object>.<field>` translation-bundle entry resolves this
|
|
36
|
+
* filter's field label and option labels". It was shipped on the spec side
|
|
37
|
+
* and never reached a consumer here, because this builder names the keys it
|
|
38
|
+
* copies and this one was not among them — so the value could not reach a
|
|
39
|
+
* renderer even in principle.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ NOT `optionsFrom.object`, which the spec separates deliberately: that
|
|
42
|
+
* one names the object dynamic OPTIONS are fetched from and may differ
|
|
43
|
+
* (filtering `opportunity` by `owner` with options sourced from `user`),
|
|
44
|
+
* while this one names the object whose translation bundle is keyed by
|
|
45
|
+
* `field`. Reading the former for label resolution resolves against the
|
|
46
|
+
* wrong object for exactly that filter.
|
|
47
|
+
*
|
|
48
|
+
* Like {@link DashboardFilterDef.label}, this module carries it through
|
|
49
|
+
* UNRESOLVED — `@object-ui/core` is locale-free by design and the bundle
|
|
50
|
+
* read belongs to the render side. Omitted entirely when the author declared
|
|
51
|
+
* none, so a dashboard that never names an object resolves to a def with the
|
|
52
|
+
* same key set it always had.
|
|
53
|
+
*/
|
|
54
|
+
object?: string;
|
|
30
55
|
/**
|
|
31
56
|
* Display label, in @objectstack/spec's `I18nLabel` vocabulary — a plain
|
|
32
57
|
* string, or an inline per-locale map (`{ en: 'Owner', 'zh-CN': '负责人' }`).
|
|
@@ -49,10 +74,11 @@ export interface DashboardFilterDef {
|
|
|
49
74
|
* `resolveDashboardFilterDefs` — consumers always see the object form.
|
|
50
75
|
*
|
|
51
76
|
* The canonical authoring form is @objectstack/spec's `{ value, label }`
|
|
52
|
-
* pair, and it is the ONLY one the platform accepts at publish.
|
|
53
|
-
* shorthand
|
|
54
|
-
*
|
|
55
|
-
*
|
|
77
|
+
* pair, and it is the ONLY one the platform accepts at publish. The
|
|
78
|
+
* bare-string shorthand a STORED document may still carry is NOT lifted
|
|
79
|
+
* (objectui#4356, retired on the objectstack#7917 option-② schedule): such
|
|
80
|
+
* a member yields no option, and `normalizeFilterOptions` says so once. Do
|
|
81
|
+
* not author one; rewrite `"X"` as `{ "value": "X", "label": "X" }`.
|
|
56
82
|
*
|
|
57
83
|
* The PAIR SHAPE is normalized; the label's own vocabulary is not. `label`
|
|
58
84
|
* is `I18nLabel` in `GlobalFilterSchema.options[]` too, and it reaches the
|
|
@@ -99,12 +125,18 @@ export interface DateRangeValue {
|
|
|
99
125
|
* protect.
|
|
100
126
|
*
|
|
101
127
|
* 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
|
|
128
|
+
* objectstack-ai/objectstack#4614's design ("the two vocabularies live one import apart and neither
|
|
103
129
|
* restates the other's grammar"): the spec owns which presets exist, this
|
|
104
130
|
* module owns what each one resolves to in date-macro tokens.
|
|
105
131
|
*/
|
|
106
132
|
export { DATE_RANGE_PRESETS };
|
|
107
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Reset the dropped-shorthand warn-once memo. Exported for tests.
|
|
135
|
+
*
|
|
136
|
+
* Kept through the objectui#4356 retirement: the memo it clears outlived the
|
|
137
|
+
* deprecation warning, because the warning that replaced it (a member was
|
|
138
|
+
* DROPPED, not lifted) dedupes on the same key for the same render-path reason.
|
|
139
|
+
*/
|
|
108
140
|
export declare function resetDashboardFilterWarnings(): void;
|
|
109
141
|
/**
|
|
110
142
|
* Normalize a dashboard schema's filter declarations into a flat list of
|
|
@@ -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.
|