@object-ui/react 17.5.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 +334 -0
- package/README.md +9 -8
- package/dist/LazyPluginLoader.d.ts +14 -7
- package/dist/LazyPluginLoader.js +14 -7
- package/dist/SchemaRenderer.js +434 -22
- package/dist/context/HostNavigationContext.d.ts +82 -0
- package/dist/context/HostNavigationContext.js +65 -0
- package/dist/context/RelatedRecordActionsContext.d.ts +15 -0
- package/dist/context/index.d.ts +13 -12
- package/dist/context/index.js +13 -12
- package/dist/element-data-source/ElementDataSourceGate.d.ts +75 -2
- package/dist/element-data-source/ElementDataSourceGate.js +81 -2
- package/dist/hooks/index.d.ts +35 -35
- package/dist/hooks/index.js +35 -35
- package/dist/hooks/useActionEngine.js +1 -1
- 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.js +1 -1
- package/dist/hooks/useDiscovery.js +1 -1
- package/dist/hooks/useElementDataSource.js +5 -2
- package/dist/hooks/useExpression.d.ts +24 -3
- package/dist/hooks/useExpression.js +56 -4
- package/dist/hooks/useNavigationOverlay.d.ts +1 -1
- package/dist/hooks/usePageAssignment.js +1 -1
- 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 -11
- package/dist/index.js +10 -10
- package/dist/schema-input.d.ts +16 -5
- package/dist/schema-input.js +15 -4
- package/dist/spec-bridge/SpecBridge.d.ts +1 -1
- package/dist/spec-bridge/SpecBridge.js +2 -2
- 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 +37 -10
- package/dist/spec-bridge/index.d.ts +4 -4
- package/dist/spec-bridge/index.js +3 -3
- package/dist/utils/error-message.d.ts +59 -0
- package/dist/utils/error-message.js +116 -1
- 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 +7 -7
|
@@ -87,6 +87,25 @@ export { toPredicateInput } from '@object-ui/core';
|
|
|
87
87
|
* and the four generic action renderers carried the same root-only binding
|
|
88
88
|
* until objectui#4075.
|
|
89
89
|
*
|
|
90
|
+
* ## The canon is `record.*` (objectui#5330, ruled 2026-08-20)
|
|
91
|
+
*
|
|
92
|
+
* All three spellings are bound here, and that is unchanged — but they are NOT
|
|
93
|
+
* peers. `record.*` is the CONTRACT; the bare shorthand and `data.*` are
|
|
94
|
+
* tolerances in a deprecation window, kept because stored metadata carries
|
|
95
|
+
* them, reported once by `warnNonCanonicalRowSpelling` from `useCondition`
|
|
96
|
+
* below, and removable only after a stored-metadata survey sizes the window
|
|
97
|
+
* (⛔ no removal before the survey — part of the same ruling).
|
|
98
|
+
*
|
|
99
|
+
* The canon states the SERVER's accept set, which is narrower than this bag:
|
|
100
|
+
* measured on `@objectstack/formula@17.1.0`, `buildScope({ record })` mounts
|
|
101
|
+
* exactly `['record']`, so of the three spellings this helper binds, only
|
|
102
|
+
* `record.*` evaluates server-side — a bare field faults `Unknown variable:
|
|
103
|
+
* status` and `data.*` faults `Unknown variable: data`. This three-way bag has
|
|
104
|
+
* no server counterpart at all, which is exactly why the warning belongs on
|
|
105
|
+
* this side. `@object-ui/core`'s `evaluator/rowPredicateCanon.ts` carries the
|
|
106
|
+
* full measurement and the layer scoping (`data` stays canonical one layer
|
|
107
|
+
* over, in a metadata-editing form — ADR-0089 D3).
|
|
108
|
+
*
|
|
90
109
|
* `record` and `data` are written AFTER the spread deliberately: a row that
|
|
91
110
|
* happens to carry a field literally named `record` or `data` must not shadow
|
|
92
111
|
* the root every predicate is written against. Same precedence as
|
|
@@ -140,9 +159,11 @@ export declare function useCondition(condition: string | boolean | undefined | {
|
|
|
140
159
|
* routes to `@object-ui/core`'s `evalRowPredicate`: a bare string is CEL (the
|
|
141
160
|
* spec contract for `ActionSchema.visible`), a `{ dialect: 'cel', source }`
|
|
142
161
|
* envelope is always CEL, and only a legacy-dialect string falls back to the
|
|
143
|
-
* old engine (with a deprecation warning). The row is bound as `record.*`
|
|
144
|
-
*
|
|
145
|
-
*
|
|
162
|
+
* old engine (with a deprecation warning). The row is bound as `record.*` — the
|
|
163
|
+
* CANON (objectui#5330) — and, for stored metadata only, as bare fields and
|
|
164
|
+
* `data.*`, both of which now warn once; the ambient predicate scope
|
|
165
|
+
* (`features` / `user` / …) is merged alongside so deployment-level gates keep
|
|
166
|
+
* resolving.
|
|
146
167
|
*
|
|
147
168
|
* @param pred The raw predicate: `boolean` (returned as-is), a CEL string,
|
|
148
169
|
* an `{ dialect, source }` envelope, or `null`/`undefined`/`''`.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import { createContext, createElement, useContext, useMemo } from 'react';
|
|
9
|
-
import { ExpressionEvaluator, evalRowPredicate } from '@object-ui/core';
|
|
9
|
+
import { ExpressionEvaluator, evalRowPredicate, isLegacyDialectSource, warnNonCanonicalRowSpelling, } from '@object-ui/core';
|
|
10
10
|
/**
|
|
11
11
|
* Global predicate scope — populated by host shells (e.g. app-shell's
|
|
12
12
|
* `ExpressionProvider`) so renderers like action buttons can resolve
|
|
@@ -101,6 +101,25 @@ export { toPredicateInput } from '@object-ui/core';
|
|
|
101
101
|
* and the four generic action renderers carried the same root-only binding
|
|
102
102
|
* until objectui#4075.
|
|
103
103
|
*
|
|
104
|
+
* ## The canon is `record.*` (objectui#5330, ruled 2026-08-20)
|
|
105
|
+
*
|
|
106
|
+
* All three spellings are bound here, and that is unchanged — but they are NOT
|
|
107
|
+
* peers. `record.*` is the CONTRACT; the bare shorthand and `data.*` are
|
|
108
|
+
* tolerances in a deprecation window, kept because stored metadata carries
|
|
109
|
+
* them, reported once by `warnNonCanonicalRowSpelling` from `useCondition`
|
|
110
|
+
* below, and removable only after a stored-metadata survey sizes the window
|
|
111
|
+
* (⛔ no removal before the survey — part of the same ruling).
|
|
112
|
+
*
|
|
113
|
+
* The canon states the SERVER's accept set, which is narrower than this bag:
|
|
114
|
+
* measured on `@objectstack/formula@17.1.0`, `buildScope({ record })` mounts
|
|
115
|
+
* exactly `['record']`, so of the three spellings this helper binds, only
|
|
116
|
+
* `record.*` evaluates server-side — a bare field faults `Unknown variable:
|
|
117
|
+
* status` and `data.*` faults `Unknown variable: data`. This three-way bag has
|
|
118
|
+
* no server counterpart at all, which is exactly why the warning belongs on
|
|
119
|
+
* this side. `@object-ui/core`'s `evaluator/rowPredicateCanon.ts` carries the
|
|
120
|
+
* full measurement and the layer scoping (`data` stays canonical one layer
|
|
121
|
+
* over, in a metadata-editing form — ADR-0089 D3).
|
|
122
|
+
*
|
|
104
123
|
* `record` and `data` are written AFTER the spread deliberately: a row that
|
|
105
124
|
* happens to carry a field literally named `record` or `data` must not shadow
|
|
106
125
|
* the root every predicate is written against. Same precedence as
|
|
@@ -169,6 +188,37 @@ export function useCondition(condition, context = {}, options) {
|
|
|
169
188
|
const scope = usePredicateScope();
|
|
170
189
|
// We evaluate directly without caching the evaluator to avoid issues with context changes
|
|
171
190
|
return useMemo(() => {
|
|
191
|
+
// objectui#5330 Phase 1 — report a deprecated row-predicate spelling on
|
|
192
|
+
// the `useCondition` tier. This is the BINDING half's evaluation entry:
|
|
193
|
+
// `usePredicateRecordContext` sees the row but never the predicate text,
|
|
194
|
+
// so it structurally cannot detect a spelling, and this is the first
|
|
195
|
+
// point where the two meet.
|
|
196
|
+
//
|
|
197
|
+
// Two guards, both of which can only remove a report:
|
|
198
|
+
// - the bag must carry the `usePredicateRecordContext` signature, `data`
|
|
199
|
+
// and `record` being the SAME object by identity. A host scope that
|
|
200
|
+
// merely happens to carry a `data` key cannot satisfy that, so a
|
|
201
|
+
// non-row `useCondition` call is never judged as a row predicate.
|
|
202
|
+
// - the source must be CEL. In this tier's legacy `${…}` dialect
|
|
203
|
+
// (`'${data.status === "active"}'` — this hook's own doc example)
|
|
204
|
+
// `data.*` is the NORMAL spelling, and reporting it would be a false
|
|
205
|
+
// positive on every legacy predicate.
|
|
206
|
+
const bag = context;
|
|
207
|
+
const boundRow = bag.record != null && typeof bag.record === 'object' && bag.record === bag.data
|
|
208
|
+
? bag.record
|
|
209
|
+
: undefined;
|
|
210
|
+
if (boundRow !== undefined) {
|
|
211
|
+
const celSource = typeof condition === 'string'
|
|
212
|
+
? isLegacyDialectSource(condition)
|
|
213
|
+
? undefined
|
|
214
|
+
: condition
|
|
215
|
+
: condition && typeof condition === 'object' && condition.dialect === 'cel'
|
|
216
|
+
? condition.source
|
|
217
|
+
: undefined;
|
|
218
|
+
if (typeof celSource === 'string') {
|
|
219
|
+
warnNonCanonicalRowSpelling(celSource, boundRow, true, options?.label);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
172
222
|
const evaluator = new ExpressionEvaluator({ ...scope, ...context });
|
|
173
223
|
if (options?.throwOnError) {
|
|
174
224
|
// Fail-closed: a predicate that can't be evaluated hides/disables
|
|
@@ -206,9 +256,11 @@ export function useCondition(condition, context = {}, options) {
|
|
|
206
256
|
* routes to `@object-ui/core`'s `evalRowPredicate`: a bare string is CEL (the
|
|
207
257
|
* spec contract for `ActionSchema.visible`), a `{ dialect: 'cel', source }`
|
|
208
258
|
* envelope is always CEL, and only a legacy-dialect string falls back to the
|
|
209
|
-
* old engine (with a deprecation warning). The row is bound as `record.*`
|
|
210
|
-
*
|
|
211
|
-
*
|
|
259
|
+
* old engine (with a deprecation warning). The row is bound as `record.*` — the
|
|
260
|
+
* CANON (objectui#5330) — and, for stored metadata only, as bare fields and
|
|
261
|
+
* `data.*`, both of which now warn once; the ambient predicate scope
|
|
262
|
+
* (`features` / `user` / …) is merged alongside so deployment-level gates keep
|
|
263
|
+
* resolving.
|
|
212
264
|
*
|
|
213
265
|
* @param pred The raw predicate: `boolean` (returned as-is), a CEL string,
|
|
214
266
|
* an `{ dialect, source }` envelope, or `null`/`undefined`/`''`.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import type { NavigationConfigSchema, NavigationMode as SpecNavigationMode } from '@objectstack/spec/ui';
|
|
9
|
-
import type { SpecAuthoredInput } from '../spec-input';
|
|
9
|
+
import type { SpecAuthoredInput } from '../spec-input.js';
|
|
10
10
|
/**
|
|
11
11
|
* The spec's `NavigationConfigSchema`, authoring side — by reference, with no
|
|
12
12
|
* divergence of its own.
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* when no record Page is authored.
|
|
25
25
|
*/
|
|
26
26
|
import { useEffect, useMemo, useState } from 'react';
|
|
27
|
-
import { useMetadata } from '../context/AppShellContext';
|
|
27
|
+
import { useMetadata } from '../context/AppShellContext.js';
|
|
28
28
|
function matchesAssignment(_page, _opts) {
|
|
29
29
|
// Placeholder — every page matches until rules-based assignment lands.
|
|
30
30
|
return true;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import { useMemo } from 'react';
|
|
9
|
-
import { useReducedMotion } from './useReducedMotion';
|
|
9
|
+
import { useReducedMotion } from './useReducedMotion.js';
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Constants
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import * as React from 'react';
|
|
9
|
-
import { usePageVariables } from './usePageVariables';
|
|
10
|
-
import { useAction } from '../context';
|
|
9
|
+
import { usePageVariables } from './usePageVariables.js';
|
|
10
|
+
import { useAction } from '../context/index.js';
|
|
11
11
|
/**
|
|
12
12
|
* PageVariableActionBridge — publishes the live page-variable snapshot into the
|
|
13
13
|
* shared ActionRunner context as `pageVariables`, so a submit action (`api` /
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { useState, useEffect, useCallback, useMemo, useContext } from 'react';
|
|
12
12
|
import { resolveDataSource } from '@object-ui/core';
|
|
13
|
-
import { SchemaRendererContext } from '../context/SchemaRendererContext';
|
|
13
|
+
import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Hook
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import { useState, useCallback, useMemo } from 'react';
|
|
9
|
-
import { useReducedMotion } from './useReducedMotion';
|
|
9
|
+
import { useReducedMotion } from './useReducedMotion.js';
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Helpers
|
|
12
12
|
// ---------------------------------------------------------------------------
|
package/dist/index.d.ts
CHANGED
|
@@ -5,15 +5,15 @@
|
|
|
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 * from './SchemaRenderer';
|
|
9
|
-
export * from './schema-input';
|
|
10
|
-
export * from './hooks';
|
|
11
|
-
export * from './context';
|
|
12
|
-
export * from './LazyPluginLoader';
|
|
13
|
-
export * from './spec-bridge';
|
|
14
|
-
export * from './data-invalidation';
|
|
15
|
-
export * from './element-data-source/ElementDataSourceGate';
|
|
16
|
-
export { resolveKeyedI18nLabel } from './utils/i18n';
|
|
17
|
-
export { extractWriteErrorMessage, isPermissionError, extractFieldErrors } from './utils/error-message';
|
|
18
|
-
export type { WriteFieldError } from './utils/error-message';
|
|
8
|
+
export * from './SchemaRenderer.js';
|
|
9
|
+
export * from './schema-input.js';
|
|
10
|
+
export * from './hooks/index.js';
|
|
11
|
+
export * from './context/index.js';
|
|
12
|
+
export * from './LazyPluginLoader.js';
|
|
13
|
+
export * from './spec-bridge/index.js';
|
|
14
|
+
export * from './data-invalidation.js';
|
|
15
|
+
export * from './element-data-source/ElementDataSourceGate.js';
|
|
16
|
+
export { resolveKeyedI18nLabel } from './utils/i18n.js';
|
|
17
|
+
export { extractWriteErrorMessage, isPermissionError, extractFieldErrors, classifyLoadError, declaredUserMessage } from './utils/error-message.js';
|
|
18
|
+
export type { WriteFieldError, LoadErrorKind } from './utils/error-message.js';
|
|
19
19
|
export { I18nProvider, useObjectTranslation, useSafeTranslate, useObjectLabel, useSafeFieldLabel, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, type I18nConfig, type I18nProviderProps, type TranslationKeys, type DateFormatOptions, type CurrencyFormatOptions, type NumberFormatOptions, } from '@object-ui/i18n';
|
package/dist/index.js
CHANGED
|
@@ -5,20 +5,20 @@
|
|
|
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 * from './SchemaRenderer';
|
|
9
|
-
export * from './schema-input';
|
|
10
|
-
export * from './hooks'; // will be empty for now
|
|
11
|
-
export * from './context'; // will be empty for now
|
|
12
|
-
export * from './LazyPluginLoader';
|
|
13
|
-
export * from './spec-bridge';
|
|
14
|
-
export * from './data-invalidation';
|
|
8
|
+
export * from './SchemaRenderer.js';
|
|
9
|
+
export * from './schema-input.js';
|
|
10
|
+
export * from './hooks/index.js'; // will be empty for now
|
|
11
|
+
export * from './context/index.js'; // will be empty for now
|
|
12
|
+
export * from './LazyPluginLoader.js';
|
|
13
|
+
export * from './spec-bridge/index.js';
|
|
14
|
+
export * from './data-invalidation.js';
|
|
15
15
|
// PageComponentSchema.dataSource — mapping the spec's per-element data binding
|
|
16
16
|
// onto the schema keys each object-bound block reads (objectstack#6953).
|
|
17
|
-
export * from './element-data-source/ElementDataSourceGate';
|
|
17
|
+
export * from './element-data-source/ElementDataSourceGate.js';
|
|
18
18
|
// i18n utilities
|
|
19
|
-
export { resolveKeyedI18nLabel } from './utils/i18n';
|
|
19
|
+
export { resolveKeyedI18nLabel } from './utils/i18n.js';
|
|
20
20
|
// Write-error surfacing utilities (shared by drag-write plugins so a failed
|
|
21
21
|
// PATCH — e.g. an RLS 403 — is never silently swallowed).
|
|
22
|
-
export { extractWriteErrorMessage, isPermissionError, extractFieldErrors } from './utils/error-message';
|
|
22
|
+
export { extractWriteErrorMessage, isPermissionError, extractFieldErrors, classifyLoadError, declaredUserMessage } from './utils/error-message.js';
|
|
23
23
|
// Built-in i18n support
|
|
24
24
|
export { I18nProvider, useObjectTranslation, useSafeTranslate, useObjectLabel, useSafeFieldLabel, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from '@object-ui/i18n';
|
package/dist/schema-input.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import type { BaseSchema } from '@object-ui/types';
|
|
9
|
-
import type { SchemaRendererProps } from './SchemaRenderer';
|
|
9
|
+
import type { SchemaRendererProps } from './SchemaRenderer.js';
|
|
10
10
|
/**
|
|
11
11
|
* Narrow a loosely-typed metadata node onto {@link SchemaRendererProps.schema}.
|
|
12
12
|
*
|
|
@@ -23,9 +23,20 @@ import type { SchemaRendererProps } from './SchemaRenderer';
|
|
|
23
23
|
* tells no lie — the value a caller forwards renders identically whether or not
|
|
24
24
|
* it passes through here.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
26
|
+
* The bridge is PERMANENT — not scaffolding awaiting a merge. The two competing
|
|
27
|
+
* repo-wide `SchemaNode` spellings ARE reconciled (objectui#4580, PR #4608):
|
|
28
|
+
* `@object-ui/core` stopped hand-declaring its own interface and now re-exports
|
|
29
|
+
* `@object-ui/types`' union, so one declaration is left to disagree with. That
|
|
30
|
+
* reconciliation resolved in favour of the UNION, while `SchemaRenderer`'s prop
|
|
31
|
+
* stays deliberately narrow (objectui#4548 ruling Q2 — it declares no `number`
|
|
32
|
+
* or `boolean`), so a `SchemaNode` is now LESS assignable to that prop than it
|
|
33
|
+
* was, not more. This step therefore bridges two intentionally different types,
|
|
34
|
+
* and it stays. ⛔ Do not "tidy" a call site back into a direct forward: the
|
|
35
|
+
* five `apps/site` sites that were forwarding directly when PR #4608 landed are
|
|
36
|
+
* what kept `Build Docs` red on `main` for ~5 hours — each one a TS2322 naming
|
|
37
|
+
* `number` against this function's return type, spelled
|
|
38
|
+
* `string | BaseSchema | null | undefined` in objectui#4617's logs — until
|
|
39
|
+
* PR #4621 routed all five through here. An earlier revision of this paragraph
|
|
40
|
+
* said the reconciliation was still pending and invited exactly that edit.
|
|
30
41
|
*/
|
|
31
42
|
export declare function toRenderableSchema(node: BaseSchema | string | number | boolean | null | undefined): SchemaRendererProps['schema'];
|
package/dist/schema-input.js
CHANGED
|
@@ -21,10 +21,21 @@
|
|
|
21
21
|
* tells no lie — the value a caller forwards renders identically whether or not
|
|
22
22
|
* it passes through here.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* The bridge is PERMANENT — not scaffolding awaiting a merge. The two competing
|
|
25
|
+
* repo-wide `SchemaNode` spellings ARE reconciled (objectui#4580, PR #4608):
|
|
26
|
+
* `@object-ui/core` stopped hand-declaring its own interface and now re-exports
|
|
27
|
+
* `@object-ui/types`' union, so one declaration is left to disagree with. That
|
|
28
|
+
* reconciliation resolved in favour of the UNION, while `SchemaRenderer`'s prop
|
|
29
|
+
* stays deliberately narrow (objectui#4548 ruling Q2 — it declares no `number`
|
|
30
|
+
* or `boolean`), so a `SchemaNode` is now LESS assignable to that prop than it
|
|
31
|
+
* was, not more. This step therefore bridges two intentionally different types,
|
|
32
|
+
* and it stays. ⛔ Do not "tidy" a call site back into a direct forward: the
|
|
33
|
+
* five `apps/site` sites that were forwarding directly when PR #4608 landed are
|
|
34
|
+
* what kept `Build Docs` red on `main` for ~5 hours — each one a TS2322 naming
|
|
35
|
+
* `number` against this function's return type, spelled
|
|
36
|
+
* `string | BaseSchema | null | undefined` in objectui#4617's logs — until
|
|
37
|
+
* PR #4621 routed all five through here. An earlier revision of this paragraph
|
|
38
|
+
* said the reconciliation was still pending and invited exactly that edit.
|
|
28
39
|
*/
|
|
29
40
|
export function toRenderableSchema(node) {
|
|
30
41
|
return typeof node === 'number' || typeof node === 'boolean' ? String(node) : node;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import type { BaseSchema } from '@object-ui/types';
|
|
9
|
-
import type { BridgeContext, BridgeFn } from './types';
|
|
9
|
+
import type { BridgeContext, BridgeFn } from './types.js';
|
|
10
10
|
export declare class SpecBridge {
|
|
11
11
|
private bridges;
|
|
12
12
|
private context;
|
|
@@ -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", {
|
|
@@ -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
|