@object-ui/core 17.3.0 → 17.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1224 -0
- package/README.md +2 -12
- package/dist/actions/ActionEngine.d.ts +42 -25
- package/dist/actions/ActionEngine.js +66 -52
- package/dist/actions/ActionRunner.d.ts +199 -21
- package/dist/actions/ActionRunner.js +332 -16
- package/dist/actions/actionKeys.d.ts +96 -20
- package/dist/actions/actionKeys.js +157 -18
- package/dist/data-scope/ViewDataProvider.d.ts +37 -12
- package/dist/data-scope/ViewDataProvider.js +83 -5
- package/dist/data-scope/element-data-source.d.ts +113 -0
- package/dist/data-scope/element-data-source.js +216 -0
- package/dist/data-scope/index.d.ts +2 -1
- package/dist/data-scope/index.js +1 -0
- package/dist/evaluator/declaredPredicate.d.ts +119 -0
- package/dist/evaluator/declaredPredicate.js +152 -0
- package/dist/evaluator/fieldRules.d.ts +16 -0
- package/dist/evaluator/fieldRules.js +51 -16
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +5 -2
- package/dist/evaluator/listConditional.js +58 -16
- package/dist/evaluator/predicateInput.d.ts +51 -3
- package/dist/evaluator/predicateInput.js +62 -5
- package/dist/index.d.ts +4 -1
- package/dist/index.js +14 -1
- package/dist/protocols/DndProtocol.d.ts +42 -1
- package/dist/protocols/KeyboardProtocol.d.ts +31 -1
- package/dist/protocols/SharingProtocol.d.ts +11 -1
- package/dist/registry/Registry.d.ts +24 -0
- package/dist/theme/ThemeEngine.d.ts +0 -8
- package/dist/theme/ThemeEngine.js +22 -103
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +7 -1
- package/dist/types/index.d.ts +42 -8
- package/dist/utils/chart-series.d.ts +258 -5
- package/dist/utils/chart-series.js +423 -20
- package/dist/utils/compare-to.d.ts +40 -13
- package/dist/utils/compare-to.js +4 -32
- package/dist/utils/dashboard-filters.d.ts +54 -8
- package/dist/utils/dashboard-filters.js +261 -12
- package/dist/utils/dataset-format.d.ts +73 -14
- package/dist/utils/dataset-format.js +130 -9
- package/dist/utils/dataset-pivot.d.ts +28 -7
- package/dist/utils/dataset-pivot.js +27 -6
- package/dist/utils/dom-props.d.ts +137 -0
- package/dist/utils/dom-props.js +153 -0
- package/dist/utils/normalize-list-view.d.ts +26 -6
- package/dist/utils/normalize-list-view.js +30 -8
- package/dist/utils/number-display.d.ts +151 -0
- package/dist/utils/number-display.js +71 -0
- package/dist/utils/resolve-view-id.d.ts +31 -0
- package/dist/utils/resolve-view-id.js +45 -0
- package/dist/utils/sort-query.d.ts +55 -0
- package/dist/utils/sort-query.js +35 -0
- package/package.json +5 -5
- package/dist/utils/merge-views-into-objects.d.ts +0 -19
- package/dist/utils/merge-views-into-objects.js +0 -39
|
@@ -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,31 @@
|
|
|
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
|
+
* Resolve a REQUESTED view name against an object's actual view ids.
|
|
10
|
+
*
|
|
11
|
+
* Canonical view ids are fully qualified (`<object>.<viewKind>`, see app-shell's
|
|
12
|
+
* MetadataProvider), but the surfaces that NAME a view emit the short form —
|
|
13
|
+
* nav items write their `viewName` verbatim, `ElementDataSource.view` on a page
|
|
14
|
+
* component is authored by hand, and legacy embedded `listViews` carry bare keys
|
|
15
|
+
* (including the `'all'` fallback view). Accept both directions (#2217):
|
|
16
|
+
*
|
|
17
|
+
* 1. exact id match;
|
|
18
|
+
* 2. short name (no `.`) → retry as `<objectName>.<name>`;
|
|
19
|
+
* 3. qualified name → retry with the `<objectName>.` prefix stripped.
|
|
20
|
+
*
|
|
21
|
+
* Returns the matching view id, or `undefined` when nothing matches — the
|
|
22
|
+
* caller decides how to fall back (and should report it rather than swallow it).
|
|
23
|
+
*
|
|
24
|
+
* Lives in `@object-ui/core` because it has two consumers in different layers:
|
|
25
|
+
* app-shell's `ObjectView` (URL-requested view) and the page-component data
|
|
26
|
+
* binding that resolves `dataSource.view` (objectstack#5576). A second copy at
|
|
27
|
+
* the second call site is the drift AGENTS.md #0.1 forbids — one matcher, one
|
|
28
|
+
* place, so a view name that resolves in the object page resolves identically
|
|
29
|
+
* when a page component names it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveViewId(requested: string | undefined | null, viewIds: readonly string[], objectName: string): string | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
* Resolve a REQUESTED view name against an object's actual view ids.
|
|
10
|
+
*
|
|
11
|
+
* Canonical view ids are fully qualified (`<object>.<viewKind>`, see app-shell's
|
|
12
|
+
* MetadataProvider), but the surfaces that NAME a view emit the short form —
|
|
13
|
+
* nav items write their `viewName` verbatim, `ElementDataSource.view` on a page
|
|
14
|
+
* component is authored by hand, and legacy embedded `listViews` carry bare keys
|
|
15
|
+
* (including the `'all'` fallback view). Accept both directions (#2217):
|
|
16
|
+
*
|
|
17
|
+
* 1. exact id match;
|
|
18
|
+
* 2. short name (no `.`) → retry as `<objectName>.<name>`;
|
|
19
|
+
* 3. qualified name → retry with the `<objectName>.` prefix stripped.
|
|
20
|
+
*
|
|
21
|
+
* Returns the matching view id, or `undefined` when nothing matches — the
|
|
22
|
+
* caller decides how to fall back (and should report it rather than swallow it).
|
|
23
|
+
*
|
|
24
|
+
* Lives in `@object-ui/core` because it has two consumers in different layers:
|
|
25
|
+
* app-shell's `ObjectView` (URL-requested view) and the page-component data
|
|
26
|
+
* binding that resolves `dataSource.view` (objectstack#5576). A second copy at
|
|
27
|
+
* the second call site is the drift AGENTS.md #0.1 forbids — one matcher, one
|
|
28
|
+
* place, so a view name that resolves in the object page resolves identically
|
|
29
|
+
* when a page component names it.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveViewId(requested, viewIds, objectName) {
|
|
32
|
+
if (!requested)
|
|
33
|
+
return undefined;
|
|
34
|
+
const has = (id) => viewIds.includes(id);
|
|
35
|
+
if (has(requested))
|
|
36
|
+
return requested;
|
|
37
|
+
const prefix = `${objectName}.`;
|
|
38
|
+
if (!requested.includes('.') && has(prefix + requested)) {
|
|
39
|
+
return prefix + requested;
|
|
40
|
+
}
|
|
41
|
+
if (requested.startsWith(prefix) && has(requested.slice(prefix.length))) {
|
|
42
|
+
return requested.slice(prefix.length);
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
* sort-query — lower a block's authored `sort` to the `$orderby` value a
|
|
10
|
+
* `DataSource.find` query carries.
|
|
11
|
+
*
|
|
12
|
+
* Two authored spellings reach every object-bound block, because both are
|
|
13
|
+
* declared: the legacy OData-ish string (`ObjectGridSchema.sort: "name desc"`)
|
|
14
|
+
* and the spec's `SortConfig[]` (`[{ field, order }]`). A read site that hands
|
|
15
|
+
* either straight to `$orderby` works only because `QueryParams.$orderby`
|
|
16
|
+
* accepts four shapes; normalizing here means one shape reaches the wire and a
|
|
17
|
+
* block does not have to know which spelling its author used.
|
|
18
|
+
*
|
|
19
|
+
* This is the same conversion three sibling blocks already inline privately
|
|
20
|
+
* (`ObjectGantt` / `ObjectMap` / `ObjectCalendar`, byte-identical copies). It is
|
|
21
|
+
* hoisted here because objectstack#7137 added two more read sites
|
|
22
|
+
* (`object-timeline`, `record:line_items`), and a fifth and sixth private copy is
|
|
23
|
+
* how the conversions start disagreeing. Collapsing the three existing copies
|
|
24
|
+
* onto this one is deliberately NOT part of #7137 — it is tracked as
|
|
25
|
+
* objectstack#7148.
|
|
26
|
+
*
|
|
27
|
+
* Two deliberate differences from those private copies, both of which make this
|
|
28
|
+
* function more faithful to the declared contract rather than adding tolerance:
|
|
29
|
+
*
|
|
30
|
+
* - **`order` is optional in `SortConfig`**, so an entry that omits it means
|
|
31
|
+
* ascending (that is what `$orderby`'s own
|
|
32
|
+
* `Array<{ field: string; order?: 'asc' | 'desc' }>` shape says). The private
|
|
33
|
+
* copies require BOTH keys and silently drop such an entry, which loses an
|
|
34
|
+
* authored sort key instead of ordering by it.
|
|
35
|
+
* - **Nothing usable yields `undefined`, never `{}`.** An empty object is a
|
|
36
|
+
* truthy value that means "no ordering" only by accident of the adapter's
|
|
37
|
+
* serializer; `undefined` says it, so the query simply carries no `$orderby`.
|
|
38
|
+
*
|
|
39
|
+
* Not a lenient alias: an unusable input (a number, an object, an array of
|
|
40
|
+
* strings) yields `undefined` rather than a guess. Only the two spellings the
|
|
41
|
+
* schema types declare are honoured.
|
|
42
|
+
*/
|
|
43
|
+
/** A field name paired with a direction — the spec's `SortConfig`. */
|
|
44
|
+
export interface QuerySortEntry {
|
|
45
|
+
field?: string;
|
|
46
|
+
order?: 'asc' | 'desc';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Normalize an authored `sort` into the field→direction map used for
|
|
50
|
+
* `QueryParams.$orderby`.
|
|
51
|
+
*
|
|
52
|
+
* @param sort `"name desc"` (legacy string) or `SortConfig[]`.
|
|
53
|
+
* @returns The ordering map, or `undefined` when nothing orderable was authored.
|
|
54
|
+
*/
|
|
55
|
+
export declare function convertSortToQueryParams(sort: string | QuerySortEntry[] | undefined | null): Record<string, 'asc' | 'desc'> | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
* Normalize an authored `sort` into the field→direction map used for
|
|
10
|
+
* `QueryParams.$orderby`.
|
|
11
|
+
*
|
|
12
|
+
* @param sort `"name desc"` (legacy string) or `SortConfig[]`.
|
|
13
|
+
* @returns The ordering map, or `undefined` when nothing orderable was authored.
|
|
14
|
+
*/
|
|
15
|
+
export function convertSortToQueryParams(sort) {
|
|
16
|
+
if (!sort)
|
|
17
|
+
return undefined;
|
|
18
|
+
// Legacy string clause: "name desc" / "name" (a bare field means ascending).
|
|
19
|
+
if (typeof sort === 'string') {
|
|
20
|
+
const [field, direction] = sort.trim().split(/\s+/);
|
|
21
|
+
if (!field)
|
|
22
|
+
return undefined;
|
|
23
|
+
return { [field]: direction?.toLowerCase() === 'desc' ? 'desc' : 'asc' };
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(sort)) {
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const entry of sort) {
|
|
28
|
+
if (!entry || typeof entry.field !== 'string' || entry.field === '')
|
|
29
|
+
continue;
|
|
30
|
+
out[entry.field] = entry.order === 'desc' ? 'desc' : 'asc';
|
|
31
|
+
}
|
|
32
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@objectstack/formula": "^17.0.0-rc.
|
|
29
|
-
"@objectstack/spec": "^17.0.0-rc.
|
|
28
|
+
"@objectstack/formula": "^17.0.0-rc.6",
|
|
29
|
+
"@objectstack/spec": "^17.0.0-rc.6",
|
|
30
30
|
"lodash": "^4.18.1",
|
|
31
31
|
"zod": "^4.4.3",
|
|
32
|
-
"@object-ui/types": "17.
|
|
32
|
+
"@object-ui/types": "17.5.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "^6.0.3",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "tsc",
|
|
64
64
|
"test": "vitest run",
|
|
65
|
-
"type-check": "tsc --noEmit",
|
|
65
|
+
"type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
66
66
|
"lint": "eslint ."
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
* Adapter: merge stack-level views into object definitions.
|
|
10
|
-
*
|
|
11
|
-
* Views are defined at the stack level (views[].listViews) but the runtime
|
|
12
|
-
* expects listViews on each object definition. This bridges the gap until
|
|
13
|
-
* the runtime/provider layer handles it natively.
|
|
14
|
-
*
|
|
15
|
-
* @param objects - Object definitions from composed stack
|
|
16
|
-
* @param views - View definitions containing listViews keyed by object name
|
|
17
|
-
* @returns Objects with listViews merged in from matching views
|
|
18
|
-
*/
|
|
19
|
-
export declare function mergeViewsIntoObjects(objects: any[], views: any[]): any[];
|
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
* Adapter: merge stack-level views into object definitions.
|
|
10
|
-
*
|
|
11
|
-
* Views are defined at the stack level (views[].listViews) but the runtime
|
|
12
|
-
* expects listViews on each object definition. This bridges the gap until
|
|
13
|
-
* the runtime/provider layer handles it natively.
|
|
14
|
-
*
|
|
15
|
-
* @param objects - Object definitions from composed stack
|
|
16
|
-
* @param views - View definitions containing listViews keyed by object name
|
|
17
|
-
* @returns Objects with listViews merged in from matching views
|
|
18
|
-
*/
|
|
19
|
-
export function mergeViewsIntoObjects(objects, views) {
|
|
20
|
-
const viewsByObject = {};
|
|
21
|
-
for (const view of views) {
|
|
22
|
-
if (!view.listViews)
|
|
23
|
-
continue;
|
|
24
|
-
for (const [viewName, listView] of Object.entries(view.listViews)) {
|
|
25
|
-
const objectName = listView?.data?.object;
|
|
26
|
-
if (!objectName)
|
|
27
|
-
continue;
|
|
28
|
-
if (!viewsByObject[objectName])
|
|
29
|
-
viewsByObject[objectName] = {};
|
|
30
|
-
viewsByObject[objectName][viewName] = listView;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return objects.map((obj) => {
|
|
34
|
-
const v = viewsByObject[obj.name];
|
|
35
|
-
if (!v)
|
|
36
|
-
return obj;
|
|
37
|
-
return { ...obj, listViews: { ...(obj.listViews || {}), ...v } };
|
|
38
|
-
});
|
|
39
|
-
}
|