@object-ui/core 17.1.0 → 17.3.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 +738 -0
- package/README.md +29 -0
- package/dist/actions/ActionEngine.d.ts +22 -7
- package/dist/actions/ActionEngine.js +40 -22
- package/dist/actions/ActionRunner.d.ts +133 -13
- package/dist/actions/ActionRunner.js +27 -4
- package/dist/actions/actionErrorDetail.d.ts +26 -0
- package/dist/actions/actionErrorDetail.js +37 -0
- package/dist/actions/actionKeys.d.ts +29 -12
- package/dist/actions/actionKeys.js +61 -10
- package/dist/actions/actionResponse.d.ts +42 -0
- package/dist/actions/actionResponse.js +104 -0
- package/dist/actions/index.d.ts +3 -0
- package/dist/actions/index.js +3 -0
- package/dist/actions/serverActionHandler.d.ts +143 -0
- package/dist/actions/serverActionHandler.js +137 -0
- package/dist/evaluator/ExpressionEvaluator.js +4 -2
- package/dist/evaluator/fieldRules.d.ts +26 -2
- package/dist/evaluator/fieldRules.js +63 -8
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +13 -1
- package/dist/evaluator/listConditional.js +0 -0
- package/dist/evaluator/optionRules.js +5 -1
- package/dist/evaluator/predicateInput.d.ts +84 -0
- package/dist/evaluator/predicateInput.js +72 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -0
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +8 -1
- 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 +18 -1
- package/dist/utils/dataset-format.js +10 -2
- package/dist/utils/dataset-pivot.d.ts +57 -0
- package/dist/utils/dataset-pivot.js +57 -0
- package/dist/utils/filter-converter.d.ts +39 -11
- package/dist/utils/filter-converter.js +87 -7
- 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/utils/predicate-fields.d.ts +96 -0
- package/dist/utils/predicate-fields.js +163 -0
- package/dist/utils/predicate-record.d.ts +32 -0
- package/dist/utils/predicate-record.js +120 -0
- package/dist/utils/record-title.js +0 -0
- package/dist/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
- package/dist/protocols/NotificationProtocol.d.ts +0 -71
- package/dist/protocols/NotificationProtocol.js +0 -89
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — predicate field harvesting
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Which record fields a view's PREDICATES read — the half of a view's field
|
|
10
|
+
* appetite that is not its columns.
|
|
11
|
+
*
|
|
12
|
+
* A list view projects what it DISPLAYS: `$select` is built from the column
|
|
13
|
+
* list (plus `id` and the expanded relation roots). That is correct for
|
|
14
|
+
* rendering and wrong for gating, because a `visible` / `disabled` /
|
|
15
|
+
* conditional-formatting predicate reads fields nobody put on screen — gating a
|
|
16
|
+
* row action on `record.owner` while showing Title / Status is the ordinary
|
|
17
|
+
* case, not an exotic one.
|
|
18
|
+
*
|
|
19
|
+
* The field then simply is not in the payload, and CEL does not treat an absent
|
|
20
|
+
* key as null: `record.owner == os.user.id` FAULTS with `No such key: owner`.
|
|
21
|
+
* Fault handling differs per surface and both answers are wrong — fail-CLOSED
|
|
22
|
+
* on the row kebab and the selection bar (the button silently disappears for
|
|
23
|
+
* everyone), fail-OPEN on the lenient paths (it appears for everyone). Nothing
|
|
24
|
+
* in either outcome points at a projection.
|
|
25
|
+
*
|
|
26
|
+
* Harvesting the names a predicate mentions and adding them to `$select` is
|
|
27
|
+
* what closes that. Note what it deliberately does NOT add: `$expand`. A
|
|
28
|
+
* predicate wants the stored foreign key, which is exactly what an unexpanded
|
|
29
|
+
* relation column already is — so a predicate-only relation field costs one
|
|
30
|
+
* more column and no extra round trip. (A relation that IS displayed still gets
|
|
31
|
+
* expanded for its label, and `toPredicateRecord` collapses it back for the
|
|
32
|
+
* predicate.)
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The columns the framework provisions on EVERY business object
|
|
36
|
+
* (`applySystemFields`) but does not publish in the object's `fields` metadata.
|
|
37
|
+
*
|
|
38
|
+
* They matter here because the most ordinary ownership predicate there is —
|
|
39
|
+
* `record.owner_id == os.user.id` — names one of them. Checking a harvested
|
|
40
|
+
* name against the declared fields alone would drop it, leaving the predicate
|
|
41
|
+
* with the same missing key the harvest exists to prevent.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately the columns OBSERVED on a live payload rather than
|
|
44
|
+
* `SYSTEM_MANAGED_FIELD_NAMES` (`@object-ui/types`), which is the broader
|
|
45
|
+
* DISPLAY-oriented set and carries legacy aliases (`_id`, `createdAt`,
|
|
46
|
+
* `modified`, `space`) that are not columns anywhere. An alias in `$select` is
|
|
47
|
+
* exactly the unknown key the guard exists to keep out.
|
|
48
|
+
*/
|
|
49
|
+
export const PLATFORM_RECORD_COLUMNS = new Set([
|
|
50
|
+
'id',
|
|
51
|
+
'created_at', 'created_by',
|
|
52
|
+
'updated_at', 'updated_by',
|
|
53
|
+
'owner_id', 'organization_id', 'owning_business_unit_id',
|
|
54
|
+
]);
|
|
55
|
+
/**
|
|
56
|
+
* Whether a harvested name is safe to put in a `$select` projection: either the
|
|
57
|
+
* object declares it, or it is one of the platform columns every object has.
|
|
58
|
+
*
|
|
59
|
+
* The check is not pedantry — an unknown key is not ignored by every backend
|
|
60
|
+
* (the cloud multi-tenant runtime answers with an EMPTY result set), so without
|
|
61
|
+
* it one typo'd predicate could blank a whole list.
|
|
62
|
+
*/
|
|
63
|
+
export function isProjectableField(name, declaredFields) {
|
|
64
|
+
if (PLATFORM_RECORD_COLUMNS.has(name))
|
|
65
|
+
return true;
|
|
66
|
+
return !!declaredFields && typeof declaredFields === 'object' && name in declaredFields;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `record.x` / `data.x` — the two record-scoped spellings every surface binds.
|
|
70
|
+
* A bare `x` is deliberately NOT harvested: at predicate scope a bare
|
|
71
|
+
* identifier is just as likely to be `features`, `os`, `user` or a CEL builtin,
|
|
72
|
+
* and there is no way to tell without resolving. Authors are already directed
|
|
73
|
+
* to the prefixed form on every surface (a bare field reference throws on the
|
|
74
|
+
* record-header path), so this covers the documented spelling.
|
|
75
|
+
*/
|
|
76
|
+
const RECORD_REF = /\b(?:record|data)\.([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
77
|
+
/** Pull the CEL source out of any of the shapes a predicate is authored in. */
|
|
78
|
+
function predicateSource(pred) {
|
|
79
|
+
if (typeof pred === 'string')
|
|
80
|
+
return pred.trim() || null;
|
|
81
|
+
if (pred && typeof pred === 'object') {
|
|
82
|
+
const src = pred.source;
|
|
83
|
+
if (typeof src === 'string')
|
|
84
|
+
return src.trim() || null;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Collect the record field names referenced by a set of predicates.
|
|
90
|
+
*
|
|
91
|
+
* Accepts predicates in every authored shape (bare CEL string, the
|
|
92
|
+
* `{ dialect, source }` envelope, `null`/`undefined`/boolean — the last three
|
|
93
|
+
* contribute nothing). Returns names in first-seen order, deduplicated.
|
|
94
|
+
*
|
|
95
|
+
* The result is a set of CANDIDATES, not verified fields: a typo'd predicate
|
|
96
|
+
* yields a name this object does not have. Callers must intersect it with the
|
|
97
|
+
* object's declared fields before putting it in a query — an unknown key in
|
|
98
|
+
* `$select` is not ignored by every backend (the cloud runtime answers with an
|
|
99
|
+
* empty result set), which would turn a typo into a blank list.
|
|
100
|
+
*/
|
|
101
|
+
export function collectPredicateFieldRefs(predicates) {
|
|
102
|
+
const out = [];
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
for (const pred of predicates) {
|
|
105
|
+
const source = predicateSource(pred);
|
|
106
|
+
if (!source)
|
|
107
|
+
continue;
|
|
108
|
+
RECORD_REF.lastIndex = 0;
|
|
109
|
+
for (let m = RECORD_REF.exec(source); m; m = RECORD_REF.exec(source)) {
|
|
110
|
+
const name = m[1];
|
|
111
|
+
if (seen.has(name))
|
|
112
|
+
continue;
|
|
113
|
+
seen.add(name);
|
|
114
|
+
out.push(name);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Every predicate a list-ish view carries, flattened for
|
|
121
|
+
* {@link collectPredicateFieldRefs}.
|
|
122
|
+
*
|
|
123
|
+
* One reader so the projection cannot fall behind the surfaces: each entry here
|
|
124
|
+
* is a place a predicate is actually evaluated against a row — conditional
|
|
125
|
+
* formatting, the row kebab (custom defs AND the built-in Edit/Delete
|
|
126
|
+
* overrides), the selection bar, and the object's declared actions.
|
|
127
|
+
*
|
|
128
|
+
* `objectActions` is deliberately the object's WHOLE action set rather than
|
|
129
|
+
* only the ones this view promotes. Narrowing it would mean re-running the
|
|
130
|
+
* name resolution `resolveLegacyRowActions` / `resolveBulkActions` already do,
|
|
131
|
+
* at a point in the fetch where the view's `rowActions` / `bulkActions` lists
|
|
132
|
+
* have not been folded yet — and the two directions of error are not
|
|
133
|
+
* symmetric: an extra column costs bytes, a missing one silently breaks the
|
|
134
|
+
* predicate that needed it.
|
|
135
|
+
*/
|
|
136
|
+
export function listViewPredicates(view) {
|
|
137
|
+
const preds = [];
|
|
138
|
+
for (const rule of view.conditionalFormatting ?? []) {
|
|
139
|
+
if (!rule || typeof rule !== 'object')
|
|
140
|
+
continue;
|
|
141
|
+
const r = rule;
|
|
142
|
+
preds.push(r.condition, r.expression);
|
|
143
|
+
// The native `{ field, operator, value }` shape names its field directly;
|
|
144
|
+
// spell it as a `record.` reference so the one harvester handles it.
|
|
145
|
+
if (typeof r.field === 'string' && r.field)
|
|
146
|
+
preds.push(`record.${r.field}`);
|
|
147
|
+
}
|
|
148
|
+
for (const list of [view.rowActionDefs, view.bulkActionDefs, view.objectActions]) {
|
|
149
|
+
for (const def of list ?? []) {
|
|
150
|
+
if (!def || typeof def !== 'object')
|
|
151
|
+
continue;
|
|
152
|
+
const d = def;
|
|
153
|
+
preds.push(d.visible, d.disabled);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const override of Object.values(view.userActions ?? {})) {
|
|
157
|
+
if (!override || typeof override !== 'object')
|
|
158
|
+
continue;
|
|
159
|
+
const o = override;
|
|
160
|
+
preds.push(o.visibleWhen, o.disabledWhen);
|
|
161
|
+
}
|
|
162
|
+
return preds;
|
|
163
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — predicate record normalization
|
|
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
|
+
/** The two field-container shapes the metadata API serves. */
|
|
9
|
+
export type FieldContainerLike = Record<string, {
|
|
10
|
+
type?: unknown;
|
|
11
|
+
} | unknown> | readonly {
|
|
12
|
+
name?: unknown;
|
|
13
|
+
type?: unknown;
|
|
14
|
+
}[] | null | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Collapse `record`'s expanded relation values back to the ids they stand for.
|
|
17
|
+
*
|
|
18
|
+
* Returns the input BY REFERENCE when there is nothing to collapse — the common
|
|
19
|
+
* case (no schema, no relations, nothing expanded) allocates nothing and keeps
|
|
20
|
+
* the identity downstream `useMemo` deps rely on.
|
|
21
|
+
*
|
|
22
|
+
* `multiple: true` relations store an ARRAY of ids and expand element-wise, so
|
|
23
|
+
* an array is mapped element-wise too; a member that is not an expanded record
|
|
24
|
+
* (already an id) is left alone.
|
|
25
|
+
*
|
|
26
|
+
* @param record A fetched row / detail record, as the payload delivered it.
|
|
27
|
+
* @param fields The object's field definitions (`objectSchema.fields`), in
|
|
28
|
+
* either served shape. Without them nothing is collapsed —
|
|
29
|
+
* a caller that cannot name the relations must not guess.
|
|
30
|
+
*/
|
|
31
|
+
export declare function toPredicateRecord<T extends Record<string, unknown>>(record: T, fields: FieldContainerLike): T;
|
|
32
|
+
export declare function toPredicateRecord<T extends Record<string, unknown>>(record: T | null | undefined, fields: FieldContainerLike): T | null | undefined;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — predicate record normalization
|
|
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
|
+
* Bind a fetched record into an expression scope the way the SERVER binds it —
|
|
10
|
+
* a relation field is its stored foreign key, never the record it points at.
|
|
11
|
+
*
|
|
12
|
+
* ## Why a normalization step exists at all
|
|
13
|
+
*
|
|
14
|
+
* A `lookup` / `master_detail` / `user` / `tree` field stores an id. That is
|
|
15
|
+
* what the server's CEL engine sees in a validation rule, a sharing predicate
|
|
16
|
+
* or a flow condition, and it is what an author writes:
|
|
17
|
+
*
|
|
18
|
+
* ```
|
|
19
|
+
* visible: 'record.owner == os.user.id'
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The client does not necessarily see that. `$expand` (the `populate=` wire
|
|
23
|
+
* param) **replaces the id in place** with the whole related record, so the same
|
|
24
|
+
* field arrives as `'P1'` or as `{ id: 'P1', name: 'Website' }` depending on
|
|
25
|
+
* whether the surface happened to expand it — and views expand exactly the
|
|
26
|
+
* relations they show as COLUMNS (`buildExpandFields`), which is a display
|
|
27
|
+
* decision no predicate author participates in. Measured against a live
|
|
28
|
+
* runtime, one predicate over one field therefore has four different fates:
|
|
29
|
+
*
|
|
30
|
+
* | value in the payload | `record.owner == "P1"` | `record.owner.id == "P1"` |
|
|
31
|
+
* |---------------------------------|------------------------|---------------------------|
|
|
32
|
+
* | `'P1'` (not expanded) | **true** | fault |
|
|
33
|
+
* | `{ id: 'P1', … }` (expanded) | **false** | true |
|
|
34
|
+
* | `null` (empty lookup) | false | fault |
|
|
35
|
+
* | absent (outside `$select`) | fault | fault |
|
|
36
|
+
*
|
|
37
|
+
* No spelling is right everywhere, and neither failure announces itself: a
|
|
38
|
+
* fault is fail-CLOSED on the row kebab / selection bar (button silently gone)
|
|
39
|
+
* and fail-OPEN on the lenient paths (button shown to everyone). That is the
|
|
40
|
+
* whole of "lookup fields cannot be used in expressions" — the author is not
|
|
41
|
+
* writing the predicate wrong, the operand means two things.
|
|
42
|
+
*
|
|
43
|
+
* Collapsing the expanded form back to its id is what makes the first column
|
|
44
|
+
* of that table the only one: `record.owner` is the id on every surface and on
|
|
45
|
+
* the server, so a predicate authored once reaches one verdict everywhere.
|
|
46
|
+
*
|
|
47
|
+
* ## Why it is schema-driven and not a shape sniff
|
|
48
|
+
*
|
|
49
|
+
* "An object carrying an `id` key" would also catch a genuine `json` field
|
|
50
|
+
* whose payload happens to have one, and silently change what
|
|
51
|
+
* `record.config.id` means. The object's own field types are the authority:
|
|
52
|
+
* only a field DECLARED relational (see `EXPANDABLE_FIELD_TYPES` — the same set
|
|
53
|
+
* that decides what gets expanded in the first place) is collapsed, so the two
|
|
54
|
+
* halves cannot disagree about which fields are relations.
|
|
55
|
+
*/
|
|
56
|
+
import { EXPANDABLE_FIELD_TYPES } from './expand-fields.js';
|
|
57
|
+
/** Names of the fields this object declares as relations (`lookup` / …). */
|
|
58
|
+
function relationFieldNames(fields) {
|
|
59
|
+
if (!fields || typeof fields !== 'object')
|
|
60
|
+
return null;
|
|
61
|
+
const out = new Set();
|
|
62
|
+
if (Array.isArray(fields)) {
|
|
63
|
+
for (const def of fields) {
|
|
64
|
+
const name = def?.name;
|
|
65
|
+
const type = def?.type;
|
|
66
|
+
if (typeof name === 'string' && EXPANDABLE_FIELD_TYPES.has(type))
|
|
67
|
+
out.add(name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
72
|
+
const type = def?.type;
|
|
73
|
+
if (EXPANDABLE_FIELD_TYPES.has(type))
|
|
74
|
+
out.add(name);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out.size > 0 ? out : null;
|
|
78
|
+
}
|
|
79
|
+
/** The id an expanded related record is stored under, or `undefined`. */
|
|
80
|
+
function expandedId(value) {
|
|
81
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
82
|
+
return undefined;
|
|
83
|
+
const rec = value;
|
|
84
|
+
const id = rec.id ?? rec._id;
|
|
85
|
+
return typeof id === 'string' || typeof id === 'number' ? String(id) : undefined;
|
|
86
|
+
}
|
|
87
|
+
// Two overloads rather than one `: T`: a null/undefined record is passed
|
|
88
|
+
// STRAIGHT THROUGH (a caller with no record loaded yet must be able to tell
|
|
89
|
+
// that from an empty one — `page:header` binds the result into an eval scope
|
|
90
|
+
// where `{}` and `undefined` are different answers). Declaring the return as
|
|
91
|
+
// plain `T` would have been a lie the `as T` cast hid.
|
|
92
|
+
export function toPredicateRecord(record, fields) {
|
|
93
|
+
if (!record || typeof record !== 'object')
|
|
94
|
+
return record;
|
|
95
|
+
const relations = relationFieldNames(fields);
|
|
96
|
+
if (!relations)
|
|
97
|
+
return record;
|
|
98
|
+
let out = null;
|
|
99
|
+
const write = (key, value) => {
|
|
100
|
+
if (!out)
|
|
101
|
+
out = { ...record };
|
|
102
|
+
out[key] = value;
|
|
103
|
+
};
|
|
104
|
+
for (const name of relations) {
|
|
105
|
+
if (!(name in record))
|
|
106
|
+
continue;
|
|
107
|
+
const value = record[name];
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
// Only rebuild the array when at least one member was expanded.
|
|
110
|
+
if (!value.some(v => expandedId(v) !== undefined))
|
|
111
|
+
continue;
|
|
112
|
+
write(name, value.map(v => expandedId(v) ?? v));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const id = expandedId(value);
|
|
116
|
+
if (id !== undefined)
|
|
117
|
+
write(name, id);
|
|
118
|
+
}
|
|
119
|
+
return (out ?? record);
|
|
120
|
+
}
|
|
Binary file
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* validation (`schema-validator.js`, `validation-engine.js`) is unaffected: it
|
|
13
13
|
* is client-side form validation, not a mirror of a server rule.
|
|
14
14
|
*/
|
|
15
|
+
export * from './required-presence.js';
|
|
15
16
|
export * from './validation-engine.js';
|
|
16
17
|
export * from './schema-validator.js';
|
|
17
18
|
export * from './validators/index.js';
|
package/dist/validation/index.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* validation (`schema-validator.js`, `validation-engine.js`) is unaffected: it
|
|
13
13
|
* is client-side form validation, not a mirror of a server rule.
|
|
14
14
|
*/
|
|
15
|
+
export * from './required-presence.js';
|
|
15
16
|
export * from './validation-engine.js';
|
|
16
17
|
export * from './schema-validator.js';
|
|
17
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
46
|
+
export interface SchemaNodeValidationResult {
|
|
31
47
|
valid: boolean;
|
|
32
|
-
errors:
|
|
33
|
-
warnings:
|
|
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):
|
|
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:
|
|
110
|
+
export declare function formatValidationErrors(result: SchemaNodeValidationResult): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@objectstack/formula": "^17.0.0-rc.
|
|
29
|
-
"@objectstack/spec": "^17.0.0-rc.
|
|
28
|
+
"@objectstack/formula": "^17.0.0-rc.2",
|
|
29
|
+
"@objectstack/spec": "^17.0.0-rc.2",
|
|
30
30
|
"lodash": "^4.18.1",
|
|
31
31
|
"zod": "^4.4.3",
|
|
32
|
-
"@object-ui/types": "17.
|
|
32
|
+
"@object-ui/types": "17.3.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "^6.0.3",
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* @object-ui/core - Notification Protocol Bridge
|
|
10
|
-
*
|
|
11
|
-
* Converts spec-aligned Notification schemas into toast-compatible
|
|
12
|
-
* objects that UI layers can render. Maps severity to variant,
|
|
13
|
-
* position names, and resolves default notification configs.
|
|
14
|
-
*
|
|
15
|
-
* @module protocols/NotificationProtocol
|
|
16
|
-
* @packageDocumentation
|
|
17
|
-
*/
|
|
18
|
-
import type { Notification as SpecNotification, NotificationConfig, NotificationPosition } from '@object-ui/types';
|
|
19
|
-
/** Fully resolved notification configuration. */
|
|
20
|
-
export interface ResolvedNotificationConfig {
|
|
21
|
-
defaultPosition: NotificationPosition;
|
|
22
|
-
defaultDuration: number;
|
|
23
|
-
maxVisible: number;
|
|
24
|
-
stackDirection: 'up' | 'down';
|
|
25
|
-
pauseOnHover: boolean;
|
|
26
|
-
}
|
|
27
|
-
/** Toast-compatible representation of a spec Notification. */
|
|
28
|
-
export interface ToastNotification {
|
|
29
|
-
title?: string;
|
|
30
|
-
description: string;
|
|
31
|
-
variant: string;
|
|
32
|
-
position: string;
|
|
33
|
-
duration: number;
|
|
34
|
-
dismissible: boolean;
|
|
35
|
-
actions: ToastAction[];
|
|
36
|
-
}
|
|
37
|
-
/** Toast-compatible action button. */
|
|
38
|
-
export interface ToastAction {
|
|
39
|
-
label: string;
|
|
40
|
-
action: string;
|
|
41
|
-
variant: 'primary' | 'secondary' | 'link';
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Map a spec notification severity to a toast variant string.
|
|
45
|
-
*
|
|
46
|
-
* @param severity - Spec severity (info, success, warning, error)
|
|
47
|
-
* @returns Toast variant (default, success, warning, destructive)
|
|
48
|
-
*/
|
|
49
|
-
export declare function mapSeverityToVariant(severity: string): string;
|
|
50
|
-
/**
|
|
51
|
-
* Map a spec notification position (underscore-separated) to a
|
|
52
|
-
* toast position string (hyphen-separated).
|
|
53
|
-
*
|
|
54
|
-
* @param position - Spec position (e.g. "top_right")
|
|
55
|
-
* @returns Toast position (e.g. "top-right")
|
|
56
|
-
*/
|
|
57
|
-
export declare function mapPosition(position: string): string;
|
|
58
|
-
/**
|
|
59
|
-
* Resolve a notification configuration by applying spec defaults.
|
|
60
|
-
*
|
|
61
|
-
* @param config - NotificationConfig from the spec
|
|
62
|
-
* @returns Fully resolved notification configuration
|
|
63
|
-
*/
|
|
64
|
-
export declare function resolveNotificationConfig(config: NotificationConfig): ResolvedNotificationConfig;
|
|
65
|
-
/**
|
|
66
|
-
* Convert a spec Notification to a toast-compatible object.
|
|
67
|
-
*
|
|
68
|
-
* @param notification - Spec Notification
|
|
69
|
-
* @returns Toast-compatible notification with title, description, variant, etc.
|
|
70
|
-
*/
|
|
71
|
-
export declare function specNotificationToToast(notification: SpecNotification): ToastNotification;
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
// ============================================================================
|
|
9
|
-
// Severity → Variant Mapping
|
|
10
|
-
// ============================================================================
|
|
11
|
-
const SEVERITY_TO_VARIANT = {
|
|
12
|
-
info: 'default',
|
|
13
|
-
success: 'success',
|
|
14
|
-
warning: 'warning',
|
|
15
|
-
error: 'destructive',
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Map a spec notification severity to a toast variant string.
|
|
19
|
-
*
|
|
20
|
-
* @param severity - Spec severity (info, success, warning, error)
|
|
21
|
-
* @returns Toast variant (default, success, warning, destructive)
|
|
22
|
-
*/
|
|
23
|
-
export function mapSeverityToVariant(severity) {
|
|
24
|
-
return SEVERITY_TO_VARIANT[severity] ?? 'default';
|
|
25
|
-
}
|
|
26
|
-
// ============================================================================
|
|
27
|
-
// Position Mapping
|
|
28
|
-
// ============================================================================
|
|
29
|
-
const POSITION_MAP = {
|
|
30
|
-
top_left: 'top-left',
|
|
31
|
-
top_center: 'top-center',
|
|
32
|
-
top_right: 'top-right',
|
|
33
|
-
bottom_left: 'bottom-left',
|
|
34
|
-
bottom_center: 'bottom-center',
|
|
35
|
-
bottom_right: 'bottom-right',
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* Map a spec notification position (underscore-separated) to a
|
|
39
|
-
* toast position string (hyphen-separated).
|
|
40
|
-
*
|
|
41
|
-
* @param position - Spec position (e.g. "top_right")
|
|
42
|
-
* @returns Toast position (e.g. "top-right")
|
|
43
|
-
*/
|
|
44
|
-
export function mapPosition(position) {
|
|
45
|
-
return POSITION_MAP[position] ?? 'top-right';
|
|
46
|
-
}
|
|
47
|
-
// ============================================================================
|
|
48
|
-
// Notification Config Resolution
|
|
49
|
-
// ============================================================================
|
|
50
|
-
/**
|
|
51
|
-
* Resolve a notification configuration by applying spec defaults.
|
|
52
|
-
*
|
|
53
|
-
* @param config - NotificationConfig from the spec
|
|
54
|
-
* @returns Fully resolved notification configuration
|
|
55
|
-
*/
|
|
56
|
-
export function resolveNotificationConfig(config) {
|
|
57
|
-
return {
|
|
58
|
-
defaultPosition: config.defaultPosition ?? 'top_right',
|
|
59
|
-
defaultDuration: config.defaultDuration ?? 5000,
|
|
60
|
-
maxVisible: config.maxVisible ?? 5,
|
|
61
|
-
stackDirection: config.stackDirection ?? 'down',
|
|
62
|
-
pauseOnHover: config.pauseOnHover ?? true,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
// ============================================================================
|
|
66
|
-
// Spec Notification → Toast
|
|
67
|
-
// ============================================================================
|
|
68
|
-
/**
|
|
69
|
-
* Convert a spec Notification to a toast-compatible object.
|
|
70
|
-
*
|
|
71
|
-
* @param notification - Spec Notification
|
|
72
|
-
* @returns Toast-compatible notification with title, description, variant, etc.
|
|
73
|
-
*/
|
|
74
|
-
export function specNotificationToToast(notification) {
|
|
75
|
-
const actions = (notification.actions ?? []).map((a) => ({
|
|
76
|
-
label: a.label,
|
|
77
|
-
action: a.action,
|
|
78
|
-
variant: a.variant ?? 'primary',
|
|
79
|
-
}));
|
|
80
|
-
return {
|
|
81
|
-
title: notification.title,
|
|
82
|
-
description: notification.message ?? '',
|
|
83
|
-
variant: mapSeverityToVariant(notification.severity ?? 'info'),
|
|
84
|
-
position: mapPosition(notification.position ?? 'top_right'),
|
|
85
|
-
duration: notification.duration ?? 5000,
|
|
86
|
-
dismissible: notification.dismissible ?? true,
|
|
87
|
-
actions,
|
|
88
|
-
};
|
|
89
|
-
}
|