@object-ui/core 16.0.0 → 17.1.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 +1589 -1
- package/dist/actions/ActionRunner.d.ts +88 -16
- package/dist/actions/ActionRunner.js +164 -81
- package/dist/actions/actionKeys.d.ts +118 -0
- package/dist/actions/actionKeys.js +243 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/adapters/ApiDataSource.d.ts +11 -1
- package/dist/adapters/ApiDataSource.js +21 -1
- package/dist/adapters/ValueDataSource.d.ts +10 -2
- package/dist/adapters/ValueDataSource.js +9 -0
- package/dist/adapters/batchTransaction.d.ts +49 -0
- package/dist/adapters/batchTransaction.js +118 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +10 -0
- package/dist/evaluator/ExpressionEvaluator.js +48 -0
- package/dist/evaluator/SafeExpressionParser.js +5 -2
- package/dist/evaluator/fieldRules.d.ts +0 -2
- package/dist/evaluator/fieldRules.js +3 -2
- package/dist/evaluator/optionLint.js +5 -2
- package/dist/evaluator/optionRules.d.ts +35 -6
- package/dist/evaluator/optionRules.js +18 -23
- package/dist/index.d.ts +5 -0
- package/dist/index.js +8 -0
- package/dist/registry/Registry.d.ts +81 -2
- package/dist/registry/Registry.js +102 -2
- package/dist/registry/public-blocks.js +52 -1
- package/dist/theme/ThemeEngine.js +0 -12
- package/dist/utils/chart-series.d.ts +28 -0
- package/dist/utils/chart-series.js +55 -0
- package/dist/utils/column-identity.d.ts +110 -0
- package/dist/utils/column-identity.js +210 -0
- package/dist/utils/dashboard-filters.d.ts +2 -2
- package/dist/utils/dataset-format.d.ts +18 -1
- package/dist/utils/dataset-format.js +14 -1
- package/dist/utils/expand-fields.js +8 -8
- package/dist/utils/export-filename.js +4 -1
- package/dist/utils/filter-converter.d.ts +47 -0
- package/dist/utils/filter-converter.js +94 -7
- package/dist/utils/filter-tokens.d.ts +101 -0
- package/dist/utils/filter-tokens.js +173 -0
- package/dist/utils/managedBy.d.ts +119 -0
- package/dist/utils/managedBy.js +147 -0
- package/dist/utils/normalize-list-view.d.ts +99 -0
- package/dist/utils/normalize-list-view.js +255 -0
- package/dist/utils/record-title.js +0 -0
- package/dist/utils/sort-values.d.ts +52 -0
- package/dist/utils/sort-values.js +125 -0
- package/dist/validation/index.d.ts +9 -1
- package/dist/validation/index.js +9 -1
- package/dist/validation/validators/index.d.ts +7 -1
- package/dist/validation/validators/index.js +7 -1
- package/dist/validation/validators/object-validation-engine.d.ts +151 -31
- package/dist/validation/validators/object-validation-engine.js +275 -142
- package/package.json +4 -4
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
/** ListView's toolbar density vocabulary — three steps, not the spec's five. */
|
|
9
|
+
export type DensityMode = 'compact' | 'comfortable' | 'spacious';
|
|
10
|
+
/** The spec's `RowHeightSchema` vocabulary. */
|
|
11
|
+
export type RowHeight = 'compact' | 'short' | 'medium' | 'tall' | 'extra_tall';
|
|
12
|
+
/**
|
|
13
|
+
* `densityMode` → `rowHeight`. The two vocabularies are not the same size: the
|
|
14
|
+
* spec has five row heights, ListView's toolbar offers three. Widening maps
|
|
15
|
+
* each density onto the spec value the renderer already resolves it back to
|
|
16
|
+
* (see {@link ROW_HEIGHT_TO_DENSITY_MODE}), so a fold followed by a read is a
|
|
17
|
+
* round trip — `spacious` → `tall` → `spacious`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DENSITY_MODE_TO_ROW_HEIGHT: Record<DensityMode, RowHeight>;
|
|
20
|
+
/**
|
|
21
|
+
* `rowHeight` → `densityMode`, the read direction. Narrowing five values onto
|
|
22
|
+
* three is lossy by construction: `short` collapses into `compact` and
|
|
23
|
+
* `extra_tall` into `spacious`. Exported so the renderer and the persistence
|
|
24
|
+
* layer agree on the collapse instead of each hard-coding its own table.
|
|
25
|
+
*/
|
|
26
|
+
export declare const ROW_HEIGHT_TO_DENSITY_MODE: Record<RowHeight, DensityMode>;
|
|
27
|
+
/**
|
|
28
|
+
* Tolerant reader for {@link ROW_HEIGHT_TO_DENSITY_MODE}. View metadata is
|
|
29
|
+
* user-authored, so `rowHeight` is not guaranteed to be one of the five spec
|
|
30
|
+
* values — an unknown one lands on `comfortable` rather than rendering an
|
|
31
|
+
* undefined density. Keeping the fallback here means every surface collapses
|
|
32
|
+
* the same way.
|
|
33
|
+
*/
|
|
34
|
+
export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode;
|
|
35
|
+
/**
|
|
36
|
+
* ObjectUI's `list-view` node historically used a different vocabulary from
|
|
37
|
+
* `@objectstack/spec` for the same concepts (`fields` where the spec says
|
|
38
|
+
* `columns`, `viewType` where it says `type`, …). Issue #2231 closed the
|
|
39
|
+
* type-level fork; #2890 closes the vocabulary fork.
|
|
40
|
+
*
|
|
41
|
+
* Stored view metadata in user databases still carries the legacy keys, so the
|
|
42
|
+
* renderer cannot simply stop accepting them. Per AGENTS.md Commandment #0.1 the
|
|
43
|
+
* answer is NOT a per-read-site `??` fallback — those fossilize a second de-facto
|
|
44
|
+
* contract and drift apart (they already had: `ObjectGrid` preferred `columns`
|
|
45
|
+
* in one branch and `fields` in another). Instead legacy acceptance lives HERE,
|
|
46
|
+
* in one documented normalizer at the component boundary, mirroring
|
|
47
|
+
* `normalizeSchemaReferenceKeys` (object schemas) and the spec's own
|
|
48
|
+
* `normalizeVisibleWhen` / `normalizeFilterOperator` migration bridges.
|
|
49
|
+
*
|
|
50
|
+
* Note this cannot be a `z.preprocess` on `ListViewSchema`: nothing on the
|
|
51
|
+
* render path parses view metadata through zod (the zod schemas are used by the
|
|
52
|
+
* CLI validator, the VS Code extension and tests only), so a schema-level fold
|
|
53
|
+
* would never run. The guarantee comes from the call site instead — `ListView`
|
|
54
|
+
* normalizes before it reads anything.
|
|
55
|
+
*
|
|
56
|
+
* The fold is deliberately one-directional: the canonical key wins when both are
|
|
57
|
+
* present, and the legacy key is REMOVED from the result so a read-site that was
|
|
58
|
+
* missed fails loudly instead of quietly taking the legacy path. Like a spec
|
|
59
|
+
* migration bridge, this is expected to be dropped in a future major once stored
|
|
60
|
+
* metadata has been migrated.
|
|
61
|
+
*
|
|
62
|
+
* Non-mutating and allocation-frugal: returns the input by reference when there
|
|
63
|
+
* is nothing to fold, so `ListView`'s downstream `useMemo`s keep a stable
|
|
64
|
+
* dependency identity on the common (already-canonical) path.
|
|
65
|
+
*
|
|
66
|
+
* Currently folded:
|
|
67
|
+
* - `fields` → `columns` (#2890 scope A step 1)
|
|
68
|
+
* - `densityMode` → `rowHeight` (#2890 scope A step 2)
|
|
69
|
+
* - the `show*` toolbar flags → `userActions` (#2890 scope A step 3), and
|
|
70
|
+
* `showDescription` → `appearance.showDescription`. The canonical key wins
|
|
71
|
+
* per-flag, so a view may carry `userActions` for some toggles and a legacy
|
|
72
|
+
* flag for others. NOTE the fold does not apply any default: an absent flag
|
|
73
|
+
* stays absent, because the defaults are per-toggle (search/sort/filter/
|
|
74
|
+
* rowHeight/group default ON, hideFields/rowColor default OFF) and belong to
|
|
75
|
+
* the renderer, not to the vocabulary bridge.
|
|
76
|
+
* - `aria: { label, describedBy }` → the spec's `AriaProps`
|
|
77
|
+
* (`{ ariaLabel, ariaDescribedBy }`), and `sharing: { visibility, enabled }`
|
|
78
|
+
* → the spec's `ViewSharing` (`{ type }`) — #2890 scope A step 5. `aria.live`
|
|
79
|
+
* survives untouched: it has no spec counterpart.
|
|
80
|
+
* - `filters` → `filter` (#2890 scope A step 4). A key rename only: BOTH keys
|
|
81
|
+
* carry an ObjectQL FilterNode array (`[['stage','=','won']]`) everywhere in
|
|
82
|
+
* objectui — every consumer passes the value straight to `$filter`. The spec
|
|
83
|
+
* types `filter` as `ViewFilterRule[]` (`{field, operator, value}` objects),
|
|
84
|
+
* so objectui's field is typed from the spec but used as something else.
|
|
85
|
+
* That mismatch is real and out of scope here; converting formats inside a
|
|
86
|
+
* vocabulary fold would change what reaches the data source.
|
|
87
|
+
* - `viewType`: a missing kind, or the view CATEGORY `'list'` that AI-authored
|
|
88
|
+
* metadata stores and hosts forward verbatim, becomes the renderable `'grid'`
|
|
89
|
+
* — otherwise it reaches the renderer's typeless default branch and shows as
|
|
90
|
+
* a red "Unknown component type" box.
|
|
91
|
+
* - each `columns` entry's IDENTITY — `name` / `fieldName` → the spec's `field`
|
|
92
|
+
* (#3104). This one MIRRORS instead of deleting, for the reason given on
|
|
93
|
+
* {@link normalizeColumnIdentities}: `columns` entries cross the package
|
|
94
|
+
* boundary into host renderers, so dropping `name` from under them is a
|
|
95
|
+
* breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
|
|
96
|
+
* above, so a view that spells its column list the legacy way still gets its
|
|
97
|
+
* entries canonicalized.
|
|
98
|
+
*/
|
|
99
|
+
export declare function normalizeListViewSchema<T>(schema: T): T;
|
|
@@ -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
|
+
}
|
|
Binary file
|
|
@@ -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,7 +2,15 @@
|
|
|
2
2
|
* @object-ui/core - Validation Module
|
|
3
3
|
*
|
|
4
4
|
* Phase 3.5: Validation engine
|
|
5
|
-
*
|
|
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
|
*/
|
|
7
15
|
export * from './validation-engine.js';
|
|
8
16
|
export * from './schema-validator.js';
|
package/dist/validation/index.js
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
* @object-ui/core - Validation Module
|
|
3
3
|
*
|
|
4
4
|
* Phase 3.5: Validation engine
|
|
5
|
-
*
|
|
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
|
*/
|
|
7
15
|
export * from './validation-engine.js';
|
|
8
16
|
export * from './schema-validator.js';
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
/**
|
|
9
9
|
* @object-ui/core - Validators
|
|
10
10
|
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
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
|