@object-ui/react 17.3.0 → 17.6.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 +1242 -0
- package/README.md +48 -21
- package/dist/LazyPluginLoader.d.ts +14 -7
- package/dist/LazyPluginLoader.js +14 -7
- package/dist/SchemaRenderer.d.ts +78 -5
- package/dist/SchemaRenderer.js +572 -34
- package/dist/context/AppShellContext.d.ts +21 -2
- package/dist/context/HostNavigationContext.d.ts +82 -0
- package/dist/context/HostNavigationContext.js +65 -0
- package/dist/context/RecordContext.d.ts +0 -4
- package/dist/context/RecordContext.js +0 -2
- package/dist/context/RelatedRecordActionsContext.d.ts +44 -0
- package/dist/context/RelatedRecordActionsContext.js +6 -0
- package/dist/context/index.d.ts +13 -12
- package/dist/context/index.js +13 -12
- package/dist/element-data-source/ElementDataSourceGate.d.ts +272 -0
- package/dist/element-data-source/ElementDataSourceGate.js +274 -0
- package/dist/hooks/index.d.ts +35 -32
- package/dist/hooks/index.js +42 -32
- package/dist/hooks/useActionEngine.js +1 -1
- package/dist/hooks/useActionTextLocalizer.d.ts +69 -0
- package/dist/hooks/useActionTextLocalizer.js +91 -0
- package/dist/hooks/useCapabilityGate.js +6 -5
- package/dist/hooks/useClientNotifications.js +2 -2
- package/dist/hooks/useCrudShortcuts.js +1 -1
- package/dist/hooks/useDatasetDimensionLabels.d.ts +104 -0
- package/dist/hooks/useDatasetDimensionLabels.js +154 -0
- package/dist/hooks/useDiscovery.js +1 -1
- package/dist/hooks/useElementDataSource.d.ts +68 -0
- package/dist/hooks/useElementDataSource.js +141 -0
- package/dist/hooks/useExpression.d.ts +82 -3
- package/dist/hooks/useExpression.js +121 -4
- package/dist/hooks/useNavigationOverlay.d.ts +59 -11
- package/dist/hooks/useOffline.d.ts +62 -36
- package/dist/hooks/useOffline.js +2 -2
- package/dist/hooks/usePageAssignment.js +5 -5
- package/dist/hooks/usePageTransition.js +1 -1
- package/dist/hooks/usePageVariableActionBridge.js +2 -2
- package/dist/hooks/useViewData.js +1 -1
- package/dist/hooks/useViewTransition.js +1 -1
- package/dist/index.d.ts +11 -9
- package/dist/index.js +12 -8
- package/dist/schema-input.d.ts +42 -0
- package/dist/schema-input.js +42 -0
- package/dist/spec-bridge/SpecBridge.d.ts +12 -6
- package/dist/spec-bridge/SpecBridge.js +9 -3
- package/dist/spec-bridge/bridges/form-view.d.ts +99 -23
- package/dist/spec-bridge/bridges/form-view.js +51 -7
- package/dist/spec-bridge/bridges/list-view.d.ts +1 -1
- package/dist/spec-bridge/bridges/list-view.js +143 -24
- package/dist/spec-bridge/index.d.ts +4 -4
- package/dist/spec-bridge/index.js +3 -3
- package/dist/spec-bridge/types.d.ts +24 -3
- package/dist/utils/error-message.d.ts +59 -0
- package/dist/utils/error-message.js +116 -1
- package/dist/utils/i18n.d.ts +43 -8
- package/dist/utils/i18n.js +42 -4
- package/dist/utils/unevaluatedExpression.d.ts +69 -0
- package/dist/utils/unevaluatedExpression.js +194 -0
- package/dist/utils/visibilityDiagnostic.d.ts +119 -0
- package/dist/utils/visibilityDiagnostic.js +284 -0
- package/package.json +8 -8
|
@@ -5,18 +5,24 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
9
|
-
import type { BridgeContext, BridgeFn } from './types';
|
|
8
|
+
import type { BaseSchema } from '@object-ui/types';
|
|
9
|
+
import type { BridgeContext, BridgeFn } from './types.js';
|
|
10
10
|
export declare class SpecBridge {
|
|
11
11
|
private bridges;
|
|
12
12
|
private context;
|
|
13
13
|
constructor(context?: BridgeContext);
|
|
14
14
|
register(specType: string, bridge: BridgeFn): void;
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Transform a spec schema into a schema node tree.
|
|
17
|
+
*
|
|
18
|
+
* Returns `BaseSchema` rather than `SchemaNode` — see {@link BridgeFn} for
|
|
19
|
+
* the ruling (objectui#4580 Q4-B) and the measurement behind it. A registered
|
|
20
|
+
* bridge always produces an object; the union only made callers narrow.
|
|
21
|
+
*/
|
|
22
|
+
transform(specType: string, spec: any): BaseSchema;
|
|
17
23
|
/** Transform a ListView spec */
|
|
18
|
-
transformListView(spec: any):
|
|
24
|
+
transformListView(spec: any): BaseSchema;
|
|
19
25
|
/** Transform a FormView spec */
|
|
20
|
-
transformFormView(spec: any):
|
|
26
|
+
transformFormView(spec: any): BaseSchema;
|
|
21
27
|
updateContext(ctx: Partial<BridgeContext>): void;
|
|
22
28
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
-
import { bridgeListView } from './bridges/list-view';
|
|
9
|
-
import { bridgeFormView } from './bridges/form-view';
|
|
8
|
+
import { bridgeListView } from './bridges/list-view.js';
|
|
9
|
+
import { bridgeFormView } from './bridges/form-view.js';
|
|
10
10
|
export class SpecBridge {
|
|
11
11
|
constructor(context = {}) {
|
|
12
12
|
Object.defineProperty(this, "bridges", {
|
|
@@ -28,7 +28,13 @@ export class SpecBridge {
|
|
|
28
28
|
register(specType, bridge) {
|
|
29
29
|
this.bridges.set(specType, bridge);
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Transform a spec schema into a schema node tree.
|
|
33
|
+
*
|
|
34
|
+
* Returns `BaseSchema` rather than `SchemaNode` — see {@link BridgeFn} for
|
|
35
|
+
* the ruling (objectui#4580 Q4-B) and the measurement behind it. A registered
|
|
36
|
+
* bridge always produces an object; the union only made callers narrow.
|
|
37
|
+
*/
|
|
32
38
|
transform(specType, spec) {
|
|
33
39
|
const bridge = this.bridges.get(specType);
|
|
34
40
|
if (!bridge) {
|
|
@@ -5,8 +5,68 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Form-view bridge input — the documented SUBSET of `@objectstack/spec`'s
|
|
10
|
+
* FormViewSchema this bridge consumes, with every drift-prone key's TYPE bound
|
|
11
|
+
* to the contract instead of restated (objectui#5652).
|
|
12
|
+
*
|
|
13
|
+
* The subset stays hand-listed on purpose: this declaration is load-bearing
|
|
14
|
+
* documentation, and its NON-declarations are a retirement ledger
|
|
15
|
+
* (`defaultSort` / `aria`, objectui#3901 / #3974) that a blanket
|
|
16
|
+
* `Omit<FormView, ...>` would erase. What was never deliberate is the TYPE each
|
|
17
|
+
* declared key was given. Three of them had drifted from what the contract
|
|
18
|
+
* accepts, and nothing ever compared the two descriptions — the mirror shape
|
|
19
|
+
* `scripts/check-spec-symbol-derivation.mjs` exists to catch. The sibling
|
|
20
|
+
* bridge in this directory (`list-view.ts`) was converted the same way and for
|
|
21
|
+
* the same reason (objectui#2231): the shape derives, so it cannot drift.
|
|
22
|
+
*
|
|
23
|
+
* ## What the contract actually answers
|
|
24
|
+
*
|
|
25
|
+
* Measured with `safeParse` against the installed `@objectstack/spec` while
|
|
26
|
+
* fixing this, NOT copied from the finding — two of its three rows were wrong,
|
|
27
|
+
* in ways that change the repair:
|
|
28
|
+
*
|
|
29
|
+
* FormSection.columns `1 | 2 | 3 | 4 | '1' | '2' | '3' | '4'`. The string
|
|
30
|
+
* arm IS admitted, and the section schema's own pipe
|
|
31
|
+
* folds it to a number. The bridge declared `number`
|
|
32
|
+
* and refused it.
|
|
33
|
+
* FormView.columns `number`, with NO string arm: `FormViewSchema`
|
|
34
|
+
* rejects `columns: '3'` at the FORM level while
|
|
35
|
+
* `FormSectionSchema` accepts it at the SECTION level.
|
|
36
|
+
* The bridge's `number` was already right here — it is
|
|
37
|
+
* bound to the contract so the asymmetry cannot rot,
|
|
38
|
+
* and the two answers are pinned side by side.
|
|
39
|
+
* FormField.dependsOn a BARE parent-field name. The array arm is rejected
|
|
40
|
+
* by the contract outright. The bridge declared
|
|
41
|
+
* `string[]`, so it admitted only the arm the contract
|
|
42
|
+
* refuses and refused the only arm it admits — the one
|
|
43
|
+
* configuration that makes `field-selector` and
|
|
44
|
+
* `dynamic-config` work (objectui#5040).
|
|
45
|
+
* *.visibleWhen `string | { dialect, source?, ast?, meta? }`, with
|
|
46
|
+
* `dialect` REQUIRED and enum-typed. `{ dialect?,
|
|
47
|
+
* source }` is a DIFFERENT layer's type — the
|
|
48
|
+
* evaluator's `FieldRulePredicate` (`@object-ui/core`,
|
|
49
|
+
* ADR-0089) — and the contract does not accept it: an
|
|
50
|
+
* object arm without `dialect` is refused. The bridge
|
|
51
|
+
* declared `string` and refused the object arm
|
|
52
|
+
* entirely.
|
|
53
|
+
*
|
|
54
|
+
* ## Declaring an arm is not consuming it
|
|
55
|
+
*
|
|
56
|
+
* A key that is declarable but dropped on the way out is the same defect one
|
|
57
|
+
* layer over (objectui#5542 / #5594), so each widened arm is followed all the
|
|
58
|
+
* way onto the `object-form` node by `FormViewWidenedArms.test.ts`, and the one
|
|
59
|
+
* arm that needs a translation to get there gets it — see `normalizeColumns`.
|
|
60
|
+
*/
|
|
61
|
+
import type { DependsOnInput } from '@object-ui/types';
|
|
62
|
+
import type { FormFieldInput, FormSection, FormView } from '@objectstack/spec/ui';
|
|
63
|
+
import type { BridgeFn } from '../types.js';
|
|
64
|
+
/**
|
|
65
|
+
* A form field as authored. Exported so the derivation pins in
|
|
66
|
+
* `__tests__/FormViewWidenedArms.test.ts` can name the declared types; NOT
|
|
67
|
+
* re-exported by `spec-bridge/index.ts`, so the package surface is unchanged.
|
|
68
|
+
*/
|
|
69
|
+
export interface FormFieldSpec {
|
|
10
70
|
field: string;
|
|
11
71
|
/** Field type (spec FormFieldSchema reuses Data.FieldType; auto-infers widget). */
|
|
12
72
|
type?: string;
|
|
@@ -22,23 +82,45 @@ interface FormField {
|
|
|
22
82
|
hidden?: boolean;
|
|
23
83
|
colSpan?: number;
|
|
24
84
|
widget?: string;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Sibling field name whose value this field's widget reads (objectui#5040).
|
|
87
|
+
*
|
|
88
|
+
* The contract admits a BARE NAME and refuses an array. This is the wider
|
|
89
|
+
* {@link DependsOnInput} rather than the contract's `string` alone because
|
|
90
|
+
* the value is FORWARDED, unread, into the node slot that is declared with
|
|
91
|
+
* exactly this type (`FormField.dependsOn`, `@object-ui/types`) and read by
|
|
92
|
+
* `resolveCascadingOptions` / `resolveDependsOnFields` (`@object-ui/core`),
|
|
93
|
+
* whose parameter is also exactly this type (framework#4074). Narrowing to
|
|
94
|
+
* the contract's `string` would refuse array-authored layouts that the
|
|
95
|
+
* runtime has always honoured, on the same never-parsed inputs `groups` and
|
|
96
|
+
* `visibleOn` below are still read for. The contract's arm is a subset of
|
|
97
|
+
* this one, which is what the derivation pin asserts — so a spec that widens
|
|
98
|
+
* `dependsOn` fails the pin instead of silently outgrowing the declaration.
|
|
99
|
+
*/
|
|
100
|
+
dependsOn?: DependsOnInput;
|
|
101
|
+
/** Canonical conditional-visibility predicate (ADR-0089), bound to the contract. */
|
|
102
|
+
visibleWhen?: FormFieldInput['visibleWhen'];
|
|
103
|
+
/** @deprecated ADR-0089 -> `visibleWhen`. */
|
|
104
|
+
visibleOn?: FormFieldInput['visibleOn'];
|
|
30
105
|
}
|
|
31
|
-
|
|
106
|
+
/** One section of a form layout, as authored. */
|
|
107
|
+
export interface FormSectionSpec {
|
|
32
108
|
/** Stable section identifier for i18n lookup (spec FormSectionSchema.name). */
|
|
33
109
|
name?: string;
|
|
34
110
|
label?: string;
|
|
35
111
|
description?: string;
|
|
36
112
|
collapsible?: boolean;
|
|
37
113
|
collapsed?: boolean;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
114
|
+
/** Bound to the contract: the string spelling of a column count is admitted. */
|
|
115
|
+
columns?: FormSection['columns'];
|
|
116
|
+
/** Section-level conditional-visibility predicate (ADR-0089), bound to the contract. */
|
|
117
|
+
visibleWhen?: FormSection['visibleWhen'];
|
|
118
|
+
/**
|
|
119
|
+
* The authored field list. A bare string is the spec's shorthand for "this
|
|
120
|
+
* object's own field, rendered with its defaults" — the same shorthand
|
|
121
|
+
* `mapColumn` already honours on the list bridge.
|
|
122
|
+
*/
|
|
123
|
+
fields?: Array<string | FormFieldSpec>;
|
|
42
124
|
}
|
|
43
125
|
/**
|
|
44
126
|
* The subset of `@objectstack/spec` FormViewSchema the bridge consumes.
|
|
@@ -46,10 +128,11 @@ interface FormSection {
|
|
|
46
128
|
* or listed here with an explicit reason for being ignored — the bridge must
|
|
47
129
|
* never silently drop spec configuration (#2545).
|
|
48
130
|
*/
|
|
49
|
-
interface FormViewSpec {
|
|
131
|
+
export interface FormViewSpec {
|
|
50
132
|
type?: string;
|
|
51
133
|
layout?: string;
|
|
52
|
-
|
|
134
|
+
/** Bound to the contract, which admits NO string arm here — unlike a section. */
|
|
135
|
+
columns?: FormView['columns'];
|
|
53
136
|
title?: string;
|
|
54
137
|
description?: string;
|
|
55
138
|
defaultTab?: string;
|
|
@@ -63,20 +146,13 @@ interface FormViewSpec {
|
|
|
63
146
|
drawerWidth?: string;
|
|
64
147
|
modalSize?: string;
|
|
65
148
|
data?: any;
|
|
66
|
-
sections?:
|
|
149
|
+
sections?: FormSectionSpec[];
|
|
67
150
|
/** Legacy alias of `sections` (spec: "Legacy support → alias to sections"). */
|
|
68
|
-
groups?:
|
|
151
|
+
groups?: FormSectionSpec[];
|
|
69
152
|
/** Inline master-detail child collections. */
|
|
70
153
|
subforms?: any[];
|
|
71
|
-
defaultSort?: any;
|
|
72
154
|
sharing?: any;
|
|
73
|
-
aria?: {
|
|
74
|
-
ariaLabel?: string;
|
|
75
|
-
ariaDescribedBy?: string;
|
|
76
|
-
role?: string;
|
|
77
|
-
};
|
|
78
155
|
submitBehavior?: any;
|
|
79
156
|
}
|
|
80
157
|
/** Transforms a FormView spec into a Form SchemaNode */
|
|
81
158
|
export declare const bridgeFormView: BridgeFn<FormViewSpec>;
|
|
82
|
-
export {};
|
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Fold the contract's string spelling of a column count onto the number the
|
|
10
|
+
* node carries.
|
|
11
|
+
*
|
|
12
|
+
* `FormSectionSchema` accepts `'2'` and `2` and normalises the string away in
|
|
13
|
+
* its own pipe; the `object-form` node's section declares `columns?: 1|2|3|4`
|
|
14
|
+
* and `FormSectionContainer` indexes its grid-class map by that number. Its
|
|
15
|
+
* header states where the fold belongs: "The normalisation belongs at the seam
|
|
16
|
+
* that parses authored metadata, not in the container's props." This bridge is
|
|
17
|
+
* that seam. Forwarding `'2'` verbatim hands every downstream renderer a value
|
|
18
|
+
* outside the type it declares.
|
|
19
|
+
*/
|
|
20
|
+
function normalizeColumns(columns) {
|
|
21
|
+
return typeof columns === 'string' ? Number(columns) : columns;
|
|
22
|
+
}
|
|
8
23
|
function mapField(field) {
|
|
9
24
|
const mapped = {
|
|
10
25
|
name: field.field,
|
|
@@ -30,12 +45,23 @@ function mapField(field) {
|
|
|
30
45
|
mapped.colSpan = field.colSpan;
|
|
31
46
|
if (field.widget)
|
|
32
47
|
mapped.widget = field.widget;
|
|
48
|
+
// Forwarded unread into the node's `dependsOn`, which is declared with the
|
|
49
|
+
// same type and read by `@object-ui/core`'s cascading-option resolver.
|
|
33
50
|
if (field.dependsOn)
|
|
34
51
|
mapped.dependsOn = field.dependsOn;
|
|
35
52
|
// ADR-0089: `visibleWhen` is the canonical view-form-field visibility predicate
|
|
36
53
|
// (the spec folds the deprecated `visibleOn` into it at parse). Prefer it and
|
|
37
54
|
// fall back to `visibleOn` for raw / un-normalized metadata. The ObjectForm
|
|
38
55
|
// renderer reads this view-level predicate from the node's `visibleOn` slot.
|
|
56
|
+
//
|
|
57
|
+
// Both contract arms travel whole: the bare CEL string, and the expression
|
|
58
|
+
// object. `evalFieldPredicate` reads `{ dialect, source }` back out of the
|
|
59
|
+
// object arm, which is how a spec-authored `{ dialect: 'cel', source: ... }`
|
|
60
|
+
// now drives visibility instead of being refused by this declaration. An
|
|
61
|
+
// `ast`-only expression (legal metadata: the contract requires `dialect` plus
|
|
62
|
+
// one of `source` / `ast`) is forwarded whole rather than dropped, but no
|
|
63
|
+
// evaluator in this repo reads an `ast` today — that gap is the evaluator's,
|
|
64
|
+
// and the bridge must not hide it by discarding the key.
|
|
39
65
|
const visiblePredicate = field.visibleWhen ?? field.visibleOn;
|
|
40
66
|
if (visiblePredicate)
|
|
41
67
|
mapped.visibleOn = visiblePredicate;
|
|
@@ -43,7 +69,12 @@ function mapField(field) {
|
|
|
43
69
|
}
|
|
44
70
|
function mapSection(section) {
|
|
45
71
|
const mapped = {
|
|
46
|
-
|
|
72
|
+
// A bare field name is the spec's shorthand and is forwarded verbatim: the
|
|
73
|
+
// node's `fields` slot admits it and `normalizeSectionField`
|
|
74
|
+
// (@object-ui/plugin-form) resolves it against the object schema. Running
|
|
75
|
+
// it through `mapField` instead produced `{ name: undefined }` — a field
|
|
76
|
+
// with no identity — for the most ordinary section a form can declare.
|
|
77
|
+
fields: (section.fields ?? []).map((field) => typeof field === 'string' ? field : mapField(field)),
|
|
47
78
|
};
|
|
48
79
|
if (section.name)
|
|
49
80
|
mapped.name = section.name;
|
|
@@ -56,7 +87,8 @@ function mapSection(section) {
|
|
|
56
87
|
if (section.collapsed != null)
|
|
57
88
|
mapped.collapsed = section.collapsed;
|
|
58
89
|
if (section.columns != null)
|
|
59
|
-
mapped.columns = section.columns;
|
|
90
|
+
mapped.columns = normalizeColumns(section.columns);
|
|
91
|
+
// Whole, both arms — same predicate contract as the field above.
|
|
60
92
|
if (section.visibleWhen)
|
|
61
93
|
mapped.visibleWhen = section.visibleWhen;
|
|
62
94
|
return mapped;
|
|
@@ -113,14 +145,26 @@ export const bridgeFormView = (spec, _context) => {
|
|
|
113
145
|
if (spec[key] != null)
|
|
114
146
|
node[key] = spec[key];
|
|
115
147
|
}
|
|
116
|
-
if (spec.defaultSort)
|
|
117
|
-
node.defaultSort = spec.defaultSort;
|
|
118
148
|
if (spec.submitBehavior)
|
|
119
149
|
node.submitBehavior = spec.submitBehavior;
|
|
120
|
-
// P1.6 —
|
|
150
|
+
// P1.6 — sharing
|
|
121
151
|
if (spec.sharing)
|
|
122
152
|
node.sharing = spec.sharing;
|
|
123
|
-
|
|
124
|
-
|
|
153
|
+
// `defaultSort` (#3974) and `aria` (#3901) USED to be copied here. Both are
|
|
154
|
+
// `retiredKey()` tombstones on spec 17's FormViewSchema, so a FormView that
|
|
155
|
+
// parsed can never carry either — the guards were unreachable, and their
|
|
156
|
+
// shape invited the next reader to copy a dead pattern. Removed under the
|
|
157
|
+
// maintainer's 2026-08-11 enforce-or-remove ruling; measured dormant at BOTH
|
|
158
|
+
// ends before removal:
|
|
159
|
+
// - producer: `FormViewSchema.safeParse` rejects both keys by name
|
|
160
|
+
// (`form.defaultSort` / `form.aria` removed in the #3896 close-out).
|
|
161
|
+
// - consumer: no form renderer reads either off the node — plugin-form
|
|
162
|
+
// reads neither, and `SchemaRenderer`'s generic ARIA injection resolves
|
|
163
|
+
// FLAT `ariaLabel`/`ariaDescribedBy`/`role` (SchemaRenderer.tsx:109-119),
|
|
164
|
+
// never a nested `aria` object.
|
|
165
|
+
// The nested `aria` copy is still correct on the LIST bridge
|
|
166
|
+
// (`list-view.ts:214`), whose carrier stayed live and IS consumed at
|
|
167
|
+
// `plugin-list/src/ListView.tsx:2389-2392` — do not "align" the two.
|
|
168
|
+
// Pinned by `__tests__/FormViewRetiredKeys.test.ts`.
|
|
125
169
|
return node;
|
|
126
170
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
-
import type { BridgeFn } from '../types';
|
|
8
|
+
import type { BridgeFn } from '../types.js';
|
|
9
9
|
import type { ListView } from '@objectstack/spec/ui';
|
|
10
10
|
/**
|
|
11
11
|
* Bridge input: the spec-canonical ListView (#2231 — the former hand-written
|
|
@@ -5,15 +5,48 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* A spec `ListColumn` in, the same column out — in the spelling the spec
|
|
10
|
+
* declares (objectui#5068).
|
|
11
|
+
*
|
|
12
|
+
* This used to down-translate every column to the data-table adapter's
|
|
13
|
+
* `{ accessorKey, header }` before handing it to an `object-grid` node, and
|
|
14
|
+
* `ObjectGrid` translated it straight back through a tolerance branch that
|
|
15
|
+
* sniffed `columns[0]`. A round trip whose input was ALREADY canonical: the
|
|
16
|
+
* bridge's own parameter is `@objectstack/spec/ui`'s `ListColumn`, where
|
|
17
|
+
* `field` is required and `accessorKey` / `header` are refused by name
|
|
18
|
+
* (`ListColumnSchema` is a strict object). So the producer emitted a spelling
|
|
19
|
+
* the contract rejects, and the renderer grew a second de-facto contract to
|
|
20
|
+
* read it back — exactly the shape AGENTS.md #0.1 forbids, and the disposition
|
|
21
|
+
* objectui#3951 already settled: unify at the producer, no consumer-side alias.
|
|
22
|
+
*
|
|
23
|
+
* `accessorKey` is not retired as a concept — it is the TanStack adapter key
|
|
24
|
+
* `@object-ui/core` deliberately keeps OUT of the metadata identity fold
|
|
25
|
+
* (`column-identity.ts`'s `TABLE_ADAPTER_COLUMN_KEY`). `ObjectGrid` still
|
|
26
|
+
* applies it on the way OUT, at the one boundary that owns it. Metadata
|
|
27
|
+
* vocabulary comes in; adapter vocabulary goes out; one translation, one place.
|
|
28
|
+
*
|
|
29
|
+
* Nothing is invented on the way through, which is the second half of the fix.
|
|
30
|
+
* `header: col.label ?? col.field` used to turn "no label declared" into "the
|
|
31
|
+
* machine name was declared as the label", and that synthesized value then
|
|
32
|
+
* pre-empted `ObjectGrid`'s own header chain — `col.label` → the OBJECT
|
|
33
|
+
* FIELD's label → the prettified name — whose whole purpose is a column
|
|
34
|
+
* authored as a bare `{ field }`, and whose middle step exists so a localized
|
|
35
|
+
* field label wins over the name-derived English one on a non-English app. A
|
|
36
|
+
* bridged view therefore rendered raw machine names where a directly authored
|
|
37
|
+
* `object-grid` rendered the field's real, localizable label. It now forwards
|
|
38
|
+
* the declaration and lets the renderer resolve what was left unsaid.
|
|
39
|
+
*/
|
|
8
40
|
function mapColumn(col) {
|
|
9
|
-
// Spec
|
|
41
|
+
// Spec shorthand: a bare field name stands for a default column.
|
|
10
42
|
if (typeof col === 'string') {
|
|
11
|
-
return {
|
|
43
|
+
return { field: col };
|
|
12
44
|
}
|
|
13
45
|
const mapped = {
|
|
14
|
-
|
|
15
|
-
header: col.label ?? col.field,
|
|
46
|
+
field: col.field,
|
|
16
47
|
};
|
|
48
|
+
if (col.label != null)
|
|
49
|
+
mapped.label = col.label;
|
|
17
50
|
if (col.width != null)
|
|
18
51
|
mapped.width = col.width;
|
|
19
52
|
if (col.align)
|
|
@@ -38,21 +71,113 @@ function mapColumn(col) {
|
|
|
38
71
|
mapped.action = col.action;
|
|
39
72
|
return mapped;
|
|
40
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* The spec's five row heights collapsed onto the renderer's three densities.
|
|
76
|
+
*
|
|
77
|
+
* `Record<RowHeight, …>` on purpose (objectui#4352): the key set is the spec's,
|
|
78
|
+
* so a row height added upstream fails the build here instead of silently
|
|
79
|
+
* arriving with no density. The table used to carry four more keys —
|
|
80
|
+
* `comfortable`, `spacious`, `small`, `large` — which `RowHeightSchema` does
|
|
81
|
+
* not admit, so no spec-valid list view could ever reach them; they only
|
|
82
|
+
* survived because the parameter was widened back to `string` and the fixture
|
|
83
|
+
* asserting three of them compiled against nothing. Deleting them is AGENTS.md
|
|
84
|
+
* #0.1: a renderer-side dialect for off-spec metadata is a second de-facto
|
|
85
|
+
* contract, and one strict contract beats N. An off-spec `rowHeight` now falls
|
|
86
|
+
* through to `undefined` — the producer is where it gets fixed.
|
|
87
|
+
*/
|
|
88
|
+
const ROW_HEIGHT_TO_DENSITY = {
|
|
89
|
+
compact: 'compact',
|
|
90
|
+
short: 'compact',
|
|
91
|
+
medium: 'comfortable',
|
|
92
|
+
tall: 'spacious',
|
|
93
|
+
extra_tall: 'spacious',
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Runtime reader for {@link ROW_HEIGHT_TO_DENSITY} — the five spec row heights
|
|
97
|
+
* and **nothing else**.
|
|
98
|
+
*
|
|
99
|
+
* `hasOwnProperty`, not a bare index (objectui#4442): the table is a plain
|
|
100
|
+
* object literal, so indexing it with an unchecked key reaches
|
|
101
|
+
* `Object.prototype`. The parameter is typed `RowHeight`, but the boundary a
|
|
102
|
+
* host's stored view definition actually crosses is `SpecBridge.transformListView`,
|
|
103
|
+
* whose parameter is `any` — so `rowHeight: 'toString'` used to come back as
|
|
104
|
+
* `Object.prototype.toString`, a FUNCTION returned from a signature that
|
|
105
|
+
* promises three strings or nothing. `bridgeListView` then writes the key under
|
|
106
|
+
* `if (density)`, and a function is truthy, so it landed on the SchemaNode.
|
|
107
|
+
*
|
|
108
|
+
* This is the same guard `@object-ui/core`'s `rowHeightToDensityMode` grew in
|
|
109
|
+
* objectui#4440, and the repo's existing convention (`freeze-schema.ts:135`,
|
|
110
|
+
* `metadata-admin/predicate.ts:305`, six more). Both rowHeight surfaces now
|
|
111
|
+
* abstain identically on every off-spec spelling; the agreement is pinned by
|
|
112
|
+
* `__tests__/RowHeightDensityAgreement.test.ts`.
|
|
113
|
+
*
|
|
114
|
+
* `typeof rowHeight !== 'string'`, not `!rowHeight` (objectui#4459): the
|
|
115
|
+
* truthiness guard this replaces rejected only FALSY values, so every truthy
|
|
116
|
+
* non-string walked past it into a lookup that coerces its key — both
|
|
117
|
+
* `hasOwnProperty.call` and the index run `String(...)`. `['compact']`, a boxed
|
|
118
|
+
* `String('compact')` and `{ toString: () => 'compact' }` therefore each
|
|
119
|
+
* selected a real density, where core's twin abstained
|
|
120
|
+
* (`packages/core/src/utils/normalize-list-view.ts:83`, which has opened with
|
|
121
|
+
* the type guard since #4440). Note the direction against #4442: that leak produced a
|
|
122
|
+
* FUNCTION, visibly wrong to everything downstream; this one produced a
|
|
123
|
+
* legitimate-looking `'compact'` that nothing can tell apart from an authored
|
|
124
|
+
* value. The type guard subsumes the old one — `undefined`, `null`, `0` and
|
|
125
|
+
* `false` are all non-strings — and `''` keeps its answer while changing route:
|
|
126
|
+
* a string, so it passes here and is refused by `hasOwnProperty` one line down.
|
|
127
|
+
*/
|
|
41
128
|
function mapDensity(rowHeight) {
|
|
42
|
-
if (
|
|
129
|
+
if (typeof rowHeight !== 'string')
|
|
43
130
|
return undefined;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
131
|
+
if (!Object.prototype.hasOwnProperty.call(ROW_HEIGHT_TO_DENSITY, rowHeight)) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return ROW_HEIGHT_TO_DENSITY[rowHeight];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The spec's own parse-time lift, applied where `parse` cannot reach
|
|
138
|
+
* (objectui#4585).
|
|
139
|
+
*
|
|
140
|
+
* `@objectstack/spec` accepts BOTH spellings of `view.exportOptions` and lifts
|
|
141
|
+
* the legacy bare format array to the object form at parse (objectstack#8010,
|
|
142
|
+
* `ui/view.zod.ts`):
|
|
143
|
+
*
|
|
144
|
+
* z.array(ListViewExportFormatSchema).transform((formats) => ({ formats }))
|
|
145
|
+
*
|
|
146
|
+
* That transform never runs on this path. The bridge's input is a TypeScript
|
|
147
|
+
* type, not a parsed value — there is no `parse`/`safeParse` anywhere under
|
|
148
|
+
* `spec-bridge/` — so a host that parses first hands over the object form while
|
|
149
|
+
* a host that forwards raw stored metadata hands over whatever was authored,
|
|
150
|
+
* and nothing here can tell them apart. The legacy array therefore used to
|
|
151
|
+
* reach the `object-grid` node verbatim, where `ObjectGrid` reads
|
|
152
|
+
* `exportOptions.formats` and only that: `.formats` on an array is `undefined`,
|
|
153
|
+
* the renderer's `['csv', 'json']` default won, and the view's declared formats
|
|
154
|
+
* were dropped with no error and no console line — the export button still
|
|
155
|
+
* showed, because a non-empty array is truthy.
|
|
156
|
+
*
|
|
157
|
+
* This mirrors that transform and NOTHING more: the same contract applied one
|
|
158
|
+
* layer out, not a second de-facto one (AGENTS.md #0.1 — the consumer-side
|
|
159
|
+
* `Array.isArray` fallback in the renderer is what that rule forbids). Hence:
|
|
160
|
+
*
|
|
161
|
+
* - the object form passes through by reference, unread and unrewritten;
|
|
162
|
+
* - an empty array lifts to `{ formats: [] }`, exactly as the spec's transform
|
|
163
|
+
* does — its `z.array()` carries no `.min(1)`, so `[]` is a legal input that
|
|
164
|
+
* wraps rather than defaults. ObjectGrid then offers no format and hides the
|
|
165
|
+
* export button: "declared zero formats", read literally;
|
|
166
|
+
* - a `'pdf'` stored before its retirement is carried, not filtered. The spec
|
|
167
|
+
* REFUSES `'pdf'` at parse with a migration prescription (objectstack#8010;
|
|
168
|
+
* PDF export itself was declined as objectstack#1301 NOT_PLANNED) — it does
|
|
169
|
+
* not silently drop the value, so neither may this. Such a format dies
|
|
170
|
+
* downstream in ObjectGrid's format-AGNOSTIC menu filter, kept deliberately
|
|
171
|
+
* at objectui#4535 for metadata predating `os migrate meta --from 16`.
|
|
172
|
+
*/
|
|
173
|
+
function liftExportOptions(exportOptions) {
|
|
174
|
+
if (!Array.isArray(exportOptions))
|
|
175
|
+
return exportOptions;
|
|
176
|
+
// The pinned `@objectstack/spec@17.0.0-rc.6` still admits `'pdf'` as an array
|
|
177
|
+
// member (the enum narrowing arrives with the pin bump), so the element type
|
|
178
|
+
// here is wider than the renderer-side `ListViewExportFormat`. Narrowing it by
|
|
179
|
+
// filtering would invent precisely the silent drop the spec declines to do.
|
|
180
|
+
return { formats: exportOptions };
|
|
56
181
|
}
|
|
57
182
|
/** Transforms a ListView spec into a DataTable SchemaNode */
|
|
58
183
|
export const bridgeListView = (spec, _context) => {
|
|
@@ -86,10 +211,6 @@ export const bridgeListView = (spec, _context) => {
|
|
|
86
211
|
node.filterableFields = spec.filterableFields;
|
|
87
212
|
if (spec.resizable != null)
|
|
88
213
|
node.resizable = spec.resizable;
|
|
89
|
-
if (spec.striped != null)
|
|
90
|
-
node.striped = spec.striped;
|
|
91
|
-
if (spec.bordered != null)
|
|
92
|
-
node.bordered = spec.bordered;
|
|
93
214
|
if (spec.navigation)
|
|
94
215
|
node.navigation = spec.navigation;
|
|
95
216
|
if (spec.kanban)
|
|
@@ -109,14 +230,12 @@ export const bridgeListView = (spec, _context) => {
|
|
|
109
230
|
node.bulkActions = spec.bulkActions;
|
|
110
231
|
if (spec.bulkActionDefs)
|
|
111
232
|
node.bulkActionDefs = spec.bulkActionDefs;
|
|
112
|
-
if (spec.virtualScroll != null)
|
|
113
|
-
node.virtualScroll = spec.virtualScroll;
|
|
114
233
|
if (spec.conditionalFormatting)
|
|
115
234
|
node.conditionalFormatting = spec.conditionalFormatting;
|
|
116
235
|
if (spec.inlineEdit != null)
|
|
117
236
|
node.inlineEdit = spec.inlineEdit;
|
|
118
237
|
if (spec.exportOptions)
|
|
119
|
-
node.exportOptions = spec.exportOptions;
|
|
238
|
+
node.exportOptions = liftExportOptions(spec.exportOptions);
|
|
120
239
|
if (spec.emptyState)
|
|
121
240
|
node.emptyState = spec.emptyState;
|
|
122
241
|
if (spec.userActions)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
-
export { SpecBridge } from './SpecBridge';
|
|
9
|
-
export type { BridgeContext, BridgeFn, ObjectDefLite } from './types';
|
|
10
|
-
export { bridgeListView } from './bridges/list-view';
|
|
11
|
-
export { bridgeFormView } from './bridges/form-view';
|
|
8
|
+
export { SpecBridge } from './SpecBridge.js';
|
|
9
|
+
export type { BridgeContext, BridgeFn, ObjectDefLite } from './types.js';
|
|
10
|
+
export { bridgeListView } from './bridges/list-view.js';
|
|
11
|
+
export { bridgeFormView } from './bridges/form-view.js';
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
-
export { SpecBridge } from './SpecBridge';
|
|
9
|
-
export { bridgeListView } from './bridges/list-view';
|
|
10
|
-
export { bridgeFormView } from './bridges/form-view';
|
|
8
|
+
export { SpecBridge } from './SpecBridge.js';
|
|
9
|
+
export { bridgeListView } from './bridges/list-view.js';
|
|
10
|
+
export { bridgeFormView } from './bridges/form-view.js';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
8
|
+
import type { BaseSchema } from '@object-ui/types';
|
|
9
9
|
/** Context passed to all bridge functions */
|
|
10
10
|
export interface BridgeContext {
|
|
11
11
|
/** Current user context for permission checks */
|
|
@@ -30,5 +30,26 @@ export interface ObjectDefLite {
|
|
|
30
30
|
}>;
|
|
31
31
|
}>;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* A bridge function transforms a spec schema into a schema node tree.
|
|
35
|
+
*
|
|
36
|
+
* Returns **`BaseSchema`**, not `SchemaNode` (objectui#4580, ruling Q4-B).
|
|
37
|
+
* Once core's `SchemaNode` became `@object-ui/types`' union
|
|
38
|
+
* (`BaseSchema | string | number | boolean | null | undefined`), declaring the
|
|
39
|
+
* return as `SchemaNode` made every caller re-ask a question no bridge has ever
|
|
40
|
+
* answered "yes" to: *is this a bare string / null?* Both shipped bridges end
|
|
41
|
+
* in a single `return node` on an object literal — `bridges/list-view.ts:226`
|
|
42
|
+
* and `bridges/form-view.ts:195` — so the wider declaration described nothing
|
|
43
|
+
* real while forcing a narrowing at every read.
|
|
44
|
+
*
|
|
45
|
+
* That cost was measured, not assumed: round 1 of this card recorded **272**
|
|
46
|
+
* mechanical errors across five spec-bridge suites, all of them TS18049
|
|
47
|
+
* (`'node' is possibly 'null' or 'undefined'`) and TS2339
|
|
48
|
+
* (`Property 'formType' does not exist on type
|
|
49
|
+
* 'string | number | boolean | BaseSchema'`) — the union being destructured by
|
|
50
|
+
* tests reading properties off a node the bridge always produces.
|
|
51
|
+
*
|
|
52
|
+
* This is a type ANNOTATION only: no runtime behaviour changes, and the emitted
|
|
53
|
+
* JavaScript is byte-identical (proven by bundle sha256 on this PR).
|
|
54
|
+
*/
|
|
55
|
+
export type BridgeFn<T = any> = (spec: T, context: BridgeContext) => BaseSchema;
|