@object-ui/core 17.1.0 → 17.2.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 +526 -0
- package/dist/actions/ActionEngine.d.ts +11 -1
- package/dist/actions/ActionEngine.js +14 -3
- package/dist/actions/ActionRunner.d.ts +117 -10
- package/dist/actions/ActionRunner.js +25 -3
- package/dist/actions/actionKeys.d.ts +28 -11
- package/dist/actions/actionKeys.js +50 -10
- package/dist/query/query-ast.d.ts +6 -6
- package/dist/query/query-ast.js +2 -2
- package/dist/registry/PluginSystem.d.ts +15 -5
- package/dist/registry/PluginSystem.js +1 -1
- package/dist/registry/WidgetRegistry.d.ts +7 -7
- package/dist/registry/WidgetRegistry.js +1 -1
- package/dist/styling/scoped-styles.d.ts +14 -8
- package/dist/utils/chart-series.d.ts +14 -2
- package/dist/utils/dashboard-filters.d.ts +9 -0
- package/dist/utils/dashboard-filters.js +99 -4
- package/dist/utils/dataset-format.d.ts +13 -1
- package/dist/utils/dataset-format.js +10 -2
- package/dist/utils/filter-tokens.d.ts +13 -6
- package/dist/utils/filter-tokens.js +13 -6
- package/dist/utils/freeze-schema.d.ts +14 -6
- package/dist/utils/freeze-schema.js +14 -6
- package/dist/utils/managedBy.d.ts +78 -50
- package/dist/utils/managedBy.js +75 -52
- package/dist/utils/normalize-list-view.d.ts +12 -2
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/required-presence.d.ts +31 -0
- package/dist/validation/required-presence.js +36 -0
- package/dist/validation/schema-validator.d.ts +24 -8
- package/package.json +4 -4
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
import { resolveDateMacros } from './date-macros.js';
|
|
8
9
|
/** Reserved filter name for the dashboard's built-in date range. */
|
|
9
10
|
export const DATE_RANGE_FILTER_NAME = 'dateRange';
|
|
10
11
|
/** Default target field for the built-in date range filter. */
|
|
@@ -31,6 +32,72 @@ const PRESET_RANGES = {
|
|
|
31
32
|
};
|
|
32
33
|
/** Preset keys the filter bar offers, in display order. */
|
|
33
34
|
export const DATE_RANGE_PRESETS = Object.keys(PRESET_RANGES);
|
|
35
|
+
/**
|
|
36
|
+
* ISO calendar date, optionally carrying a time part — `2026-01-15`,
|
|
37
|
+
* `2026-01-15T08:30:00Z`. Deliberately narrower than `Date.parse`, which
|
|
38
|
+
* also accepts locale prose (`March 5, 2026`) and bare years (`2026`);
|
|
39
|
+
* neither is a value the backend compares a date column against usefully.
|
|
40
|
+
*/
|
|
41
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:[T ][\d:.]+(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
42
|
+
/**
|
|
43
|
+
* True when a bare string value can legitimately reach a query as a date
|
|
44
|
+
* (#3151). Two spellings qualify:
|
|
45
|
+
*
|
|
46
|
+
* - a **date-macro token** (`{today}`, `${current_month_start}`,
|
|
47
|
+
* `{7_days_ago}`) — it stays symbolic in the generated condition and is
|
|
48
|
+
* resolved at query time by `resolveDateMacros`, exactly like the bounds
|
|
49
|
+
* `PRESET_RANGES` emits. The check asks that resolver itself instead of
|
|
50
|
+
* restating its grammar here: one token vocabulary, no second dialect to
|
|
51
|
+
* drift — and a token it does not know is precisely the typo this guard
|
|
52
|
+
* exists to catch;
|
|
53
|
+
* - an **ISO date**, which means equality on that day (documented behaviour).
|
|
54
|
+
*/
|
|
55
|
+
function isUsableDateString(value) {
|
|
56
|
+
if (resolveDateMacros(value) !== value)
|
|
57
|
+
return true;
|
|
58
|
+
return ISO_DATE_RE.test(value) && !Number.isNaN(Date.parse(value));
|
|
59
|
+
}
|
|
60
|
+
/** `today, yesterday, …` — quoted in every rejection so the fix is in reach. */
|
|
61
|
+
function presetList() {
|
|
62
|
+
return DATE_RANGE_PRESETS.join(', ');
|
|
63
|
+
}
|
|
64
|
+
function warnDateFilter(message) {
|
|
65
|
+
if (typeof console !== 'undefined')
|
|
66
|
+
console.warn(`[dashboard-filters] ${message}`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Normalize a date filter's DECLARED default into the `DateRangeValue` shape
|
|
70
|
+
* every date consumer in this module reads (framework#4475).
|
|
71
|
+
*
|
|
72
|
+
* The built-in `dateRange` declaration has always been normalized this way —
|
|
73
|
+
* `schema.dateRange.defaultRange` is a preset NAME and
|
|
74
|
+
* `resolveDashboardFilterDefs` lifts it to `{ preset }`. A `globalFilters`
|
|
75
|
+
* entry of `type: 'date'` was passed through raw instead, and that asymmetry
|
|
76
|
+
* is the whole bug: `@objectstack/spec`'s `GlobalFilterSchema.defaultValue` is
|
|
77
|
+
* `string | number | boolean`, so a bare preset name is the ONLY spelling an
|
|
78
|
+
* author can write, yet nothing ever mapped it. Both symptoms of Setup's
|
|
79
|
+
* System Overview reading 0 across every KPI tile follow from that:
|
|
80
|
+
*
|
|
81
|
+
* - `buildFilterCondition` fell through to its "a bare string date means
|
|
82
|
+
* equality on that day" branch and emitted `created_at = 'last_7_days'`,
|
|
83
|
+
* which matches no row — a query the backend answers `200 OK` with a 0;
|
|
84
|
+
* - `DateRangeFilter` reads `value.preset` / `.from` / `.to`, all `undefined`
|
|
85
|
+
* on a bare string, so the control displayed "All time" while sending that
|
|
86
|
+
* equality — the tiles looked deliberately unfiltered and merely empty.
|
|
87
|
+
*
|
|
88
|
+
* Only a name this module actually knows is lifted. A genuine ISO date string
|
|
89
|
+
* still means equality on that day (the documented behaviour), and a number /
|
|
90
|
+
* boolean / unrecognised string is left exactly as declared — an unrecognised
|
|
91
|
+
* string then never reaches a query at all: `buildFilterCondition` skips it
|
|
92
|
+
* with a warning rather than comparing a column against it (#3151).
|
|
93
|
+
*/
|
|
94
|
+
function normalizeDateDefault(type, defaultValue) {
|
|
95
|
+
if (type !== 'date' && type !== 'dateRange')
|
|
96
|
+
return defaultValue;
|
|
97
|
+
if (typeof defaultValue !== 'string')
|
|
98
|
+
return defaultValue;
|
|
99
|
+
return defaultValue in PRESET_RANGES ? { preset: defaultValue } : defaultValue;
|
|
100
|
+
}
|
|
34
101
|
/**
|
|
35
102
|
* Normalize a filter's static `options` declaration to `{ value, label }`
|
|
36
103
|
* pairs. The @objectstack/spec `GlobalFilterSchema.options` form is
|
|
@@ -88,14 +155,18 @@ export function resolveDashboardFilterDefs(schema) {
|
|
|
88
155
|
if (byName.has(name) && typeof console !== 'undefined') {
|
|
89
156
|
console.warn(`[dashboard-filters] duplicate filter name "${name}" — the later definition wins`);
|
|
90
157
|
}
|
|
158
|
+
const type = f.type ?? 'text';
|
|
91
159
|
byName.set(name, {
|
|
92
160
|
name,
|
|
93
161
|
field: f.field,
|
|
94
162
|
label: f.label,
|
|
95
|
-
type
|
|
163
|
+
type,
|
|
96
164
|
options: normalizeFilterOptions(f.options),
|
|
97
165
|
optionsFrom: f.optionsFrom,
|
|
98
|
-
|
|
166
|
+
// framework#4475 — same preset-name lifting the built-in `dateRange`
|
|
167
|
+
// above already does; see normalizeDateDefault for why a bare string is
|
|
168
|
+
// the only thing an author can declare here.
|
|
169
|
+
defaultValue: normalizeDateDefault(type, f.defaultValue),
|
|
99
170
|
targetWidgets: f.targetWidgets,
|
|
100
171
|
});
|
|
101
172
|
}
|
|
@@ -134,6 +205,15 @@ function isEmptyValue(def, value) {
|
|
|
134
205
|
* Build the operator shape (the value side of a `FilterCondition` entry) for
|
|
135
206
|
* one filter's current value. Returns `undefined` when the value imposes no
|
|
136
207
|
* constraint. The caller keys the result by the bound field name.
|
|
208
|
+
*
|
|
209
|
+
* A `date`/`dateRange` value is held to three spellings (#3151): a known
|
|
210
|
+
* preset name → range bounds; a date-macro token or ISO date → equality on
|
|
211
|
+
* that day; **anything else → skipped with a console warning**, never
|
|
212
|
+
* silently downgraded to an equality nothing can match. That last branch is
|
|
213
|
+
* the same strictness `buildWidgetScopedFilter` applies to a default binding
|
|
214
|
+
* on an unknown field name, for the same reason: a query the backend answers
|
|
215
|
+
* `200 OK` with zero rows is indistinguishable from "this range has no data",
|
|
216
|
+
* so the typo has to be said out loud somewhere.
|
|
137
217
|
*/
|
|
138
218
|
export function buildFilterCondition(def, value) {
|
|
139
219
|
if (isEmptyValue(def, value))
|
|
@@ -141,9 +221,18 @@ export function buildFilterCondition(def, value) {
|
|
|
141
221
|
if (def.type === 'dateRange' || def.type === 'date') {
|
|
142
222
|
const v = value;
|
|
143
223
|
if (typeof v === 'object') {
|
|
144
|
-
const
|
|
224
|
+
const preset = typeof v.preset === 'string' && v.preset ? v.preset : undefined;
|
|
225
|
+
const range = preset ? PRESET_RANGES[preset] : undefined;
|
|
145
226
|
const from = range?.from ?? v.from;
|
|
146
227
|
const to = range?.to ?? v.to;
|
|
228
|
+
if (preset && !range) {
|
|
229
|
+
// Was already dropped here — but silently, which reads as "no data".
|
|
230
|
+
warnDateFilter(from || to
|
|
231
|
+
? `filter "${def.name}": ignoring unknown date range preset "${preset}" — ` +
|
|
232
|
+
`using the explicit from/to bounds instead; known presets: ${presetList()}`
|
|
233
|
+
: `skipping filter "${def.name}": unknown date range preset "${preset}" — ` +
|
|
234
|
+
`expected one of: ${presetList()}`);
|
|
235
|
+
}
|
|
147
236
|
if (!from && !to)
|
|
148
237
|
return undefined;
|
|
149
238
|
return {
|
|
@@ -151,7 +240,13 @@ export function buildFilterCondition(def, value) {
|
|
|
151
240
|
...(to ? { $lte: to } : {}),
|
|
152
241
|
};
|
|
153
242
|
}
|
|
154
|
-
|
|
243
|
+
if (typeof v === 'string' && !isUsableDateString(v)) {
|
|
244
|
+
warnDateFilter(`skipping filter "${def.name}": value "${v}" is neither a known date range preset ` +
|
|
245
|
+
`nor an ISO date — expected one of: ${presetList()}, an ISO date (YYYY-MM-DD), ` +
|
|
246
|
+
`or a date macro such as {today}`);
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
// A bare string date (or date macro) means equality on that day.
|
|
155
250
|
return value;
|
|
156
251
|
}
|
|
157
252
|
if (def.type === 'select' || def.type === 'lookup') {
|
|
@@ -27,7 +27,19 @@ export interface DatasetResultField {
|
|
|
27
27
|
label?: string;
|
|
28
28
|
format?: string;
|
|
29
29
|
currency?: string;
|
|
30
|
+
percentScale?: PercentScale;
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* How a percentage column's stored number relates to its displayed percentage —
|
|
34
|
+
* the server's answer to a question a `%` format string cannot express:
|
|
35
|
+
* `fraction` is a 0–1 ratio (`1` ⇒ "100%"), `whole` is already percentage
|
|
36
|
+
* points (`1` ⇒ "1%"). Resolved from metadata server-side (a `ratio` measure is
|
|
37
|
+
* a fraction by definition; a measure over a `percent` field inherits that
|
|
38
|
+
* field's scale) and carried on the result column, so display never has to
|
|
39
|
+
* infer it from the value's magnitude — the inference that printed a ratio of
|
|
40
|
+
* exactly 1 as "1.0%" (#3136).
|
|
41
|
+
*/
|
|
42
|
+
export type PercentScale = 'fraction' | 'whole';
|
|
31
43
|
/**
|
|
32
44
|
* Scale a stored `percent`-field value to its DISPLAY magnitude.
|
|
33
45
|
*
|
|
@@ -49,7 +61,7 @@ export declare function percentDisplayValue(value: number): number;
|
|
|
49
61
|
* controls grouping / decimals / percent; it can't be baked into the row value
|
|
50
62
|
* server-side (the same number feeds charts), so it is applied here.
|
|
51
63
|
*/
|
|
52
|
-
export declare function formatMeasure(v: unknown, format?: string, currency?: string): string;
|
|
64
|
+
export declare function formatMeasure(v: unknown, format?: string, currency?: string, percentScale?: PercentScale): string;
|
|
53
65
|
/**
|
|
54
66
|
* Format a non-measure (dimension / label) value — the server already resolves
|
|
55
67
|
* dimension display labels, so this only tidies numbers and nulls.
|
|
@@ -28,7 +28,7 @@ export function percentDisplayValue(value) {
|
|
|
28
28
|
* controls grouping / decimals / percent; it can't be baked into the row value
|
|
29
29
|
* server-side (the same number feeds charts), so it is applied here.
|
|
30
30
|
*/
|
|
31
|
-
export function formatMeasure(v, format, currency) {
|
|
31
|
+
export function formatMeasure(v, format, currency, percentScale) {
|
|
32
32
|
if (v == null)
|
|
33
33
|
return '—';
|
|
34
34
|
if (typeof v !== 'number')
|
|
@@ -59,7 +59,15 @@ export function formatMeasure(v, format, currency) {
|
|
|
59
59
|
// (0.75 ⇒ 75%). Scale to display magnitude the SAME way the list-view cell
|
|
60
60
|
// renderer does — otherwise an avg of 0.608 renders as "0.6%" instead of
|
|
61
61
|
// "60.8%", disagreeing with the per-row "75%" the list already shows.
|
|
62
|
-
|
|
62
|
+
//
|
|
63
|
+
// A DECLARED `percentScale` from the server wins outright: the value-magnitude
|
|
64
|
+
// heuristic below cannot tell a 0–1 ratio of exactly 1 from 1 percentage point
|
|
65
|
+
// (both are the number 1) and resolves it as "1%", so an SLA rate of full
|
|
66
|
+
// compliance rendered as "1.0%" instead of "100.0%" (#3136). The heuristic
|
|
67
|
+
// stays as the fallback for columns that arrive without the annotation.
|
|
68
|
+
const display = isPercent
|
|
69
|
+
? (percentScale ? (percentScale === 'fraction' ? v * 100 : v) : percentDisplayValue(v))
|
|
70
|
+
: v;
|
|
63
71
|
const body = display.toLocaleString(undefined, { minimumFractionDigits: decimals ?? 0, maximumFractionDigits: decimals ?? 0 });
|
|
64
72
|
return `${legacyDollar}${body}${isPercent ? '%' : ''}`;
|
|
65
73
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
import { CONTEXT_TOKENS } from '@objectstack/spec/data';
|
|
8
9
|
/**
|
|
9
10
|
* Session-scoped filter placeholders — the sibling vocabulary to
|
|
10
11
|
* `{date-macros}`.
|
|
@@ -42,13 +43,19 @@
|
|
|
42
43
|
* ## Contract source
|
|
43
44
|
*
|
|
44
45
|
* The canonical vocabulary is published as part of the platform contract at
|
|
45
|
-
* `@objectstack/spec` → `CONTEXT_TOKENS
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* `@objectstack/spec/data` → `CONTEXT_TOKENS`. This module used to carry a
|
|
47
|
+
* hand-written copy of that tuple under the spec's own name, with a comment
|
|
48
|
+
* saying the duplication was "temporary until the next coordinated release"
|
|
49
|
+
* — the installed spec (17.0.0-rc.0) has exported it for some time, so the
|
|
50
|
+
* copy was simply a fork wearing the spec's name (objectstack#4115).
|
|
51
|
+
*
|
|
52
|
+
* It is now a **re-export**, which is the only form that cannot drift: the
|
|
53
|
+
* copy was byte-identical, so every value comparison and every behavioural
|
|
54
|
+
* test passed while it sat here. Reference identity is the one check that
|
|
55
|
+
* distinguishes a re-export from a fork (objectui#3003).
|
|
49
56
|
*/
|
|
50
|
-
/** The complete set of session-scoped filter tokens. */
|
|
51
|
-
export
|
|
57
|
+
/** The complete set of session-scoped filter tokens (spec-owned, re-exported). */
|
|
58
|
+
export { CONTEXT_TOKENS };
|
|
52
59
|
export type ContextTokenName = (typeof CONTEXT_TOKENS)[number];
|
|
53
60
|
/** Session values a filter placeholder can resolve against. */
|
|
54
61
|
export interface FilterTokenScope {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
import { CONTEXT_TOKENS } from '@objectstack/spec/data';
|
|
8
9
|
import { resolveDateMacros } from './date-macros.js';
|
|
9
10
|
/**
|
|
10
11
|
* Session-scoped filter placeholders — the sibling vocabulary to
|
|
@@ -43,13 +44,19 @@ import { resolveDateMacros } from './date-macros.js';
|
|
|
43
44
|
* ## Contract source
|
|
44
45
|
*
|
|
45
46
|
* The canonical vocabulary is published as part of the platform contract at
|
|
46
|
-
* `@objectstack/spec` → `CONTEXT_TOKENS
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* `@objectstack/spec/data` → `CONTEXT_TOKENS`. This module used to carry a
|
|
48
|
+
* hand-written copy of that tuple under the spec's own name, with a comment
|
|
49
|
+
* saying the duplication was "temporary until the next coordinated release"
|
|
50
|
+
* — the installed spec (17.0.0-rc.0) has exported it for some time, so the
|
|
51
|
+
* copy was simply a fork wearing the spec's name (objectstack#4115).
|
|
52
|
+
*
|
|
53
|
+
* It is now a **re-export**, which is the only form that cannot drift: the
|
|
54
|
+
* copy was byte-identical, so every value comparison and every behavioural
|
|
55
|
+
* test passed while it sat here. Reference identity is the one check that
|
|
56
|
+
* distinguishes a re-export from a fork (objectui#3003).
|
|
50
57
|
*/
|
|
51
|
-
/** The complete set of session-scoped filter tokens. */
|
|
52
|
-
export
|
|
58
|
+
/** The complete set of session-scoped filter tokens (spec-owned, re-exported). */
|
|
59
|
+
export { CONTEXT_TOKENS };
|
|
53
60
|
/**
|
|
54
61
|
* Near-miss spellings → the token the author meant.
|
|
55
62
|
*
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* System View immutability layer.
|
|
10
10
|
*
|
|
11
11
|
* A "System View" is any UI schema authored in source code (imported `.ts`/`.json`,
|
|
12
|
-
* `as const` literals, or returned from a `
|
|
12
|
+
* `as const` literals, or returned from a `defineSystemView()` call). Such schemas are
|
|
13
13
|
* part of the product contract and MUST NOT be mutated at runtime. Mutation
|
|
14
14
|
* would cause behavior drift, break TypeScript inference, and bypass code review.
|
|
15
15
|
*
|
|
@@ -37,7 +37,7 @@ export type DeepReadonly<T> = T extends (...args: any[]) => any ? T : T extends
|
|
|
37
37
|
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
38
38
|
} : T;
|
|
39
39
|
/**
|
|
40
|
-
* A schema that has been frozen by `
|
|
40
|
+
* A schema that has been frozen by `defineSystemView()`. The marker symbol is
|
|
41
41
|
* non-enumerable and therefore invisible to consumers, but its presence
|
|
42
42
|
* lets us discriminate at runtime.
|
|
43
43
|
*/
|
|
@@ -52,11 +52,19 @@ export declare function deepFreeze<T>(value: T, seen?: WeakSet<object>): DeepRea
|
|
|
52
52
|
/**
|
|
53
53
|
* Mark and freeze a code-loaded schema as a System View.
|
|
54
54
|
*
|
|
55
|
+
* Named `defineView` until objectstack#4115, which is a name `@objectstack/spec`
|
|
56
|
+
* owns for something else entirely: the spec's `defineView(config)` is the
|
|
57
|
+
* VIEW-DOCUMENT factory — it parses a `ViewSchema` (`{ list, form, listViews,
|
|
58
|
+
* formViews }`) and returns a validated `View`. This one takes an arbitrary
|
|
59
|
+
* object, stamps the System-View marker and deep-freezes it; it validates
|
|
60
|
+
* nothing. An app-shell comment already conflated the two, which is precisely
|
|
61
|
+
* the planted-premise failure the guard exists to stop.
|
|
62
|
+
*
|
|
55
63
|
* @example
|
|
56
64
|
* ```ts
|
|
57
|
-
* import {
|
|
65
|
+
* import { defineSystemView } from '@object-ui/core';
|
|
58
66
|
*
|
|
59
|
-
* export const userListView =
|
|
67
|
+
* export const userListView = defineSystemView({
|
|
60
68
|
* type: 'list',
|
|
61
69
|
* data: { object: 'User' },
|
|
62
70
|
* columns: [{ name: 'email' }],
|
|
@@ -69,9 +77,9 @@ export declare function deepFreeze<T>(value: T, seen?: WeakSet<object>): DeepRea
|
|
|
69
77
|
* To produce a mutable derivative (Tenant or User View), call
|
|
70
78
|
* `cloneAsOverride(userListView)`.
|
|
71
79
|
*/
|
|
72
|
-
export declare function
|
|
80
|
+
export declare function defineSystemView<T extends object>(schema: T): SystemView<T>;
|
|
73
81
|
/**
|
|
74
|
-
* Runtime check: was this object produced by `
|
|
82
|
+
* Runtime check: was this object produced by `defineSystemView()` (or loaded from a
|
|
75
83
|
* source that forwards the marker)?
|
|
76
84
|
*/
|
|
77
85
|
export declare function isSystemView(value: unknown): boolean;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* System View immutability layer.
|
|
10
10
|
*
|
|
11
11
|
* A "System View" is any UI schema authored in source code (imported `.ts`/`.json`,
|
|
12
|
-
* `as const` literals, or returned from a `
|
|
12
|
+
* `as const` literals, or returned from a `defineSystemView()` call). Such schemas are
|
|
13
13
|
* part of the product contract and MUST NOT be mutated at runtime. Mutation
|
|
14
14
|
* would cause behavior drift, break TypeScript inference, and bypass code review.
|
|
15
15
|
*
|
|
@@ -86,11 +86,19 @@ export function deepFreeze(value, seen = new WeakSet()) {
|
|
|
86
86
|
/**
|
|
87
87
|
* Mark and freeze a code-loaded schema as a System View.
|
|
88
88
|
*
|
|
89
|
+
* Named `defineView` until objectstack#4115, which is a name `@objectstack/spec`
|
|
90
|
+
* owns for something else entirely: the spec's `defineView(config)` is the
|
|
91
|
+
* VIEW-DOCUMENT factory — it parses a `ViewSchema` (`{ list, form, listViews,
|
|
92
|
+
* formViews }`) and returns a validated `View`. This one takes an arbitrary
|
|
93
|
+
* object, stamps the System-View marker and deep-freezes it; it validates
|
|
94
|
+
* nothing. An app-shell comment already conflated the two, which is precisely
|
|
95
|
+
* the planted-premise failure the guard exists to stop.
|
|
96
|
+
*
|
|
89
97
|
* @example
|
|
90
98
|
* ```ts
|
|
91
|
-
* import {
|
|
99
|
+
* import { defineSystemView } from '@object-ui/core';
|
|
92
100
|
*
|
|
93
|
-
* export const userListView =
|
|
101
|
+
* export const userListView = defineSystemView({
|
|
94
102
|
* type: 'list',
|
|
95
103
|
* data: { object: 'User' },
|
|
96
104
|
* columns: [{ name: 'email' }],
|
|
@@ -103,9 +111,9 @@ export function deepFreeze(value, seen = new WeakSet()) {
|
|
|
103
111
|
* To produce a mutable derivative (Tenant or User View), call
|
|
104
112
|
* `cloneAsOverride(userListView)`.
|
|
105
113
|
*/
|
|
106
|
-
export function
|
|
114
|
+
export function defineSystemView(schema) {
|
|
107
115
|
if (schema == null || typeof schema !== 'object') {
|
|
108
|
-
throw new TypeError('[ObjectUI]
|
|
116
|
+
throw new TypeError('[ObjectUI] defineSystemView() expects a non-null object schema.');
|
|
109
117
|
}
|
|
110
118
|
// Stamp the marker on the root only — nested nodes share origin via lineage.
|
|
111
119
|
// Non-enumerable keeps it out of JSON, spreads, and Object.keys.
|
|
@@ -120,7 +128,7 @@ export function defineView(schema) {
|
|
|
120
128
|
return deepFreeze(schema);
|
|
121
129
|
}
|
|
122
130
|
/**
|
|
123
|
-
* Runtime check: was this object produced by `
|
|
131
|
+
* Runtime check: was this object produced by `defineSystemView()` (or loaded from a
|
|
124
132
|
* source that forwards the marker)?
|
|
125
133
|
*/
|
|
126
134
|
export function isSystemView(value) {
|
|
@@ -1,51 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* managedBy — the
|
|
3
|
-
*
|
|
2
|
+
* managedBy — the UI end of the platform's `managedBy` → CRUD affordance
|
|
3
|
+
* contract (ADR-0103).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* (`@objectstack/spec/data/object.zod.ts`, ADR-0103). Lives in `@object-ui/core`
|
|
7
|
-
* — React-free and reachable by every UI package (app-shell, plugin-detail,
|
|
8
|
-
* plugin-form, plugin-grid) — so the bucket logic is defined ONCE instead of
|
|
9
|
-
* hand-mirrored in each. Components ask "should I show New / Import / Edit /
|
|
10
|
-
* Delete / Export?" and get an answer that tracks the object's lifecycle without
|
|
11
|
-
* special-casing `sys_*` names.
|
|
5
|
+
* ## What is spec-owned here, and what is not (objectstack#4115)
|
|
12
6
|
*
|
|
13
|
-
* The bucket
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
7
|
+
* The bucket logic is **the spec's**, and this module now says so in code
|
|
8
|
+
* rather than in a comment. `CrudAffordances` / `RowCrudPredicates` are
|
|
9
|
+
* re-exported from `@objectstack/spec/data`, and the bucket resolution itself
|
|
10
|
+
* delegates to the spec's `resolveCrudAffordances()`.
|
|
11
|
+
*
|
|
12
|
+
* This module used to describe itself as a "UI-side mirror" of that function
|
|
13
|
+
* and carry its own `DEFAULTS` table — a line-for-line copy of the spec's
|
|
14
|
+
* `CRUD_AFFORDANCE_DEFAULTS`, plus a copy of its override parser. A faithful
|
|
15
|
+
* copy passes every value comparison and every behavioural test, so nothing
|
|
16
|
+
* would have caught the release in which the spec re-buckets an object class;
|
|
17
|
+
* reference identity is the only check that distinguishes a re-export from a
|
|
18
|
+
* fork (objectui#3003).
|
|
19
|
+
*
|
|
20
|
+
* What is genuinely objectui's and keeps a local name:
|
|
21
|
+
* {@link resolveEffectiveCrudAffordances} — the spec's object-level matrix
|
|
22
|
+
* INTERSECTED with the server-resolved effective API operation set (#3391), so
|
|
23
|
+
* the UI never offers a button the server would 405. The spec's function has
|
|
24
|
+
* no such notion; naming ours after it was the misdescription this burn-down
|
|
25
|
+
* removes.
|
|
26
|
+
*
|
|
27
|
+
* Lives in `@object-ui/core` — React-free and reachable by every UI package
|
|
28
|
+
* (app-shell, plugin-detail, plugin-form, plugin-grid) — so the intersection is
|
|
29
|
+
* defined ONCE instead of hand-mirrored in each. The bucket UNION type lives in
|
|
30
|
+
* `@object-ui/types` (`ManagedByBucket`) and is re-exported here for
|
|
31
|
+
* convenience. All translated copy (badge variants, empty-state messages) stays
|
|
32
|
+
* in `@object-ui/app-shell`.
|
|
17
33
|
*/
|
|
34
|
+
import { type CrudAffordances, type RowCrudPredicates } from '@objectstack/spec/data';
|
|
18
35
|
export type { ManagedByBucket } from '@object-ui/types';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Per-record CEL predicates for the built-in row Edit/Delete actions,
|
|
32
|
-
* present only when `userActions.edit` / `delete` used the object form
|
|
33
|
-
* (objectui#2614). Carried through as authored for row renderers to evaluate;
|
|
34
|
-
* they never affect the object-level booleans above.
|
|
35
|
-
*/
|
|
36
|
-
editPredicates?: RowCrudPredicates;
|
|
37
|
-
deletePredicates?: RowCrudPredicates;
|
|
38
|
-
}
|
|
39
|
-
/** Per-record predicates from the #2614 object form of a userActions flag. */
|
|
40
|
-
export interface RowCrudPredicates {
|
|
41
|
-
visibleWhen?: unknown;
|
|
42
|
-
disabledWhen?: unknown;
|
|
43
|
-
}
|
|
36
|
+
/**
|
|
37
|
+
* The resolved CRUD affordance matrix and its per-record predicate envelope
|
|
38
|
+
* are **spec-owned** (`@objectstack/spec/data`, ADR-0103) and re-exported here,
|
|
39
|
+
* not re-declared.
|
|
40
|
+
*
|
|
41
|
+
* Note this also TIGHTENS the predicate types: the local copies typed
|
|
42
|
+
* `visibleWhen` / `disabledWhen` as `unknown`, while the spec types them as
|
|
43
|
+
* `Expression | ExpressionInput` — the authored CEL shorthand or its envelope.
|
|
44
|
+
* The local `unknown` was imprecision, not a deliberate dialect.
|
|
45
|
+
*/
|
|
46
|
+
export type { CrudAffordances, RowCrudPredicates };
|
|
44
47
|
/** `edit`/`delete` accept a bare boolean or the #2614 object form. */
|
|
45
48
|
export type UserActionOverride = boolean | {
|
|
46
49
|
enabled?: boolean;
|
|
47
|
-
visibleWhen?:
|
|
48
|
-
disabledWhen?:
|
|
50
|
+
visibleWhen?: RowCrudPredicates['visibleWhen'];
|
|
51
|
+
disabledWhen?: RowCrudPredicates['disabledWhen'];
|
|
49
52
|
};
|
|
50
53
|
export interface UserActionsOverride {
|
|
51
54
|
create?: boolean;
|
|
@@ -63,11 +66,14 @@ export interface SchemaLike {
|
|
|
63
66
|
* the given bucket default, surfacing any per-record predicates alongside.
|
|
64
67
|
*
|
|
65
68
|
* THE single parser for the `userActions.{edit,delete}` override shape — every
|
|
66
|
-
* UI package (grid row affordances, related-list row predicates
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
69
|
+
* UI package (grid row affordances, related-list row predicates) routes through
|
|
70
|
+
* here instead of re-implementing the boolean/object-form parse locally. The
|
|
71
|
+
* spec keeps its equivalent (`normalizeRowCrudOverride`) module-private, so
|
|
72
|
+
* this stays a local declaration — under a name the spec does not own.
|
|
73
|
+
*
|
|
74
|
+
* `base` is the bucket default an omitted `enabled` falls back to (callers that
|
|
75
|
+
* only gate on an explicit opt-out pass `true`; predicate extraction is
|
|
76
|
+
* independent of `base`).
|
|
71
77
|
*/
|
|
72
78
|
export declare function normalizeUserAction(v: UserActionOverride | undefined | null, base: boolean): {
|
|
73
79
|
enabled: boolean;
|
|
@@ -82,7 +88,15 @@ export declare function normalizeUserAction(v: UserActionOverride | undefined |
|
|
|
82
88
|
*/
|
|
83
89
|
export declare function userActionPredicates(v: UserActionOverride | undefined | null): RowCrudPredicates | undefined;
|
|
84
90
|
/**
|
|
85
|
-
* Resolve the
|
|
91
|
+
* Resolve the CRUD affordances a UI surface should expose for an object —
|
|
92
|
+
* the spec's object-level matrix, INTERSECTED with what the server will
|
|
93
|
+
* actually accept.
|
|
94
|
+
*
|
|
95
|
+
* The bucket/`userActions` half is **not computed here**: it is delegated to
|
|
96
|
+
* `resolveCrudAffordances()` from `@objectstack/spec/data` (ADR-0103), so the
|
|
97
|
+
* bucket table has exactly one definition on the platform. Only the
|
|
98
|
+
* intersection below is objectui's, which is why this function no longer
|
|
99
|
+
* carries the spec's name (objectstack#4115).
|
|
86
100
|
*
|
|
87
101
|
* @param obj The object schema (managedBy bucket + userActions overrides).
|
|
88
102
|
* @param effectiveApiOperations OPTIONAL server-resolved effective API operation
|
|
@@ -93,13 +107,27 @@ export declare function userActionPredicates(v: UserActionOverride | undefined |
|
|
|
93
107
|
* (old backend / no effective set) leaves the affordances untouched —
|
|
94
108
|
* backward-compatible. An empty array means "expose nothing" → all bits off.
|
|
95
109
|
*/
|
|
96
|
-
export declare function
|
|
110
|
+
export declare function resolveEffectiveCrudAffordances(obj: SchemaLike | null | undefined, effectiveApiOperations?: readonly string[] | null): CrudAffordances;
|
|
97
111
|
/** True only when a `userActions` flag (bare boolean or #2614 object form) opts the write in. */
|
|
98
112
|
export declare function isWriteOptedIn(v: UserActionOverride | undefined | null): boolean;
|
|
99
113
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
114
|
+
* True for a platform-defined schema whose DATA is admin/user-writable — the
|
|
115
|
+
* `system-data` bucket. Used to pick the writable-system badge/empty-state copy
|
|
116
|
+
* instead of the engine-owned "read-only monitoring surface" copy.
|
|
117
|
+
*
|
|
118
|
+
* **Simplified in protocol 17 (objectstack#3355).** Under ADR-0103's v16 split
|
|
119
|
+
* this had to RECOVER the distinction from `userActions`: `system` doubled as
|
|
120
|
+
* both the engine-owned default and the writable set, so "did it open any
|
|
121
|
+
* write?" was the only way to tell a Notification Preferences grid from an
|
|
122
|
+
* automation-run log. v17 retired that overload — the writable half is now the
|
|
123
|
+
* `system-data` bucket (writable by DEFAULT) and the engine-owned half is
|
|
124
|
+
* `engine-owned` — so the bucket answers the question directly and the
|
|
125
|
+
* `userActions` probe would only re-derive what the value already states.
|
|
126
|
+
*
|
|
127
|
+
* The narrowing case is deliberately still `true`: a `system-data` grid that
|
|
128
|
+
* narrows to edit-only is still platform-schema/user-data and should read as
|
|
129
|
+
* such. The spec refuses `system-data` on an object that narrows away every
|
|
130
|
+
* write, so there is no "writable bucket with no writes" shape to worry about.
|
|
103
131
|
*/
|
|
104
132
|
export declare function isSystemWritable(obj: SchemaLike | null | undefined): boolean;
|
|
105
133
|
/**
|