@object-ui/core 17.2.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 +1436 -0
- package/README.md +31 -12
- package/dist/actions/ActionEngine.d.ts +53 -31
- package/dist/actions/ActionEngine.js +87 -66
- package/dist/actions/ActionRunner.d.ts +215 -24
- package/dist/actions/ActionRunner.js +334 -17
- package/dist/actions/actionErrorDetail.d.ts +26 -0
- package/dist/actions/actionErrorDetail.js +37 -0
- package/dist/actions/actionKeys.d.ts +96 -20
- package/dist/actions/actionKeys.js +168 -18
- package/dist/actions/actionResponse.d.ts +42 -0
- package/dist/actions/actionResponse.js +104 -0
- package/dist/actions/index.d.ts +3 -0
- package/dist/actions/index.js +3 -0
- package/dist/actions/serverActionHandler.d.ts +143 -0
- package/dist/actions/serverActionHandler.js +137 -0
- 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/ExpressionEvaluator.js +4 -2
- package/dist/evaluator/declaredPredicate.d.ts +119 -0
- package/dist/evaluator/declaredPredicate.js +152 -0
- package/dist/evaluator/fieldRules.d.ts +42 -2
- package/dist/evaluator/fieldRules.js +101 -11
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +18 -3
- package/dist/evaluator/listConditional.js +0 -0
- package/dist/evaluator/optionRules.js +5 -1
- package/dist/evaluator/predicateInput.d.ts +132 -0
- package/dist/evaluator/predicateInput.js +129 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +26 -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/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +8 -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 +79 -15
- package/dist/utils/dataset-format.js +130 -9
- package/dist/utils/dataset-pivot.d.ts +78 -0
- package/dist/utils/dataset-pivot.js +78 -0
- package/dist/utils/dom-props.d.ts +137 -0
- package/dist/utils/dom-props.js +153 -0
- package/dist/utils/filter-converter.d.ts +39 -11
- package/dist/utils/filter-converter.js +87 -7
- 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/predicate-fields.d.ts +96 -0
- package/dist/utils/predicate-fields.js +163 -0
- package/dist/utils/predicate-record.d.ts +32 -0
- package/dist/utils/predicate-record.js +120 -0
- package/dist/utils/record-title.js +0 -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/protocols/NotificationProtocol.d.ts +0 -71
- package/dist/protocols/NotificationProtocol.js +0 -89
- package/dist/utils/merge-views-into-objects.d.ts +0 -19
- package/dist/utils/merge-views-into-objects.js +0 -39
|
@@ -5,12 +5,6 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
9
|
-
* Filter Converter Utilities
|
|
10
|
-
*
|
|
11
|
-
* Shared utilities for converting MongoDB-like filter operators
|
|
12
|
-
* to ObjectStack FilterNode AST format.
|
|
13
|
-
*/
|
|
14
8
|
/**
|
|
15
9
|
* FilterNode AST type definition
|
|
16
10
|
* Represents a filter condition or a logical combination of conditions
|
|
@@ -83,6 +77,36 @@ export declare function convertFiltersToAST(filter: Record<string, any>): Filter
|
|
|
83
77
|
* `table.defaultFilters` (declared `Record<string, any>`) was DROPPED and the
|
|
84
78
|
* view returned every record. Silently: no error, just a wider answer.
|
|
85
79
|
*
|
|
80
|
+
* The FIRST never worked at all (objectui#3431). The array branch returned
|
|
81
|
+
* every array VERBATIM, so a saved view's `ViewFilterRule[]` travelled to
|
|
82
|
+
* `$filter` as bare rule objects — which the server refuses: `isFilterAST` is
|
|
83
|
+
* false for an array of objects, and the wire face answers `400
|
|
84
|
+
* INVALID_FILTER`. Verified against a real backend on the showcase's SHIPPED
|
|
85
|
+
* `showcase_task.in_progress` view (`filter: [{field:'status',
|
|
86
|
+
* operator:'equals', value:'in_progress'}]`): 400 as sent, 200 with its 2 rows
|
|
87
|
+
* once lowered to `[['status','equals','in_progress']]`. Every saved view
|
|
88
|
+
* carrying a filter was a failed list. `mergeFilterNodes` below has warned
|
|
89
|
+
* about exactly this hazard since it was written — the warning was accurate,
|
|
90
|
+
* and the sink it warns for never implemented the lowering it describes.
|
|
91
|
+
*
|
|
92
|
+
* **Why the fold lives HERE and not at the producer.** `ListViewSchema.filter`
|
|
93
|
+
* / `ViewTab.filter` are spec-declared `z.array(ViewFilterRuleSchema)`, and a
|
|
94
|
+
* view hands a renderer a `ListViewSchema` — so folding one hop earlier (in
|
|
95
|
+
* `app-shell`'s ObjectView, say) would write ObjectQL AST triples INTO a
|
|
96
|
+
* spec-declared rule-array slot: an off-spec value in a spec field, AGENTS.md
|
|
97
|
+
* #0.1 inverted. This function is the last hop before the wire, where the value
|
|
98
|
+
* legitimately leaves the spec's view vocabulary and becomes an ObjectQL AST.
|
|
99
|
+
* It is also the single sink both producers already share — `plugin-list`'s
|
|
100
|
+
* `buildEffectiveFilter` (which feeds the grid AND the export) and
|
|
101
|
+
* `plugin-view`'s ObjectView (calendar / kanban / gallery / timeline). One
|
|
102
|
+
* lowering, one place; the same reason the MongoDB-style shape is lowered here
|
|
103
|
+
* rather than at each of its callers.
|
|
104
|
+
*
|
|
105
|
+
* Mixed arrays fold ELEMENT-WISE, because that is what reaches this function in
|
|
106
|
+
* practice: `ObjectView` concatenates a saved view's rules with the
|
|
107
|
+
* `?filter[<field>]=<value>` URL triples into one array. Triples pass through
|
|
108
|
+
* untouched.
|
|
109
|
+
*
|
|
86
110
|
* Returns `undefined` for an absent or empty source, so callers can skip
|
|
87
111
|
* `$filter` rather than sending an empty array.
|
|
88
112
|
*/
|
|
@@ -92,11 +116,15 @@ export declare function toFilterNode(source: unknown): FilterNode | Record<strin
|
|
|
92
116
|
*
|
|
93
117
|
* Wrapping rather than spreading, on purpose. `['and', ...rules]` looks
|
|
94
118
|
* equivalent and is not: spreading a `ViewFilterRule[]` puts bare rule OBJECTS
|
|
95
|
-
* where the AST expects nodes, and the server
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
119
|
+
* where the AST expects nodes, and the server does not accept that —
|
|
120
|
+
* `isFilterAST` says no and the wire face answers `400 INVALID_FILTER`
|
|
121
|
+
* (objectstack#4121). Spreading is only correct when the source happens to be
|
|
122
|
+
* an array of nodes, which is why it survived.
|
|
123
|
+
*
|
|
124
|
+
* Since objectui#3431 the rule objects never reach that position anyway:
|
|
125
|
+
* `toFilterNode` lowers a `ViewFilterRule[]` to AST nodes on the way in. The
|
|
126
|
+
* paragraph above is kept because it is why each source stays its own child —
|
|
127
|
+
* and because it correctly diagnosed the bug the sink itself was carrying.
|
|
100
128
|
*
|
|
101
129
|
* Sources that normalize to nothing are skipped; one surviving source is
|
|
102
130
|
* returned as-is rather than wrapped in a pointless `and`.
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Filter Converter Utilities
|
|
10
|
+
*
|
|
11
|
+
* Shared utilities for converting MongoDB-like filter operators
|
|
12
|
+
* to ObjectStack FilterNode AST format.
|
|
13
|
+
*/
|
|
14
|
+
import { normalizeFilterOperator } from '@objectstack/spec/ui';
|
|
8
15
|
/**
|
|
9
16
|
* Map MongoDB-like operators to ObjectStack filter operators.
|
|
10
17
|
*
|
|
@@ -152,6 +159,37 @@ export function convertFiltersToAST(filter) {
|
|
|
152
159
|
// Multiple conditions: combine with 'and'
|
|
153
160
|
return ['and', ...conditions];
|
|
154
161
|
}
|
|
162
|
+
function isViewFilterRule(value) {
|
|
163
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
164
|
+
return false;
|
|
165
|
+
const field = value.field;
|
|
166
|
+
return typeof field === 'string' && field !== '';
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Lower ONE spec `ViewFilterRule` to an ObjectQL AST comparison node.
|
|
170
|
+
*
|
|
171
|
+
* The operator goes through the spec's OWN {@link normalizeFilterOperator} —
|
|
172
|
+
* the exact exit the WRITE side uses (`app-shell/views/viewFilterFold.ts`), so
|
|
173
|
+
* the two directions cannot drift into two dialects. No second canonical map is
|
|
174
|
+
* introduced, and none is needed: all 19 `VIEW_FILTER_OPERATORS` are already
|
|
175
|
+
* members of the wire's `VALID_AST_OPERATORS`, so the lowering is purely
|
|
176
|
+
* structural. An operator the spec does not know is passed through VERBATIM, so
|
|
177
|
+
* `isFilterAST` still refuses it and the server still answers `400
|
|
178
|
+
* INVALID_FILTER` — a misspelling must not be coerced into a valid one.
|
|
179
|
+
*
|
|
180
|
+
* `value` is emitted only when the rule carries one. A rule that omits it
|
|
181
|
+
* (`is_empty` / `is_null`, whose direction comes from the operator NAME) would
|
|
182
|
+
* otherwise gain an invented `null` the author never wrote — `JSON.stringify`
|
|
183
|
+
* turns a hole in an array into `null`, and `['x', 'equals', null]` is a real
|
|
184
|
+
* `{x: null}` predicate, i.e. a silently-wrong filter. Same rule the write side
|
|
185
|
+
* applies (`if (c.value !== undefined)`).
|
|
186
|
+
*/
|
|
187
|
+
function viewFilterRuleToNode(rule) {
|
|
188
|
+
const operator = normalizeFilterOperator(rule.operator);
|
|
189
|
+
return (rule.value === undefined
|
|
190
|
+
? [rule.field, operator]
|
|
191
|
+
: [rule.field, operator, rule.value]);
|
|
192
|
+
}
|
|
155
193
|
/**
|
|
156
194
|
* Normalize ONE filter source into a single filter node.
|
|
157
195
|
*
|
|
@@ -167,14 +205,52 @@ export function convertFiltersToAST(filter) {
|
|
|
167
205
|
* `table.defaultFilters` (declared `Record<string, any>`) was DROPPED and the
|
|
168
206
|
* view returned every record. Silently: no error, just a wider answer.
|
|
169
207
|
*
|
|
208
|
+
* The FIRST never worked at all (objectui#3431). The array branch returned
|
|
209
|
+
* every array VERBATIM, so a saved view's `ViewFilterRule[]` travelled to
|
|
210
|
+
* `$filter` as bare rule objects — which the server refuses: `isFilterAST` is
|
|
211
|
+
* false for an array of objects, and the wire face answers `400
|
|
212
|
+
* INVALID_FILTER`. Verified against a real backend on the showcase's SHIPPED
|
|
213
|
+
* `showcase_task.in_progress` view (`filter: [{field:'status',
|
|
214
|
+
* operator:'equals', value:'in_progress'}]`): 400 as sent, 200 with its 2 rows
|
|
215
|
+
* once lowered to `[['status','equals','in_progress']]`. Every saved view
|
|
216
|
+
* carrying a filter was a failed list. `mergeFilterNodes` below has warned
|
|
217
|
+
* about exactly this hazard since it was written — the warning was accurate,
|
|
218
|
+
* and the sink it warns for never implemented the lowering it describes.
|
|
219
|
+
*
|
|
220
|
+
* **Why the fold lives HERE and not at the producer.** `ListViewSchema.filter`
|
|
221
|
+
* / `ViewTab.filter` are spec-declared `z.array(ViewFilterRuleSchema)`, and a
|
|
222
|
+
* view hands a renderer a `ListViewSchema` — so folding one hop earlier (in
|
|
223
|
+
* `app-shell`'s ObjectView, say) would write ObjectQL AST triples INTO a
|
|
224
|
+
* spec-declared rule-array slot: an off-spec value in a spec field, AGENTS.md
|
|
225
|
+
* #0.1 inverted. This function is the last hop before the wire, where the value
|
|
226
|
+
* legitimately leaves the spec's view vocabulary and becomes an ObjectQL AST.
|
|
227
|
+
* It is also the single sink both producers already share — `plugin-list`'s
|
|
228
|
+
* `buildEffectiveFilter` (which feeds the grid AND the export) and
|
|
229
|
+
* `plugin-view`'s ObjectView (calendar / kanban / gallery / timeline). One
|
|
230
|
+
* lowering, one place; the same reason the MongoDB-style shape is lowered here
|
|
231
|
+
* rather than at each of its callers.
|
|
232
|
+
*
|
|
233
|
+
* Mixed arrays fold ELEMENT-WISE, because that is what reaches this function in
|
|
234
|
+
* practice: `ObjectView` concatenates a saved view's rules with the
|
|
235
|
+
* `?filter[<field>]=<value>` URL triples into one array. Triples pass through
|
|
236
|
+
* untouched.
|
|
237
|
+
*
|
|
170
238
|
* Returns `undefined` for an absent or empty source, so callers can skip
|
|
171
239
|
* `$filter` rather than sending an empty array.
|
|
172
240
|
*/
|
|
173
241
|
export function toFilterNode(source) {
|
|
174
242
|
if (source === null || source === undefined)
|
|
175
243
|
return undefined;
|
|
176
|
-
if (Array.isArray(source))
|
|
177
|
-
|
|
244
|
+
if (Array.isArray(source)) {
|
|
245
|
+
if (source.length === 0)
|
|
246
|
+
return undefined;
|
|
247
|
+
// Spec `ViewFilterRule[]` (possibly mixed with AST nodes) → AST nodes. Left
|
|
248
|
+
// untouched when the array holds no rule objects, which is the common case:
|
|
249
|
+
// user-filter conditions and URL triples are already nodes.
|
|
250
|
+
return (source.some(isViewFilterRule)
|
|
251
|
+
? source.map((el) => (isViewFilterRule(el) ? viewFilterRuleToNode(el) : el))
|
|
252
|
+
: source);
|
|
253
|
+
}
|
|
178
254
|
if (typeof source !== 'object')
|
|
179
255
|
return undefined;
|
|
180
256
|
const obj = source;
|
|
@@ -188,11 +264,15 @@ export function toFilterNode(source) {
|
|
|
188
264
|
*
|
|
189
265
|
* Wrapping rather than spreading, on purpose. `['and', ...rules]` looks
|
|
190
266
|
* equivalent and is not: spreading a `ViewFilterRule[]` puts bare rule OBJECTS
|
|
191
|
-
* where the AST expects nodes, and the server
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
267
|
+
* where the AST expects nodes, and the server does not accept that —
|
|
268
|
+
* `isFilterAST` says no and the wire face answers `400 INVALID_FILTER`
|
|
269
|
+
* (objectstack#4121). Spreading is only correct when the source happens to be
|
|
270
|
+
* an array of nodes, which is why it survived.
|
|
271
|
+
*
|
|
272
|
+
* Since objectui#3431 the rule objects never reach that position anyway:
|
|
273
|
+
* `toFilterNode` lowers a `ViewFilterRule[]` to AST nodes on the way in. The
|
|
274
|
+
* paragraph above is kept because it is why each source stays its own child —
|
|
275
|
+
* and because it correctly diagnosed the bug the sink itself was carrying.
|
|
196
276
|
*
|
|
197
277
|
* Sources that normalize to nothing are skipped; one surviving source is
|
|
198
278
|
* returned as-is rather than wrapped in a pointless `and`.
|
|
@@ -35,13 +35,33 @@ export declare const DENSITY_MODE_TO_ROW_HEIGHT: Record<DensityMode, RowHeight>;
|
|
|
35
35
|
*/
|
|
36
36
|
export declare const ROW_HEIGHT_TO_DENSITY_MODE: Record<RowHeight, DensityMode>;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
38
|
+
* Runtime reader for {@link ROW_HEIGHT_TO_DENSITY_MODE}: the spec's five row
|
|
39
|
+
* heights narrowed onto the renderer's three densities, and **nothing else**.
|
|
40
|
+
*
|
|
41
|
+
* The parameter stays `unknown` because this is the boundary user-authored view
|
|
42
|
+
* metadata actually crosses — `ListViewSchema.rowHeight` is statically a
|
|
43
|
+
* `RowHeight`, but the value arrives from stored view definitions TypeScript
|
|
44
|
+
* never saw (`ObjectView`: `viewDef.rowHeight ?? listSchema.rowHeight`). The
|
|
45
|
+
* type-level half of the guarantee is the `Record<RowHeight, …>` on the table
|
|
46
|
+
* above, which fails the build when the spec grows a row height.
|
|
47
|
+
*
|
|
48
|
+
* An off-spec value gets NO density (objectui#4440). It used to be coerced to
|
|
49
|
+
* `comfortable`, which is the opposite of what `@object-ui/react`'s spec bridge
|
|
50
|
+
* answers for the same string after objectui#4352 — one metadata-driven system
|
|
51
|
+
* holding two answers for one input. AGENTS.md #0.1 decides which one survives:
|
|
52
|
+
* a renderer-side rehabilitation of off-spec metadata is a second de-facto
|
|
53
|
+
* contract, and one strict contract beats N. The producer is where a bad
|
|
54
|
+
* `rowHeight` gets fixed.
|
|
55
|
+
*
|
|
56
|
+
* Callers apply their own "nothing was said" default to `undefined`, so an
|
|
57
|
+
* off-spec row height now renders exactly like an absent one — `'compact'` in
|
|
58
|
+
* both `ListView` and `ObjectGrid`.
|
|
59
|
+
*
|
|
60
|
+
* `hasOwnProperty`, not `in`: `in` walks the prototype chain, so `'toString'`
|
|
61
|
+
* used to come back as `Object.prototype.toString` — a FUNCTION returned from
|
|
62
|
+
* something typed `DensityMode`.
|
|
43
63
|
*/
|
|
44
|
-
export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode;
|
|
64
|
+
export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode | undefined;
|
|
45
65
|
/**
|
|
46
66
|
* ObjectUI's `list-view` node historically used a different vocabulary from
|
|
47
67
|
* `@objectstack/spec` for the same concepts (`fields` where the spec says
|
|
@@ -32,17 +32,39 @@ export const ROW_HEIGHT_TO_DENSITY_MODE = {
|
|
|
32
32
|
extra_tall: 'spacious',
|
|
33
33
|
};
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* Runtime reader for {@link ROW_HEIGHT_TO_DENSITY_MODE}: the spec's five row
|
|
36
|
+
* heights narrowed onto the renderer's three densities, and **nothing else**.
|
|
37
|
+
*
|
|
38
|
+
* The parameter stays `unknown` because this is the boundary user-authored view
|
|
39
|
+
* metadata actually crosses — `ListViewSchema.rowHeight` is statically a
|
|
40
|
+
* `RowHeight`, but the value arrives from stored view definitions TypeScript
|
|
41
|
+
* never saw (`ObjectView`: `viewDef.rowHeight ?? listSchema.rowHeight`). The
|
|
42
|
+
* type-level half of the guarantee is the `Record<RowHeight, …>` on the table
|
|
43
|
+
* above, which fails the build when the spec grows a row height.
|
|
44
|
+
*
|
|
45
|
+
* An off-spec value gets NO density (objectui#4440). It used to be coerced to
|
|
46
|
+
* `comfortable`, which is the opposite of what `@object-ui/react`'s spec bridge
|
|
47
|
+
* answers for the same string after objectui#4352 — one metadata-driven system
|
|
48
|
+
* holding two answers for one input. AGENTS.md #0.1 decides which one survives:
|
|
49
|
+
* a renderer-side rehabilitation of off-spec metadata is a second de-facto
|
|
50
|
+
* contract, and one strict contract beats N. The producer is where a bad
|
|
51
|
+
* `rowHeight` gets fixed.
|
|
52
|
+
*
|
|
53
|
+
* Callers apply their own "nothing was said" default to `undefined`, so an
|
|
54
|
+
* off-spec row height now renders exactly like an absent one — `'compact'` in
|
|
55
|
+
* both `ListView` and `ObjectGrid`.
|
|
56
|
+
*
|
|
57
|
+
* `hasOwnProperty`, not `in`: `in` walks the prototype chain, so `'toString'`
|
|
58
|
+
* used to come back as `Object.prototype.toString` — a FUNCTION returned from
|
|
59
|
+
* something typed `DensityMode`.
|
|
40
60
|
*/
|
|
41
61
|
export function rowHeightToDensityMode(rowHeight) {
|
|
42
|
-
if (typeof rowHeight
|
|
43
|
-
return
|
|
62
|
+
if (typeof rowHeight !== 'string')
|
|
63
|
+
return undefined;
|
|
64
|
+
if (!Object.prototype.hasOwnProperty.call(ROW_HEIGHT_TO_DENSITY_MODE, rowHeight)) {
|
|
65
|
+
return undefined;
|
|
44
66
|
}
|
|
45
|
-
return
|
|
67
|
+
return ROW_HEIGHT_TO_DENSITY_MODE[rowHeight];
|
|
46
68
|
}
|
|
47
69
|
const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
48
70
|
/**
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
* `formatDisplayNumber` — the ONE number-display formatter behind every field
|
|
10
|
+
* cell, field widget, metric renderer and dataset measure in the console.
|
|
11
|
+
*
|
|
12
|
+
* It exists because the same `new Intl.NumberFormat('en-US', …)` construction
|
|
13
|
+
* had been copied into the number cell renderer, the currency cell renderer,
|
|
14
|
+
* the currency widget, the compact `formatNumber` helper and the dashboard
|
|
15
|
+
* metric widget. Two defects therefore had five homes each, and fixing "the"
|
|
16
|
+
* renderer never changed the answer (objectui#4033, source thread
|
|
17
|
+
* objectstack#5067):
|
|
18
|
+
*
|
|
19
|
+
* 1. the locale was hardcoded to `en-US`, so a `zh-CN` / `de-DE` console still
|
|
20
|
+
* grouped and pointed decimals the US way; and
|
|
21
|
+
* 2. `useGrouping` was never set, so a four-digit YEAR stored as
|
|
22
|
+
* `Field.number({ scale: 0 })` rendered as `2,026` — in every locale, with
|
|
23
|
+
* no field property able to turn it off.
|
|
24
|
+
*
|
|
25
|
+
* Both policies now live here and nowhere else. Call sites bring the value and
|
|
26
|
+
* the display width; they do not bring a locale default and they do not decide
|
|
27
|
+
* grouping.
|
|
28
|
+
*
|
|
29
|
+
* ── Why this module lives in `@object-ui/core` (objectui#4576) ──
|
|
30
|
+
* It used to live in `@object-ui/i18n`, and that home cost the repo a real
|
|
31
|
+
* defect. `@object-ui/core`'s `utils/dataset-format.ts` could not import it —
|
|
32
|
+
* `core` is the React-free engine (AGENTS.md §3: "No UI-lib deps. Logic only.")
|
|
33
|
+
* and is a runtime dependency of React-FREE consumers (the `object-ui` VS Code
|
|
34
|
+
* extension, `@object-ui/data-objectstack`), while `i18n` depends on
|
|
35
|
+
* `i18next`/`react-i18next` and peer-depends on React. So `formatMeasure` kept
|
|
36
|
+
* a parallel `Intl` implementation, the two drifted, and a German session read
|
|
37
|
+
* `1.234,5 %` from a list cell beside `1.234,5%` from a dashboard measure
|
|
38
|
+
* (objectui#4576). #4577 measured the option mapping LOSSLESS across 32,760
|
|
39
|
+
* combinations and recorded the duplication at both ends rather than routing
|
|
40
|
+
* through a boundary it could not cross.
|
|
41
|
+
*
|
|
42
|
+
* The function is pure — no React, no i18next, no DOM — so the boundary was
|
|
43
|
+
* never a property of the CODE, only of where the code sat. Moving it DOWN to
|
|
44
|
+
* the lower layer removes the obstacle instead of working around it:
|
|
45
|
+
* `@object-ui/i18n` now RE-EXPORTS these three names, so every existing import
|
|
46
|
+
* path keeps compiling and `import { formatDisplayNumber } from '@object-ui/i18n'`
|
|
47
|
+
* and `from '@object-ui/core'` are the SAME symbol (pinned in
|
|
48
|
+
* `packages/i18n/src/__tests__/number-display.reexport-identity.test.ts`).
|
|
49
|
+
* `core` imports `i18n` nowhere, so the new edge introduces no cycle.
|
|
50
|
+
*/
|
|
51
|
+
export interface DisplayNumberFormatOptions {
|
|
52
|
+
/**
|
|
53
|
+
* BCP-47 tag for the ACTIVE display locale — in React, whatever
|
|
54
|
+
* `useDisplayLocale()` returns.
|
|
55
|
+
*
|
|
56
|
+
* `undefined` means "follow the runtime default", which is the honest answer
|
|
57
|
+
* for a non-React caller that has no locale in hand. It never means `en-US`:
|
|
58
|
+
* assuming US conventions for the whole world is the defect this module was
|
|
59
|
+
* created to remove.
|
|
60
|
+
*/
|
|
61
|
+
locale?: string;
|
|
62
|
+
/**
|
|
63
|
+
* ISO 4217 code. When present the value is formatted as money — which also
|
|
64
|
+
* means grouping is kept, because a grouped amount is what every currency
|
|
65
|
+
* convention expects and an ordinal amount of money is not a thing.
|
|
66
|
+
*
|
|
67
|
+
* Money wins over {@link DisplayNumberFormatOptions.style}: a call that sets
|
|
68
|
+
* both gets a currency rendering. Nothing in the repo does that, and the
|
|
69
|
+
* combination has no meaning to express.
|
|
70
|
+
*/
|
|
71
|
+
currency?: string;
|
|
72
|
+
/**
|
|
73
|
+
* The FIELD's declared `scale` — the `s` of a `decimal(p, s)` column — and
|
|
74
|
+
* nothing else. This is a POLICY input, not a display width: pass it only
|
|
75
|
+
* when a field declaration actually said so.
|
|
76
|
+
*
|
|
77
|
+
* `scale: 0` with no currency declares a discrete integer (a year, a fiscal
|
|
78
|
+
* period, an ordinal), and those are not grouped. Leave `scale` undefined and
|
|
79
|
+
* grouping is kept — which is correct for the two cases that look similar but
|
|
80
|
+
* are not:
|
|
81
|
+
*
|
|
82
|
+
* - an UNDECLARED scale (`scale` is optional in the spec, so absent means
|
|
83
|
+
* "decimals unknown", not "integer"); and
|
|
84
|
+
* - a caller whose zero-decimal display comes from something other than a
|
|
85
|
+
* field declaration — e.g. the dashboard `MetricWidget`, whose decimals
|
|
86
|
+
* come from a numeral.js format pattern and whose large KPI aggregates
|
|
87
|
+
* are *documented* to want separators ("`1,930,000` not `1930000`").
|
|
88
|
+
*
|
|
89
|
+
* ⚠️ INTERIM DEFAULT (objectui#4033, PM ruling 2026-08-11). Suppressing
|
|
90
|
+
* 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. It is the
|
|
92
|
+
* better trade only until the spec gains an authorable presentation hint
|
|
93
|
+
* (`useGrouping` / `displayFormat` — being specified separately, contract-first,
|
|
94
|
+
* in the objectstack repo). When that hint lands it OVERRIDES this default,
|
|
95
|
+
* and this heuristic should be reduced to the fallback for fields that
|
|
96
|
+
* declare nothing.
|
|
97
|
+
*/
|
|
98
|
+
scale?: number;
|
|
99
|
+
/**
|
|
100
|
+
* How the number relates to a percentage — the ONE place that distinction is
|
|
101
|
+
* expressed, because getting it wrong is the whole of objectui#4576.
|
|
102
|
+
*
|
|
103
|
+
* - `'decimal'` (default) — a plain number, no percent sign.
|
|
104
|
+
* - `'percent'` — the value is a FRACTION. `Intl` multiplies it by 100 and
|
|
105
|
+
* appends the locale's percent convention, so `0.8` renders as `80%`.
|
|
106
|
+
* - `'percentPoints'` — the value is ALREADY in percentage points, so `80`
|
|
107
|
+
* renders as `80%`. Same locale convention, NO re-scaling.
|
|
108
|
+
*
|
|
109
|
+
* The third one exists because "divide by 100 so `Intl` can multiply it back"
|
|
110
|
+
* is not free, and the round trip is lossy at rounding TIES: measured on this
|
|
111
|
+
* repo's runner, `80.175` percentage points formatted to 2 decimals renders
|
|
112
|
+
* `80.18%` when formatted directly and `80.17%` after the divide-and-remultiply
|
|
113
|
+
* — 27,581 of 1,200,013 ordinary-magnitude en-US forms move, plus every
|
|
114
|
+
* value at the top of the double range (`MAX_SAFE_INTEGER`, `1e23`). A caller
|
|
115
|
+
* holding percentage points must therefore NOT reach for `'percent'`; that is
|
|
116
|
+
* exactly the trap this option removes.
|
|
117
|
+
*
|
|
118
|
+
* `'percentPoints'` is implemented with `Intl`'s `style: 'unit'` /
|
|
119
|
+
* `unit: 'percent'`, which was measured to produce a BYTE-IDENTICAL percent
|
|
120
|
+
* affix to `style: 'percent'` across all 171 locale tags tested — including
|
|
121
|
+
* the German/French/Russian no-break space before the sign, Turkish's PREFIX
|
|
122
|
+
* position (`%1.234,5`), Arabic's own percent sign plus U+061C, and the
|
|
123
|
+
* Bengali/Marathi percent patterns that group Western-style where their
|
|
124
|
+
* decimal patterns group by lakh. It is the same CONVENTION by a route that
|
|
125
|
+
* does not touch the value.
|
|
126
|
+
*/
|
|
127
|
+
style?: 'decimal' | 'percent' | 'percentPoints';
|
|
128
|
+
minimumFractionDigits?: number;
|
|
129
|
+
maximumFractionDigits?: number;
|
|
130
|
+
notation?: 'standard' | 'compact';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The grouping policy, alone and testable: does this number get thousands
|
|
134
|
+
* separators?
|
|
135
|
+
*
|
|
136
|
+
* @param scale the field's declared `scale`, or `undefined` when the caller
|
|
137
|
+
* has no field declaration behind it
|
|
138
|
+
* @param currency ISO 4217 code when the number is money
|
|
139
|
+
*/
|
|
140
|
+
export declare function shouldGroupDisplayNumber(scale?: number, currency?: string): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Format a number for DISPLAY, in the active locale, under the grouping policy
|
|
143
|
+
* above.
|
|
144
|
+
*
|
|
145
|
+
* Throws for a bad `currency` code exactly as `Intl.NumberFormat` does, so the
|
|
146
|
+
* fallbacks call sites already had (`${currency} ${value.toFixed(n)}`) keep
|
|
147
|
+
* working unchanged. A bad LOCALE is handled here instead of throwing: `locale`
|
|
148
|
+
* arrives from a server response (ADR-0053 `localization.locale`), and a
|
|
149
|
+
* malformed tag from a tenant config must never take a grid cell down.
|
|
150
|
+
*/
|
|
151
|
+
export declare function formatDisplayNumber(value: number, options?: DisplayNumberFormatOptions): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
* The grouping policy, alone and testable: does this number get thousands
|
|
10
|
+
* separators?
|
|
11
|
+
*
|
|
12
|
+
* @param scale the field's declared `scale`, or `undefined` when the caller
|
|
13
|
+
* has no field declaration behind it
|
|
14
|
+
* @param currency ISO 4217 code when the number is money
|
|
15
|
+
*/
|
|
16
|
+
export function shouldGroupDisplayNumber(scale, currency) {
|
|
17
|
+
// Money always groups — including money whose currency code could not be
|
|
18
|
+
// resolved, which still renders as an amount (just without a symbol).
|
|
19
|
+
if (currency)
|
|
20
|
+
return true;
|
|
21
|
+
// Only an explicitly declared scale of 0 is an ordinal. `undefined !== 0`.
|
|
22
|
+
return scale !== 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Format a number for DISPLAY, in the active locale, under the grouping policy
|
|
26
|
+
* above.
|
|
27
|
+
*
|
|
28
|
+
* Throws for a bad `currency` code exactly as `Intl.NumberFormat` does, so the
|
|
29
|
+
* fallbacks call sites already had (`${currency} ${value.toFixed(n)}`) keep
|
|
30
|
+
* working unchanged. A bad LOCALE is handled here instead of throwing: `locale`
|
|
31
|
+
* arrives from a server response (ADR-0053 `localization.locale`), and a
|
|
32
|
+
* malformed tag from a tenant config must never take a grid cell down.
|
|
33
|
+
*/
|
|
34
|
+
export function formatDisplayNumber(value, options = {}) {
|
|
35
|
+
const { locale, currency, scale, style, ...passthrough } = options;
|
|
36
|
+
const intlOptions = { ...passthrough };
|
|
37
|
+
if (style === 'percentPoints') {
|
|
38
|
+
// The locale's percent convention WITHOUT `style: 'percent'`'s ×100 — see
|
|
39
|
+
// the option's doc above for the measurement that separates the two.
|
|
40
|
+
intlOptions.style = 'unit';
|
|
41
|
+
intlOptions.unit = 'percent';
|
|
42
|
+
intlOptions.unitDisplay = 'narrow';
|
|
43
|
+
}
|
|
44
|
+
else if (style) {
|
|
45
|
+
intlOptions.style = style;
|
|
46
|
+
}
|
|
47
|
+
if (currency) {
|
|
48
|
+
intlOptions.style = 'currency';
|
|
49
|
+
intlOptions.currency = currency;
|
|
50
|
+
delete intlOptions.unit;
|
|
51
|
+
delete intlOptions.unitDisplay;
|
|
52
|
+
}
|
|
53
|
+
// ⚠️ Set `useGrouping` ONLY to suppress. `useGrouping: true` is NOT the same
|
|
54
|
+
// as omitting the key: `true` means "always", while omitting it means "auto"
|
|
55
|
+
// (and "min2" under compact notation), which is the locale's own preference.
|
|
56
|
+
// Measured — for 1234: es-ES "auto" → `1234` but "always" → `1.234`; pl-PL
|
|
57
|
+
// "auto" → `1234` but "always" → `1 234`. Writing `true` here would silently
|
|
58
|
+
// override those locales' conventions in the name of preserving en-US output.
|
|
59
|
+
if (!shouldGroupDisplayNumber(scale, currency)) {
|
|
60
|
+
intlOptions.useGrouping = false;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return new Intl.NumberFormat(locale, intlOptions).format(value);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Retry WITHOUT the locale, keeping every other option: this rescues a
|
|
67
|
+
// malformed tag while still surfacing a genuinely bad `currency` to the
|
|
68
|
+
// caller's own catch.
|
|
69
|
+
return new Intl.NumberFormat(undefined, intlOptions).format(value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — predicate field harvesting
|
|
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
|
+
* Which record fields a view's PREDICATES read — the half of a view's field
|
|
10
|
+
* appetite that is not its columns.
|
|
11
|
+
*
|
|
12
|
+
* A list view projects what it DISPLAYS: `$select` is built from the column
|
|
13
|
+
* list (plus `id` and the expanded relation roots). That is correct for
|
|
14
|
+
* rendering and wrong for gating, because a `visible` / `disabled` /
|
|
15
|
+
* conditional-formatting predicate reads fields nobody put on screen — gating a
|
|
16
|
+
* row action on `record.owner` while showing Title / Status is the ordinary
|
|
17
|
+
* case, not an exotic one.
|
|
18
|
+
*
|
|
19
|
+
* The field then simply is not in the payload, and CEL does not treat an absent
|
|
20
|
+
* key as null: `record.owner == os.user.id` FAULTS with `No such key: owner`.
|
|
21
|
+
* Fault handling differs per surface and both answers are wrong — fail-CLOSED
|
|
22
|
+
* on the row kebab and the selection bar (the button silently disappears for
|
|
23
|
+
* everyone), fail-OPEN on the lenient paths (it appears for everyone). Nothing
|
|
24
|
+
* in either outcome points at a projection.
|
|
25
|
+
*
|
|
26
|
+
* Harvesting the names a predicate mentions and adding them to `$select` is
|
|
27
|
+
* what closes that. Note what it deliberately does NOT add: `$expand`. A
|
|
28
|
+
* predicate wants the stored foreign key, which is exactly what an unexpanded
|
|
29
|
+
* relation column already is — so a predicate-only relation field costs one
|
|
30
|
+
* more column and no extra round trip. (A relation that IS displayed still gets
|
|
31
|
+
* expanded for its label, and `toPredicateRecord` collapses it back for the
|
|
32
|
+
* predicate.)
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The columns the framework provisions on EVERY business object
|
|
36
|
+
* (`applySystemFields`) but does not publish in the object's `fields` metadata.
|
|
37
|
+
*
|
|
38
|
+
* They matter here because the most ordinary ownership predicate there is —
|
|
39
|
+
* `record.owner_id == os.user.id` — names one of them. Checking a harvested
|
|
40
|
+
* name against the declared fields alone would drop it, leaving the predicate
|
|
41
|
+
* with the same missing key the harvest exists to prevent.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately the columns OBSERVED on a live payload rather than
|
|
44
|
+
* `SYSTEM_MANAGED_FIELD_NAMES` (`@object-ui/types`), which is the broader
|
|
45
|
+
* DISPLAY-oriented set and carries legacy aliases (`_id`, `createdAt`,
|
|
46
|
+
* `modified`, `space`) that are not columns anywhere. An alias in `$select` is
|
|
47
|
+
* exactly the unknown key the guard exists to keep out.
|
|
48
|
+
*/
|
|
49
|
+
export declare const PLATFORM_RECORD_COLUMNS: ReadonlySet<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Whether a harvested name is safe to put in a `$select` projection: either the
|
|
52
|
+
* object declares it, or it is one of the platform columns every object has.
|
|
53
|
+
*
|
|
54
|
+
* The check is not pedantry — an unknown key is not ignored by every backend
|
|
55
|
+
* (the cloud multi-tenant runtime answers with an EMPTY result set), so without
|
|
56
|
+
* it one typo'd predicate could blank a whole list.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isProjectableField(name: string, declaredFields: Record<string, unknown> | null | undefined): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Collect the record field names referenced by a set of predicates.
|
|
61
|
+
*
|
|
62
|
+
* Accepts predicates in every authored shape (bare CEL string, the
|
|
63
|
+
* `{ dialect, source }` envelope, `null`/`undefined`/boolean — the last three
|
|
64
|
+
* contribute nothing). Returns names in first-seen order, deduplicated.
|
|
65
|
+
*
|
|
66
|
+
* The result is a set of CANDIDATES, not verified fields: a typo'd predicate
|
|
67
|
+
* yields a name this object does not have. Callers must intersect it with the
|
|
68
|
+
* object's declared fields before putting it in a query — an unknown key in
|
|
69
|
+
* `$select` is not ignored by every backend (the cloud runtime answers with an
|
|
70
|
+
* empty result set), which would turn a typo into a blank list.
|
|
71
|
+
*/
|
|
72
|
+
export declare function collectPredicateFieldRefs(predicates: readonly unknown[]): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Every predicate a list-ish view carries, flattened for
|
|
75
|
+
* {@link collectPredicateFieldRefs}.
|
|
76
|
+
*
|
|
77
|
+
* One reader so the projection cannot fall behind the surfaces: each entry here
|
|
78
|
+
* is a place a predicate is actually evaluated against a row — conditional
|
|
79
|
+
* formatting, the row kebab (custom defs AND the built-in Edit/Delete
|
|
80
|
+
* overrides), the selection bar, and the object's declared actions.
|
|
81
|
+
*
|
|
82
|
+
* `objectActions` is deliberately the object's WHOLE action set rather than
|
|
83
|
+
* only the ones this view promotes. Narrowing it would mean re-running the
|
|
84
|
+
* name resolution `resolveLegacyRowActions` / `resolveBulkActions` already do,
|
|
85
|
+
* at a point in the fetch where the view's `rowActions` / `bulkActions` lists
|
|
86
|
+
* have not been folded yet — and the two directions of error are not
|
|
87
|
+
* symmetric: an extra column costs bytes, a missing one silently breaks the
|
|
88
|
+
* predicate that needed it.
|
|
89
|
+
*/
|
|
90
|
+
export declare function listViewPredicates(view: {
|
|
91
|
+
conditionalFormatting?: readonly unknown[] | null;
|
|
92
|
+
rowActionDefs?: readonly unknown[] | null;
|
|
93
|
+
bulkActionDefs?: readonly unknown[] | null;
|
|
94
|
+
objectActions?: readonly unknown[] | null;
|
|
95
|
+
userActions?: Record<string, unknown> | null;
|
|
96
|
+
}): unknown[];
|