@object-ui/core 16.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +1881 -4
  2. package/dist/actions/ActionEngine.d.ts +11 -1
  3. package/dist/actions/ActionEngine.js +14 -3
  4. package/dist/actions/ActionRunner.d.ts +199 -24
  5. package/dist/actions/ActionRunner.js +189 -84
  6. package/dist/actions/actionKeys.d.ts +135 -0
  7. package/dist/actions/actionKeys.js +283 -0
  8. package/dist/actions/index.d.ts +1 -0
  9. package/dist/actions/index.js +1 -0
  10. package/dist/adapters/ApiDataSource.js +10 -1
  11. package/dist/adapters/ValueDataSource.d.ts +2 -2
  12. package/dist/evaluator/fieldRules.d.ts +0 -2
  13. package/dist/evaluator/fieldRules.js +3 -2
  14. package/dist/evaluator/optionLint.js +5 -2
  15. package/dist/evaluator/optionRules.d.ts +13 -6
  16. package/dist/evaluator/optionRules.js +0 -23
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.js +7 -0
  19. package/dist/query/query-ast.d.ts +6 -6
  20. package/dist/query/query-ast.js +2 -2
  21. package/dist/registry/PluginSystem.d.ts +15 -5
  22. package/dist/registry/PluginSystem.js +1 -1
  23. package/dist/registry/Registry.d.ts +81 -2
  24. package/dist/registry/Registry.js +102 -2
  25. package/dist/registry/WidgetRegistry.d.ts +7 -7
  26. package/dist/registry/WidgetRegistry.js +1 -1
  27. package/dist/registry/public-blocks.js +52 -1
  28. package/dist/styling/scoped-styles.d.ts +14 -8
  29. package/dist/theme/ThemeEngine.js +0 -12
  30. package/dist/utils/chart-series.d.ts +42 -2
  31. package/dist/utils/chart-series.js +55 -0
  32. package/dist/utils/column-identity.d.ts +110 -0
  33. package/dist/utils/column-identity.js +210 -0
  34. package/dist/utils/dashboard-filters.d.ts +11 -2
  35. package/dist/utils/dashboard-filters.js +99 -4
  36. package/dist/utils/dataset-format.d.ts +13 -1
  37. package/dist/utils/dataset-format.js +10 -2
  38. package/dist/utils/expand-fields.js +8 -8
  39. package/dist/utils/filter-converter.d.ts +47 -0
  40. package/dist/utils/filter-converter.js +94 -7
  41. package/dist/utils/filter-tokens.d.ts +108 -0
  42. package/dist/utils/filter-tokens.js +180 -0
  43. package/dist/utils/freeze-schema.d.ts +14 -6
  44. package/dist/utils/freeze-schema.js +14 -6
  45. package/dist/utils/managedBy.d.ts +97 -51
  46. package/dist/utils/managedBy.js +117 -53
  47. package/dist/utils/normalize-list-view.d.ts +109 -0
  48. package/dist/utils/normalize-list-view.js +255 -0
  49. package/dist/utils/sort-values.d.ts +52 -0
  50. package/dist/utils/sort-values.js +125 -0
  51. package/dist/validation/index.d.ts +10 -1
  52. package/dist/validation/index.js +10 -1
  53. package/dist/validation/required-presence.d.ts +31 -0
  54. package/dist/validation/required-presence.js +36 -0
  55. package/dist/validation/schema-validator.d.ts +24 -8
  56. package/dist/validation/validators/index.d.ts +7 -1
  57. package/dist/validation/validators/index.js +7 -1
  58. package/dist/validation/validators/object-validation-engine.d.ts +151 -31
  59. package/dist/validation/validators/object-validation-engine.js +275 -142
  60. package/package.json +4 -4
@@ -0,0 +1,255 @@
1
+ /**
2
+ * ObjectUI — list-view vocabulary canonicalization
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import { normalizeColumnIdentities } from './column-identity.js';
9
+ /**
10
+ * `densityMode` → `rowHeight`. The two vocabularies are not the same size: the
11
+ * spec has five row heights, ListView's toolbar offers three. Widening maps
12
+ * each density onto the spec value the renderer already resolves it back to
13
+ * (see {@link ROW_HEIGHT_TO_DENSITY_MODE}), so a fold followed by a read is a
14
+ * round trip — `spacious` → `tall` → `spacious`.
15
+ */
16
+ export const DENSITY_MODE_TO_ROW_HEIGHT = {
17
+ compact: 'compact',
18
+ comfortable: 'medium',
19
+ spacious: 'tall',
20
+ };
21
+ /**
22
+ * `rowHeight` → `densityMode`, the read direction. Narrowing five values onto
23
+ * three is lossy by construction: `short` collapses into `compact` and
24
+ * `extra_tall` into `spacious`. Exported so the renderer and the persistence
25
+ * layer agree on the collapse instead of each hard-coding its own table.
26
+ */
27
+ export const ROW_HEIGHT_TO_DENSITY_MODE = {
28
+ compact: 'compact',
29
+ short: 'compact',
30
+ medium: 'comfortable',
31
+ tall: 'spacious',
32
+ extra_tall: 'spacious',
33
+ };
34
+ /**
35
+ * Tolerant reader for {@link ROW_HEIGHT_TO_DENSITY_MODE}. View metadata is
36
+ * user-authored, so `rowHeight` is not guaranteed to be one of the five spec
37
+ * values — an unknown one lands on `comfortable` rather than rendering an
38
+ * undefined density. Keeping the fallback here means every surface collapses
39
+ * the same way.
40
+ */
41
+ export function rowHeightToDensityMode(rowHeight) {
42
+ if (typeof rowHeight === 'string' && rowHeight in ROW_HEIGHT_TO_DENSITY_MODE) {
43
+ return ROW_HEIGHT_TO_DENSITY_MODE[rowHeight];
44
+ }
45
+ return 'comfortable';
46
+ }
47
+ const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
48
+ /**
49
+ * Legacy toolbar-visibility flags → their `userActions` key (#2890 scope A
50
+ * step 3). The spec documents `userActions` as "which interactive actions are
51
+ * available to users in the view toolbar", and already carries `rowHeight` —
52
+ * objectui's `showDensity` under its spec name. `group` / `hideFields` /
53
+ * `rowColor` are the same kind of toggle and are named after the config key
54
+ * they gate (`grouping`, `hiddenFields`, `rowColor`), pending promotion into
55
+ * `UserActionsConfigSchema` upstream.
56
+ */
57
+ const SHOW_FLAG_TO_USER_ACTION = {
58
+ showSearch: 'search',
59
+ showSort: 'sort',
60
+ showFilters: 'filter',
61
+ showDensity: 'rowHeight',
62
+ showGroup: 'group',
63
+ showHideFields: 'hideFields',
64
+ showColor: 'rowColor',
65
+ };
66
+ /**
67
+ * Legacy `sharing.visibility` → the spec's `ViewSharing.type`. The spec models
68
+ * two ownership kinds; objectui's four-value audience enum collapses onto them:
69
+ * only `private` is personal, everything wider is collaborative.
70
+ */
71
+ const VISIBILITY_TO_SHARING_TYPE = {
72
+ private: 'personal',
73
+ team: 'collaborative',
74
+ organization: 'collaborative',
75
+ public: 'collaborative',
76
+ };
77
+ /** Legacy ARIA spellings → the spec's `AriaProps` keys. */
78
+ const ARIA_KEY_ALIASES = {
79
+ label: 'ariaLabel',
80
+ describedBy: 'ariaDescribedBy',
81
+ };
82
+ /**
83
+ * ObjectUI's `list-view` node historically used a different vocabulary from
84
+ * `@objectstack/spec` for the same concepts (`fields` where the spec says
85
+ * `columns`, `viewType` where it says `type`, …). Issue #2231 closed the
86
+ * type-level fork; #2890 closes the vocabulary fork.
87
+ *
88
+ * Stored view metadata in user databases still carries the legacy keys, so the
89
+ * renderer cannot simply stop accepting them. Per AGENTS.md Commandment #0.1 the
90
+ * answer is NOT a per-read-site `??` fallback — those fossilize a second de-facto
91
+ * contract and drift apart (they already had: `ObjectGrid` preferred `columns`
92
+ * in one branch and `fields` in another). Instead legacy acceptance lives HERE,
93
+ * in one documented normalizer at the component boundary, mirroring
94
+ * `normalizeSchemaReferenceKeys` (object schemas) and the spec's own
95
+ * `normalizeVisibleWhen` / `normalizeFilterOperator` migration bridges.
96
+ *
97
+ * Note this cannot be a `z.preprocess` on `ListViewSchema`: nothing on the
98
+ * render path parses view metadata through zod (the zod schemas are used by the
99
+ * CLI validator, the VS Code extension and tests only), so a schema-level fold
100
+ * would never run. The guarantee comes from the call site instead — `ListView`
101
+ * normalizes before it reads anything.
102
+ *
103
+ * The fold is deliberately one-directional: the canonical key wins when both are
104
+ * present, and the legacy key is REMOVED from the result so a read-site that was
105
+ * missed fails loudly instead of quietly taking the legacy path. Like a spec
106
+ * migration bridge, this is expected to be dropped in a future major once stored
107
+ * metadata has been migrated.
108
+ *
109
+ * Non-mutating and allocation-frugal: returns the input by reference when there
110
+ * is nothing to fold, so `ListView`'s downstream `useMemo`s keep a stable
111
+ * dependency identity on the common (already-canonical) path.
112
+ *
113
+ * Currently folded:
114
+ * - `fields` → `columns` (#2890 scope A step 1)
115
+ * - `densityMode` → `rowHeight` (#2890 scope A step 2)
116
+ * - the `show*` toolbar flags → `userActions` (#2890 scope A step 3), and
117
+ * `showDescription` → `appearance.showDescription`. The canonical key wins
118
+ * per-flag, so a view may carry `userActions` for some toggles and a legacy
119
+ * flag for others. NOTE the fold does not apply any default: an absent flag
120
+ * stays absent, because the defaults are per-toggle (search/sort/filter/
121
+ * rowHeight/group default ON, hideFields/rowColor default OFF) and belong to
122
+ * the renderer, not to the vocabulary bridge.
123
+ * - `aria: { label, describedBy }` → the spec's `AriaProps`
124
+ * (`{ ariaLabel, ariaDescribedBy }`), and `sharing: { visibility, enabled }`
125
+ * → the spec's `ViewSharing` (`{ type }`) — #2890 scope A step 5. `aria.live`
126
+ * survives untouched: it has no spec counterpart.
127
+ * - `filters` → `filter` (#2890 scope A step 4). A key rename only: BOTH keys
128
+ * carry an ObjectQL FilterNode array (`[['stage','=','won']]`) everywhere in
129
+ * objectui — every consumer passes the value straight to `$filter`. The spec
130
+ * types `filter` as `ViewFilterRule[]` (`{field, operator, value}` objects),
131
+ * so objectui's field is typed from the spec but used as something else.
132
+ * That mismatch is real and out of scope here; converting formats inside a
133
+ * vocabulary fold would change what reaches the data source.
134
+ * - `viewType`: a missing kind, or the view CATEGORY `'list'` that AI-authored
135
+ * metadata stores and hosts forward verbatim, becomes the renderable `'grid'`
136
+ * — otherwise it reaches the renderer's typeless default branch and shows as
137
+ * a red "Unknown component type" box.
138
+ * - each `columns` entry's IDENTITY — `name` / `fieldName` → the spec's `field`
139
+ * (#3104). This one MIRRORS instead of deleting, for the reason given on
140
+ * {@link normalizeColumnIdentities}: `columns` entries cross the package
141
+ * boundary into host renderers, so dropping `name` from under them is a
142
+ * breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
143
+ * above, so a view that spells its column list the legacy way still gets its
144
+ * entries canonicalized.
145
+ */
146
+ export function normalizeListViewSchema(schema) {
147
+ if (!schema || typeof schema !== 'object')
148
+ return schema;
149
+ const s = schema;
150
+ const legacyFields = s.fields;
151
+ const foldColumns = Array.isArray(legacyFields);
152
+ const legacyDensity = s.densityMode;
153
+ const foldRowHeight = typeof legacyDensity === 'string' && legacyDensity in DENSITY_MODE_TO_ROW_HEIGHT;
154
+ const legacyFilters = s.filters;
155
+ const foldFilter = Array.isArray(legacyFilters);
156
+ const legacyFlags = Object.keys(SHOW_FLAG_TO_USER_ACTION).filter((k) => typeof s[k] === 'boolean');
157
+ const foldDescription = typeof s.showDescription === 'boolean';
158
+ const aria = isRecord(s.aria) ? s.aria : undefined;
159
+ const foldAria = !!aria && Object.keys(ARIA_KEY_ALIASES).some((k) => aria[k] !== undefined);
160
+ const sharing = isRecord(s.sharing) ? s.sharing : undefined;
161
+ const foldSharing = !!sharing && (sharing.visibility !== undefined || sharing.enabled !== undefined);
162
+ const viewType = s.viewType;
163
+ const defaultViewKind = !viewType || viewType === 'list';
164
+ // The columns array the identity fold will see — mirroring the `foldColumns`
165
+ // precedence below (canonical `columns` wins; otherwise the legacy `fields`
166
+ // that is about to become it). `normalizeColumnIdentities` returns its input
167
+ // by reference when every entry is already canonical, so this comparison is
168
+ // also the "is there anything to do" test.
169
+ const columnsSource = Array.isArray(s.columns)
170
+ ? s.columns
171
+ : foldColumns
172
+ ? legacyFields
173
+ : undefined;
174
+ const foldedColumns = normalizeColumnIdentities(columnsSource);
175
+ const foldColumnIdentity = foldedColumns !== columnsSource;
176
+ if (!foldColumns && !foldRowHeight && !foldFilter && !legacyFlags.length &&
177
+ !foldDescription && !foldAria && !foldSharing && !defaultViewKind &&
178
+ !foldColumnIdentity) {
179
+ return schema;
180
+ }
181
+ const next = { ...s };
182
+ if (foldColumns) {
183
+ if (!Array.isArray(next.columns))
184
+ next.columns = legacyFields;
185
+ delete next.fields;
186
+ }
187
+ // After the `fields` → `columns` rename, so a legacy-spelled column LIST gets
188
+ // its per-entry identities canonicalized in the same pass.
189
+ if (foldColumnIdentity)
190
+ next.columns = foldedColumns;
191
+ if (foldRowHeight) {
192
+ if (typeof next.rowHeight !== 'string') {
193
+ next.rowHeight = DENSITY_MODE_TO_ROW_HEIGHT[legacyDensity];
194
+ }
195
+ delete next.densityMode;
196
+ }
197
+ if (foldFilter) {
198
+ if (!Array.isArray(next.filter))
199
+ next.filter = legacyFilters;
200
+ delete next.filters;
201
+ }
202
+ if (legacyFlags.length) {
203
+ const ua = { ...(isRecord(next.userActions) ? next.userActions : {}) };
204
+ for (const flag of legacyFlags) {
205
+ const key = SHOW_FLAG_TO_USER_ACTION[flag];
206
+ if (typeof ua[key] !== 'boolean')
207
+ ua[key] = s[flag];
208
+ delete next[flag];
209
+ }
210
+ next.userActions = ua;
211
+ }
212
+ if (foldDescription) {
213
+ const appearance = {
214
+ ...(isRecord(next.appearance) ? next.appearance : {}),
215
+ };
216
+ if (typeof appearance.showDescription !== 'boolean') {
217
+ appearance.showDescription = s.showDescription;
218
+ }
219
+ delete next.showDescription;
220
+ next.appearance = appearance;
221
+ }
222
+ if (foldAria && aria) {
223
+ const nextAria = { ...aria };
224
+ for (const [legacy, canonical] of Object.entries(ARIA_KEY_ALIASES)) {
225
+ if (nextAria[canonical] === undefined && aria[legacy] !== undefined) {
226
+ nextAria[canonical] = aria[legacy];
227
+ }
228
+ delete nextAria[legacy];
229
+ }
230
+ next.aria = nextAria;
231
+ }
232
+ if (foldSharing && sharing) {
233
+ const nextSharing = { ...sharing };
234
+ if (nextSharing.type === undefined) {
235
+ const visibility = typeof sharing.visibility === 'string' ? sharing.visibility : undefined;
236
+ // `enabled: true` with no audience is under-specified legacy input. It
237
+ // used to render the share badge titled "private", so it maps to
238
+ // `personal` — preserving what the user saw rather than adopting the
239
+ // spec's `collaborative` default and silently relabeling the badge.
240
+ const resolved = visibility
241
+ ? VISIBILITY_TO_SHARING_TYPE[visibility]
242
+ : sharing.enabled === true
243
+ ? 'personal'
244
+ : undefined;
245
+ if (resolved)
246
+ nextSharing.type = resolved;
247
+ }
248
+ delete nextSharing.visibility;
249
+ delete nextSharing.enabled;
250
+ next.sharing = nextSharing;
251
+ }
252
+ if (defaultViewKind)
253
+ next.viewType = 'grid';
254
+ return next;
255
+ }
@@ -0,0 +1,52 @@
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
+ /** A cell value reduced to something two rows can be ordered by. */
9
+ export type SortValue = string | number | boolean | null;
10
+ export interface SortValueOptions {
11
+ /**
12
+ * `id → label` map for relational columns whose cell value is still a RAW id
13
+ * because the surface resolves labels client-side instead of asking the
14
+ * server for `$expand` (RelatedList does exactly this). When the raw value
15
+ * has an entry, the label the cell renders becomes the sort key.
16
+ */
17
+ labels?: Record<string, string>;
18
+ }
19
+ /**
20
+ * Reduce one cell value to the primitive a client-side sort should order by.
21
+ *
22
+ * - empty (`null` / `undefined` / `''`) → `null`, so callers can place blanks
23
+ * consistently rather than sorting `undefined` against a string;
24
+ * - a `labels` hit (by the raw id) wins — that IS the string in the cell;
25
+ * - an expanded reference record resolves through {@link getRecordDisplayName},
26
+ * the ONE shared display-name resolver (ADR-0079), so the sort key and the
27
+ * lookup cell agree on which field names a record;
28
+ * - arrays join their resolved parts, mirroring how a multi-value cell reads;
29
+ * - primitives pass through unchanged (numbers stay numbers, so a number column
30
+ * still sorts numerically rather than lexicographically).
31
+ */
32
+ export declare function getSortValue(value: unknown, options?: SortValueOptions): SortValue;
33
+ /**
34
+ * Compare two {@link SortValue}s **ascending**.
35
+ *
36
+ * Callers negate the result for descending, which is also why blanks are placed
37
+ * last here rather than "always last": negating moves them to the front, the
38
+ * same convention the inline comparators this replaces already had.
39
+ *
40
+ * Same-typed values compare in their own domain (numbers numerically, booleans
41
+ * false-before-true); anything else falls back to a locale-aware string compare
42
+ * with `numeric: true`, so `Item 2` precedes `Item 10`.
43
+ */
44
+ export declare function compareSortValues(a: SortValue, b: SortValue): number;
45
+ /**
46
+ * Convenience wrapper: reduce both cell values and compare them ascending.
47
+ *
48
+ * Prefer resolving the sort key ONCE per row (decorate → sort → undecorate)
49
+ * when sorting a whole collection — `getRecordDisplayName` walks the record, so
50
+ * calling it inside the comparator repeats that walk `O(n log n)` times.
51
+ */
52
+ export declare function compareCellValues(a: unknown, b: unknown, options?: SortValueOptions): number;
@@ -0,0 +1,125 @@
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-values — reduce a *cell value* to the primitive a CLIENT-SIDE sort should
10
+ * order by, and compare two of those primitives (objectui#3096).
11
+ *
12
+ * Why this exists: a relational column (`lookup` / `master_detail` / `tree` /
13
+ * `user` — see `EXPANDABLE_FIELD_TYPES`) does not hold the string its cell
14
+ * shows. Depending on the surface it holds either the `$expand`-ed related
15
+ * RECORD (`{ id, name, … }`) or a raw foreign-key id whose label was resolved
16
+ * separately. Both break the naive comparators these surfaces used to inline:
17
+ *
18
+ * - `a[col] < b[col]` on two objects is ALWAYS false, so the comparator
19
+ * collapses to the constant `1` and the "sort" permutes rows into an order
20
+ * with no relation to anything on screen;
21
+ * - `String(a[col])` on an object is `"[object Object]"` — every row compares
22
+ * equal, so the sort silently does nothing;
23
+ * - comparing raw ids (`rec_7f3…`) orders a column of human-readable names by
24
+ * an opaque key, which reads to the user as "sorting is broken".
25
+ *
26
+ * Reducing the value to the label the cell renders makes a client-side sort
27
+ * order by what the user actually sees. This CANNOT fix a server `$orderby`,
28
+ * where the sort key is the stored id by construction — surfaces that sort on
29
+ * the server must instead not offer relational columns as sort keys (that half
30
+ * of #3096 lives at each entry point).
31
+ *
32
+ * Pure (no React / i18n), so every renderer package can share it.
33
+ */
34
+ import { getRecordDisplayName } from './record-title.js';
35
+ /**
36
+ * Reduce one cell value to the primitive a client-side sort should order by.
37
+ *
38
+ * - empty (`null` / `undefined` / `''`) → `null`, so callers can place blanks
39
+ * consistently rather than sorting `undefined` against a string;
40
+ * - a `labels` hit (by the raw id) wins — that IS the string in the cell;
41
+ * - an expanded reference record resolves through {@link getRecordDisplayName},
42
+ * the ONE shared display-name resolver (ADR-0079), so the sort key and the
43
+ * lookup cell agree on which field names a record;
44
+ * - arrays join their resolved parts, mirroring how a multi-value cell reads;
45
+ * - primitives pass through unchanged (numbers stay numbers, so a number column
46
+ * still sorts numerically rather than lexicographically).
47
+ */
48
+ export function getSortValue(value, options) {
49
+ if (value === null || value === undefined || value === '')
50
+ return null;
51
+ const labels = options?.labels;
52
+ if (typeof value === 'string') {
53
+ return labels?.[value] ?? value;
54
+ }
55
+ if (typeof value === 'number') {
56
+ if (!Number.isFinite(value))
57
+ return null;
58
+ // A numeric foreign key still deserves its resolved label.
59
+ return labels?.[String(value)] ?? value;
60
+ }
61
+ if (typeof value === 'boolean')
62
+ return value;
63
+ if (value instanceof Date) {
64
+ const time = value.getTime();
65
+ return Number.isNaN(time) ? null : time;
66
+ }
67
+ if (Array.isArray(value)) {
68
+ const parts = value
69
+ .map((entry) => getSortValue(entry, options))
70
+ .filter((entry) => entry !== null)
71
+ .map((entry) => String(entry));
72
+ return parts.length > 0 ? parts.join(', ') : null;
73
+ }
74
+ if (typeof value === 'object') {
75
+ const record = value;
76
+ // An expanded record can still carry a client-resolved label (the surface
77
+ // built its map from ids, the server expanded the same field).
78
+ const id = record.id ?? record._id;
79
+ if (id !== null && id !== undefined) {
80
+ const mapped = labels?.[String(id)];
81
+ if (mapped)
82
+ return mapped;
83
+ }
84
+ // `fallback: ''` so a record with nothing name-like reduces to `null`
85
+ // (sorted with the blanks) instead of the literal string "Untitled",
86
+ // which would bunch unrelated empty records under "U".
87
+ const name = getRecordDisplayName(undefined, record, { fallback: '' });
88
+ return name === '' ? null : name;
89
+ }
90
+ return String(value);
91
+ }
92
+ /**
93
+ * Compare two {@link SortValue}s **ascending**.
94
+ *
95
+ * Callers negate the result for descending, which is also why blanks are placed
96
+ * last here rather than "always last": negating moves them to the front, the
97
+ * same convention the inline comparators this replaces already had.
98
+ *
99
+ * Same-typed values compare in their own domain (numbers numerically, booleans
100
+ * false-before-true); anything else falls back to a locale-aware string compare
101
+ * with `numeric: true`, so `Item 2` precedes `Item 10`.
102
+ */
103
+ export function compareSortValues(a, b) {
104
+ if (a === null && b === null)
105
+ return 0;
106
+ if (a === null)
107
+ return 1;
108
+ if (b === null)
109
+ return -1;
110
+ if (typeof a === 'boolean' && typeof b === 'boolean')
111
+ return (a ? 1 : 0) - (b ? 1 : 0);
112
+ if (typeof a === 'number' && typeof b === 'number')
113
+ return a - b;
114
+ return String(a).localeCompare(String(b), undefined, { numeric: true });
115
+ }
116
+ /**
117
+ * Convenience wrapper: reduce both cell values and compare them ascending.
118
+ *
119
+ * Prefer resolving the sort key ONCE per row (decorate → sort → undecorate)
120
+ * when sorting a whole collection — `getRecordDisplayName` walks the record, so
121
+ * calling it inside the comparator repeats that walk `O(n log n)` times.
122
+ */
123
+ export function compareCellValues(a, b, options) {
124
+ return compareSortValues(getSortValue(a, options), getSortValue(b, options));
125
+ }
@@ -2,8 +2,17 @@
2
2
  * @object-ui/core - Validation Module
3
3
  *
4
4
  * Phase 3.5: Validation engine
5
- * ObjectStack Spec v2.0.1: Object-level validation
5
+ *
6
+ * Object-level validation. The rule vocabulary is owned by
7
+ * `@objectstack/spec/data` and derived in `@object-ui/types`; canonicity is
8
+ * carried by that derivation and its parity gate, not by this comment.
9
+ *
10
+ * The object-level rule ENGINE (`validators/`) is @deprecated (#3110) — the
11
+ * server is the single implementation of rule enforcement. Field/record SHAPE
12
+ * validation (`schema-validator.js`, `validation-engine.js`) is unaffected: it
13
+ * is client-side form validation, not a mirror of a server rule.
6
14
  */
15
+ export * from './required-presence.js';
7
16
  export * from './validation-engine.js';
8
17
  export * from './schema-validator.js';
9
18
  export * from './validators/index.js';
@@ -2,8 +2,17 @@
2
2
  * @object-ui/core - Validation Module
3
3
  *
4
4
  * Phase 3.5: Validation engine
5
- * ObjectStack Spec v2.0.1: Object-level validation
5
+ *
6
+ * Object-level validation. The rule vocabulary is owned by
7
+ * `@objectstack/spec/data` and derived in `@object-ui/types`; canonicity is
8
+ * carried by that derivation and its parity gate, not by this comment.
9
+ *
10
+ * The object-level rule ENGINE (`validators/`) is @deprecated (#3110) — the
11
+ * server is the single implementation of rule enforcement. Field/record SHAPE
12
+ * validation (`schema-validator.js`, `validation-engine.js`) is unaffected: it
13
+ * is client-side form validation, not a mirror of a server rule.
6
14
  */
15
+ export * from './required-presence.js';
7
16
  export * from './validation-engine.js';
8
17
  export * from './schema-validator.js';
9
18
  export * from './validators/index.js';
@@ -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
+ * The ONE client-side answer to "is this field's value absent?" for a
10
+ * `required` check.
11
+ *
12
+ * `required` is a PRESENCE contract, not a truthiness one. `@objectstack/spec`
13
+ * FieldSchema.required (ADR-0113) reads "an insert must provide a NON-NULL
14
+ * value", and objectql's record validator implements exactly that — its
15
+ * `isMissing` is `undefined | null | blank string`, nothing more. This mirrors
16
+ * that verdict so the form, the wizard's cross-step gate, and the server all
17
+ * agree instead of each inventing a dialect.
18
+ *
19
+ * What this deliberately does NOT treat as absent:
20
+ * - `false` — a boolean's second value. react-hook-form's own `required` rule
21
+ * fails on `isBoolean(v) && !v`, which quietly turns a required checkbox into
22
+ * "must be TRUE": an AI-built task tracker could not create an UNFINISHED
23
+ * task, because the state the switch shows by default was the one value it
24
+ * refused to save (cloud#972).
25
+ * - `0` — a number's identity element. "Zero hours logged" is data.
26
+ *
27
+ * An empty ARRAY *is* absent: the server has no array-valued scalar to
28
+ * disagree about, and "nothing picked" is a multiselect's own empty state,
29
+ * exactly as a blank string is a text input's.
30
+ */
31
+ export declare function isMissingForRequired(value: unknown): boolean;
@@ -0,0 +1,36 @@
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 ONE client-side answer to "is this field's value absent?" for a
10
+ * `required` check.
11
+ *
12
+ * `required` is a PRESENCE contract, not a truthiness one. `@objectstack/spec`
13
+ * FieldSchema.required (ADR-0113) reads "an insert must provide a NON-NULL
14
+ * value", and objectql's record validator implements exactly that — its
15
+ * `isMissing` is `undefined | null | blank string`, nothing more. This mirrors
16
+ * that verdict so the form, the wizard's cross-step gate, and the server all
17
+ * agree instead of each inventing a dialect.
18
+ *
19
+ * What this deliberately does NOT treat as absent:
20
+ * - `false` — a boolean's second value. react-hook-form's own `required` rule
21
+ * fails on `isBoolean(v) && !v`, which quietly turns a required checkbox into
22
+ * "must be TRUE": an AI-built task tracker could not create an UNFINISHED
23
+ * task, because the state the switch shows by default was the one value it
24
+ * refused to save (cloud#972).
25
+ * - `0` — a number's identity element. "Zero hours logged" is data.
26
+ *
27
+ * An empty ARRAY *is* absent: the server has no array-valued scalar to
28
+ * disagree about, and "nothing picked" is a multiselect's own empty state,
29
+ * exactly as a blank string is a text input's.
30
+ */
31
+ export function isMissingForRequired(value) {
32
+ return (value === undefined ||
33
+ value === null ||
34
+ (typeof value === 'string' && value.trim() === '') ||
35
+ (Array.isArray(value) && value.length === 0));
36
+ }
@@ -16,21 +16,37 @@
16
16
  */
17
17
  import type { BaseSchema } from '@object-ui/types';
18
18
  /**
19
- * Validation error details
19
+ * One issue found while walking an ObjectUI schema TREE — `path` locates the
20
+ * offending node (e.g. `children[0].type`), and `type` says whether it is fatal.
21
+ *
22
+ * NOT the spec's `ValidationError` (`@objectstack/spec/kernel`), whose name this
23
+ * interface carried until objectstack#4115. That one describes PLUGIN-manifest
24
+ * validation and is keyed by `field`, with no severity discriminant. The two
25
+ * share no key beyond `message`/`code`.
26
+ *
27
+ * Naming convention for this family, set here and followed by the rest of the
28
+ * burn-down: `<what-is-validated>Validation<Error|Result>`. (`SchemaValidationResult`
29
+ * and `SchemaValidationReport` are themselves spec exports — checked, not assumed.)
20
30
  */
21
- export interface ValidationError {
31
+ export interface SchemaNodeValidationError {
22
32
  path: string;
23
33
  message: string;
24
34
  type: 'error' | 'warning';
25
35
  code?: string;
26
36
  }
27
37
  /**
28
- * Validation result
38
+ * The outcome of validating an ObjectUI schema tree.
39
+ *
40
+ * NOT the spec's `ValidationResult` (`@objectstack/spec/kernel` and
41
+ * `@objectstack/spec/contracts`), whose name this interface carried until
42
+ * objectstack#4115: there `errors`/`warnings` are OPTIONAL arrays of
43
+ * `{ field, message, code? }` describing a plugin manifest. Here both arrays are
44
+ * required and carry node paths.
29
45
  */
30
- export interface ValidationResult {
46
+ export interface SchemaNodeValidationResult {
31
47
  valid: boolean;
32
- errors: ValidationError[];
33
- warnings: ValidationError[];
48
+ errors: SchemaNodeValidationError[];
49
+ warnings: SchemaNodeValidationError[];
34
50
  }
35
51
  /**
36
52
  * Validate a complete schema
@@ -53,7 +69,7 @@ export interface ValidationResult {
53
69
  * }
54
70
  * ```
55
71
  */
56
- export declare function validateSchema(schema: any, path?: string): ValidationResult;
72
+ export declare function validateSchema(schema: any, path?: string): SchemaNodeValidationResult;
57
73
  /**
58
74
  * Assert that a schema is valid, throwing an error if not
59
75
  *
@@ -91,4 +107,4 @@ export declare function isValidSchema(value: any): value is BaseSchema;
91
107
  * @param result - The validation result
92
108
  * @returns Formatted error summary
93
109
  */
94
- export declare function formatValidationErrors(result: ValidationResult): string;
110
+ export declare function formatValidationErrors(result: SchemaNodeValidationResult): string;
@@ -8,7 +8,13 @@
8
8
  /**
9
9
  * @object-ui/core - Validators
10
10
  *
11
- * ObjectStack Spec v2.0.1 compliant validators
11
+ * Publishes the @deprecated `ObjectValidationEngine` (#3110). Object-level
12
+ * validation rules are enforcement, and enforcement is single-implementation on
13
+ * the server (`objectql`'s rule-validator); objectui renders the server's
14
+ * rejection rather than predicting it, and pre-submit feedback belongs in a
15
+ * validate-only (dry-run) write. This barrel keeps the API exported for host
16
+ * applications that already depend on it — re-exporting is not wiring, and
17
+ * `validation-engine-stays-unwired.test.ts` enforces that distinction.
12
18
  *
13
19
  * @module validators
14
20
  * @packageDocumentation
@@ -8,7 +8,13 @@
8
8
  /**
9
9
  * @object-ui/core - Validators
10
10
  *
11
- * ObjectStack Spec v2.0.1 compliant validators
11
+ * Publishes the @deprecated `ObjectValidationEngine` (#3110). Object-level
12
+ * validation rules are enforcement, and enforcement is single-implementation on
13
+ * the server (`objectql`'s rule-validator); objectui renders the server's
14
+ * rejection rather than predicting it, and pre-submit feedback belongs in a
15
+ * validate-only (dry-run) write. This barrel keeps the API exported for host
16
+ * applications that already depend on it — re-exporting is not wiring, and
17
+ * `validation-engine-stays-unwired.test.ts` enforces that distinction.
12
18
  *
13
19
  * @module validators
14
20
  * @packageDocumentation