@object-ui/react 17.6.0 → 18.0.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 +4263 -0
- package/README.md +383 -42
- package/dist/SchemaRenderer.d.ts +64 -7
- package/dist/SchemaRenderer.js +1326 -106
- package/dist/context/ActionContext.js +40 -1
- package/dist/context/AppShellContext.d.ts +27 -0
- package/dist/context/DiscussionContext.d.ts +27 -0
- package/dist/context/DiscussionContext.js +4 -0
- package/dist/context/DrillNavigationContext.d.ts +13 -0
- package/dist/context/InlineEditContext.d.ts +151 -1
- package/dist/context/InlineEditContext.js +36 -2
- package/dist/context/PrintPageContext.d.ts +60 -0
- package/dist/context/PrintPageContext.js +32 -0
- package/dist/context/RecordContext.d.ts +48 -5
- package/dist/context/RecordContext.js +10 -1
- package/dist/context/RelatedRecordActionsContext.d.ts +72 -2
- package/dist/context/RelatedRecordActionsContext.js +4 -0
- package/dist/context/SchemaRendererContext.d.ts +61 -6
- package/dist/context/SchemaRendererContext.js +39 -6
- package/dist/context/ThemeContext.js +1 -1
- package/dist/context/actionRunnerTranslator.d.ts +27 -0
- package/dist/context/actionRunnerTranslator.js +37 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +1 -0
- package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
- package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
- package/dist/element-data-source/ElementDataSourceGate.d.ts +118 -3
- package/dist/element-data-source/ElementDataSourceGate.js +401 -19
- package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
- package/dist/element-data-source/useElementApiDataSource.js +198 -0
- package/dist/hooks/index.d.ts +3 -1
- package/dist/hooks/index.js +10 -1
- package/dist/hooks/useActionEngine.js +46 -3
- package/dist/hooks/useActionRunner.js +4 -0
- package/dist/hooks/useActionTextLocalizer.d.ts +35 -9
- package/dist/hooks/useActionTextLocalizer.js +70 -14
- package/dist/hooks/useConfigBagEvaluator.d.ts +31 -0
- package/dist/hooks/useConfigBagEvaluator.js +40 -0
- package/dist/hooks/useDatasetDimensionLabels.js +25 -10
- package/dist/hooks/useDensityMode.d.ts +8 -2
- package/dist/hooks/useDensityMode.js +8 -2
- package/dist/hooks/useDiscovery.d.ts +1 -10
- package/dist/hooks/useETagCache.js +25 -4
- package/dist/hooks/useElementDataSource.d.ts +15 -2
- package/dist/hooks/useElementDataSource.js +24 -7
- package/dist/hooks/useExpression.d.ts +52 -59
- package/dist/hooks/useExpression.js +58 -90
- package/dist/hooks/useFilterScope.d.ts +22 -2
- package/dist/hooks/useFilterScope.js +29 -4
- package/dist/hooks/useGlobalUndo.d.ts +10 -0
- package/dist/hooks/useGlobalUndo.js +112 -46
- package/dist/hooks/useNavigationOverlay.d.ts +76 -11
- package/dist/hooks/useNavigationOverlay.js +119 -9
- package/dist/hooks/useOffline.js +65 -11
- package/dist/hooks/usePageAssignment.d.ts +33 -7
- package/dist/hooks/usePageAssignment.js +85 -21
- package/dist/hooks/useRecordSearch.d.ts +34 -1
- package/dist/hooks/useRecordSearch.js +42 -7
- package/dist/hooks/useResolvedFilter.d.ts +46 -0
- package/dist/hooks/useResolvedFilter.js +130 -0
- package/dist/hooks/useSchemaPersistence.d.ts +7 -0
- package/dist/hooks/useSchemaPersistence.js +144 -5
- package/dist/hooks/useSettledSchema.d.ts +123 -0
- package/dist/hooks/useSettledSchema.js +186 -0
- package/dist/hooks/useViewData.d.ts +10 -3
- package/dist/hooks/useViewData.js +5 -14
- package/dist/index.d.ts +9 -1
- package/dist/index.js +48 -1
- package/dist/schema-input.d.ts +38 -8
- package/dist/schema-input.js +39 -7
- package/dist/utils/configBag.d.ts +68 -0
- package/dist/utils/configBag.js +70 -0
- package/dist/utils/error-message.d.ts +1 -1
- package/dist/utils/error-message.js +1 -1
- package/dist/utils/inlineAria.d.ts +61 -0
- package/dist/utils/inlineAria.js +70 -0
- package/dist/utils/localeMap.d.ts +27 -0
- package/dist/utils/localeMap.js +106 -0
- package/dist/utils/nonGridRowCeiling.d.ts +44 -0
- package/dist/utils/nonGridRowCeiling.js +78 -0
- package/dist/utils/paramsBag.d.ts +28 -0
- package/dist/utils/paramsBag.js +146 -0
- package/dist/utils/propsBagDiagnostic.d.ts +196 -0
- package/dist/utils/propsBagDiagnostic.js +422 -0
- package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
- package/dist/utils/refusedDataPropDiagnostic.js +94 -0
- package/dist/utils/unevaluatedExpression.d.ts +44 -1
- package/dist/utils/unevaluatedExpression.js +92 -11
- package/dist/utils/visibilityDiagnostic.d.ts +282 -21
- package/dist/utils/visibilityDiagnostic.js +321 -46
- package/package.json +10 -10
- package/dist/hooks/useClientNotifications.d.ts +0 -47
- package/dist/hooks/useClientNotifications.js +0 -162
- package/dist/spec-bridge/SpecBridge.d.ts +0 -28
- package/dist/spec-bridge/SpecBridge.js +0 -56
- package/dist/spec-bridge/bridges/form-view.d.ts +0 -158
- package/dist/spec-bridge/bridges/form-view.js +0 -170
- package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
- package/dist/spec-bridge/bridges/list-view.js +0 -268
- package/dist/spec-bridge/index.d.ts +0 -11
- package/dist/spec-bridge/index.js +0 -10
- package/dist/spec-bridge/types.d.ts +0 -55
- package/dist/spec-bridge/types.js +0 -8
|
@@ -77,7 +77,12 @@ import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
|
77
77
|
export function useDatasetDimensionMeta(object, dimensionFields, dimensions, options) {
|
|
78
78
|
// The host's AUTHENTICATED fetch (objectui#4121) — see property 1 in the
|
|
79
79
|
// file header for why it is read off the context directly.
|
|
80
|
-
const
|
|
80
|
+
const context = useContext(SchemaRendererContext);
|
|
81
|
+
const apiFetch = context?.apiFetch;
|
|
82
|
+
// The host data source, when it can answer `getObjectSchema` (objectui#13039):
|
|
83
|
+
// the data layer's shared request-key cache, so N dataset widgets over one
|
|
84
|
+
// object make ONE `meta/object/NAME` request instead of one each.
|
|
85
|
+
const dataSource = context?.dataSource;
|
|
81
86
|
const enabled = options?.enabled ?? true;
|
|
82
87
|
// LOCALE-FREE state (objectui#4030) — see property 2 in the file header.
|
|
83
88
|
const [meta, setMeta] = useState(null);
|
|
@@ -103,14 +108,22 @@ export function useDatasetDimensionMeta(object, dimensionFields, dimensions, opt
|
|
|
103
108
|
(async () => {
|
|
104
109
|
try {
|
|
105
110
|
const doFetch = apiFetch ?? fetch;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
// objectui#13039 — through the data source's `getObjectSchema` when one
|
|
112
|
+
// that answers it is in scope: the read the dashboard's chart, metric
|
|
113
|
+
// and table widgets make, which the adapter's metadata cache joins while
|
|
114
|
+
// it is in flight. It answers the bare object document, which is what
|
|
115
|
+
// the raw leg's unwrap yields, so core gets one shape from either leg.
|
|
116
|
+
// The raw read stays as the fallback for a surface with no such source.
|
|
117
|
+
const loadObjectSchema = typeof dataSource?.getObjectSchema === 'function'
|
|
118
|
+
? (name) => dataSource.getObjectSchema(name)
|
|
119
|
+
: async (name) => {
|
|
120
|
+
const r = await doFetch(`/api/v1/meta/object/${encodeURIComponent(name)}`, {
|
|
121
|
+
headers: { accept: 'application/json' },
|
|
122
|
+
credentials: 'include',
|
|
123
|
+
});
|
|
124
|
+
const doc = await r.json().catch(() => null);
|
|
125
|
+
return doc?.item ?? doc?.data ?? doc;
|
|
126
|
+
};
|
|
114
127
|
// ONE walk for every dimension, memoized per call inside core — sibling
|
|
115
128
|
// dimensions sharing a relationship prefix read that object once, and
|
|
116
129
|
// the base object is seeded so it is never re-read.
|
|
@@ -133,8 +146,10 @@ export function useDatasetDimensionMeta(object, dimensionFields, dimensions, opt
|
|
|
133
146
|
// (objectui#4030 / PR #4324). `apiFetch` comes from the provider's
|
|
134
147
|
// memoized context value, and this hook's own `setState` cannot re-render
|
|
135
148
|
// the provider, so its identity is stable across the effect's own updates.
|
|
149
|
+
// `dataSource` joins for the same reason and from the same value
|
|
150
|
+
// (objectui#13039): it decides which leg the read takes.
|
|
136
151
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
-
}, [signature, apiFetch]);
|
|
152
|
+
}, [signature, apiFetch, dataSource]);
|
|
138
153
|
return meta;
|
|
139
154
|
}
|
|
140
155
|
/**
|
|
@@ -50,8 +50,14 @@ export interface DensityResult {
|
|
|
50
50
|
*
|
|
51
51
|
* @example
|
|
52
52
|
* ```tsx
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* import { DENSITY_MODE_TO_ROW_HEIGHT, rowHeightToDensityMode } from '@object-ui/core';
|
|
54
|
+
*
|
|
55
|
+
* // A view stores the protocol's `rowHeight`; the legacy `densityMode` key is
|
|
56
|
+
* // retired on `NamedListView` (objectui#7924), so persist the spec value —
|
|
57
|
+
* // here into the host's own per-user store (`saveUserRowHeight` is the
|
|
58
|
+
* // host's; the console keeps the choice on the user's own list state).
|
|
59
|
+
* const density = useDensityMode(rowHeightToDensityMode(activeView?.rowHeight) ?? 'comfortable', {
|
|
60
|
+
* onChange: (m) => saveUserRowHeight(vid, DENSITY_MODE_TO_ROW_HEIGHT[m]),
|
|
55
61
|
* });
|
|
56
62
|
* ```
|
|
57
63
|
*/
|
|
@@ -35,8 +35,14 @@ const DENSITY_ORDER = ['compact', 'comfortable', 'spacious'];
|
|
|
35
35
|
*
|
|
36
36
|
* @example
|
|
37
37
|
* ```tsx
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* import { DENSITY_MODE_TO_ROW_HEIGHT, rowHeightToDensityMode } from '@object-ui/core';
|
|
39
|
+
*
|
|
40
|
+
* // A view stores the protocol's `rowHeight`; the legacy `densityMode` key is
|
|
41
|
+
* // retired on `NamedListView` (objectui#7924), so persist the spec value —
|
|
42
|
+
* // here into the host's own per-user store (`saveUserRowHeight` is the
|
|
43
|
+
* // host's; the console keeps the choice on the user's own list state).
|
|
44
|
+
* const density = useDensityMode(rowHeightToDensityMode(activeView?.rowHeight) ?? 'comfortable', {
|
|
45
|
+
* onChange: (m) => saveUserRowHeight(vid, DENSITY_MODE_TO_ROW_HEIGHT[m]),
|
|
40
46
|
* });
|
|
41
47
|
* ```
|
|
42
48
|
*/
|
|
@@ -40,17 +40,8 @@ export interface DiscoveryInfo {
|
|
|
40
40
|
/** Server name and version */
|
|
41
41
|
name?: string;
|
|
42
42
|
version?: string;
|
|
43
|
-
/** Runtime mode (
|
|
43
|
+
/** Runtime mode the server reports (`RuntimeMode` in `@objectstack/spec`) */
|
|
44
44
|
mode?: string;
|
|
45
|
-
/** Preview mode configuration from the kernel (present when mode is 'preview') */
|
|
46
|
-
previewMode?: {
|
|
47
|
-
autoLogin?: boolean;
|
|
48
|
-
simulatedRole?: 'admin' | 'user' | 'viewer';
|
|
49
|
-
simulatedUserName?: string;
|
|
50
|
-
readOnly?: boolean;
|
|
51
|
-
expiresInSeconds?: number;
|
|
52
|
-
bannerMessage?: string;
|
|
53
|
-
};
|
|
54
45
|
/** Service availability status */
|
|
55
46
|
services?: {
|
|
56
47
|
/** Authentication service status */
|
|
@@ -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 { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
+
import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from 'react';
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Defaults
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
@@ -141,9 +141,30 @@ export function useETagCache(userConfig = {}) {
|
|
|
141
141
|
const swUrl = userConfig.serviceWorkerUrl ?? DEFAULTS.serviceWorkerUrl;
|
|
142
142
|
const [cacheSize, setCacheSize] = useState(0);
|
|
143
143
|
const [serviceWorkerActive, setServiceWorkerActive] = useState(false);
|
|
144
|
-
// Keep config in a ref so callbacks always see latest values
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
// Keep config in a ref so callbacks always see latest values. Every reader
|
|
145
|
+
// below is a `useCallback` with `[]` deps whose identity consumers may key
|
|
146
|
+
// effects on, so the config has to reach them without changing it — that is
|
|
147
|
+
// the ref's job. Only the WRITE moves: done in the render body it also ran
|
|
148
|
+
// for renders React discards or replays, publishing config from a tree that
|
|
149
|
+
// never committed. `useInsertionEffect` runs in the mutation phase, ahead of
|
|
150
|
+
// every layout effect, ref attachment and paint, so the sole window deferred
|
|
151
|
+
// is the render phase — where `fetchWithETag` / `clearCache` and the other
|
|
152
|
+
// side effects are not callable anyway.
|
|
153
|
+
//
|
|
154
|
+
// The object itself is built by `useMemo` rather than inline at the two
|
|
155
|
+
// places that consume it. `useRef({ ... })` evaluates its argument on EVERY
|
|
156
|
+
// render and keeps only the first result, so every later render allocated a
|
|
157
|
+
// five-key object that was thrown away (objectui#6817) — the same shape PR
|
|
158
|
+
// objectui#6796 repaired in `useSchemaPersistence`. Its identity is
|
|
159
|
+
// unobservable from outside the hook: the ref is private and every reader
|
|
160
|
+
// only reads fields off `.current`, so a memo React chooses to discard is
|
|
161
|
+
// harmless — it rebuilds an equal object, which is what every render used
|
|
162
|
+
// to do unconditionally.
|
|
163
|
+
const config = useMemo(() => ({ enabled, storage, storagePrefix, maxEntries, ttl }), [enabled, storage, storagePrefix, maxEntries, ttl]);
|
|
164
|
+
const configRef = useRef(config);
|
|
165
|
+
useInsertionEffect(() => {
|
|
166
|
+
configRef.current = config;
|
|
167
|
+
});
|
|
147
168
|
// Hydrate memory cache from localStorage on mount
|
|
148
169
|
useEffect(() => {
|
|
149
170
|
if (storage === 'localStorage') {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* still ignores `view`), and the remaining object-bound blocks will need the same
|
|
20
20
|
* resolution. One resolver in the runtime layer, not one per plugin.
|
|
21
21
|
*/
|
|
22
|
-
import { type ComposedElementDataSource, type ElementDataSourceConfig, type ElementSavedView } from '@object-ui/core';
|
|
22
|
+
import { FilterOperatorError, type ComposedElementDataSource, type ElementDataSourceConfig, type ElementSavedView } from '@object-ui/core';
|
|
23
23
|
/**
|
|
24
24
|
* Where a resolution attempt stands. Distinguishing `loading` from `missing`
|
|
25
25
|
* matters: a component that treated "not resolved yet" as "does not exist" would
|
|
@@ -34,7 +34,12 @@ export type ElementDataSourceStatus =
|
|
|
34
34
|
| 'loading'
|
|
35
35
|
/** The named `view` was found and composed in. */
|
|
36
36
|
| 'resolved'
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The binding cannot be applied: the named `view` does not exist on the
|
|
39
|
+
* object (or could not be read), or a filter it combines is REFUSED by the
|
|
40
|
+
* filter converter — then `filterRefusal` carries the refusal (objectui#10789).
|
|
41
|
+
* Either way `composed` is withheld, so no query runs without the filter.
|
|
42
|
+
*/
|
|
38
43
|
| 'missing';
|
|
39
44
|
export interface UseElementDataSourceResult {
|
|
40
45
|
status: ElementDataSourceStatus;
|
|
@@ -50,6 +55,14 @@ export interface UseElementDataSourceResult {
|
|
|
50
55
|
view?: ElementSavedView;
|
|
51
56
|
/** Author-facing explanation, set only for `missing`. */
|
|
52
57
|
error?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Why the binding could not be composed, when the reason is a refused
|
|
60
|
+
* filter — set only with `missing`, and `error` then carries its message
|
|
61
|
+
* (objectui#10789). A caller that draws the malformed-filter notice reads the
|
|
62
|
+
* refused operator or field off it (`filterRefusalSubject`) rather than
|
|
63
|
+
* scraping `error`.
|
|
64
|
+
*/
|
|
65
|
+
filterRefusal?: FilterOperatorError;
|
|
53
66
|
}
|
|
54
67
|
/**
|
|
55
68
|
* Resolve a schema node's `dataSource` binding, fetching the named saved view.
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* resolution. One resolver in the runtime layer, not one per plugin.
|
|
21
21
|
*/
|
|
22
22
|
import { useContext, useEffect, useMemo, useState } from 'react';
|
|
23
|
-
import { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, isElementDataSourceConfig, resolveSavedView, } from '@object-ui/core';
|
|
23
|
+
import { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, FilterOperatorError, isElementDataSourceConfig, resolveSavedView, } from '@object-ui/core';
|
|
24
24
|
import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
25
25
|
const isRecord = (v) => !!v && typeof v === 'object';
|
|
26
26
|
/**
|
|
@@ -71,6 +71,9 @@ async function fetchSavedViews(dataSource, object) {
|
|
|
71
71
|
* ```
|
|
72
72
|
*/
|
|
73
73
|
export function useElementDataSource(schema, dataSource) {
|
|
74
|
+
// Read AS DECLARED (objectui#7209) — no cast between the hook and the value,
|
|
75
|
+
// so a member the context does not declare is a compile error here. Pinned by
|
|
76
|
+
// `useElementDataSource.schemaRendererContextRead-7209.test.ts`.
|
|
74
77
|
const context = useContext(SchemaRendererContext);
|
|
75
78
|
const adapter = (dataSource ?? context?.dataSource ?? null);
|
|
76
79
|
const config = useMemo(() => {
|
|
@@ -131,11 +134,25 @@ export function useElementDataSource(schema, dataSource) {
|
|
|
131
134
|
return { status: 'loading', config };
|
|
132
135
|
if (resolved.view === null)
|
|
133
136
|
return { status: 'missing', config, error: resolved.error };
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
// objectui#10789 — composing the view's filter with the binding's own is a
|
|
138
|
+
// MERGE, and the merge lowers both through the throwing converter form.
|
|
139
|
+
// This is a render-time `useMemo`, so an uncaught refusal was a render
|
|
140
|
+
// error: the block's error boundary instead of a sentence naming the
|
|
141
|
+
// operator. The refusal is kept as a VALUE and reported as `missing`,
|
|
142
|
+
// which every caller already answers by withholding the query — never as
|
|
143
|
+
// `undefined`, which would read as "no filter" and widen the read. Only a
|
|
144
|
+
// `FilterOperatorError` is caught, the rule `toFilterNodeSafely` states:
|
|
145
|
+
// anything else is a defect, not a statement about the author's filter.
|
|
146
|
+
// (The `ready` branch above composes no view, so nothing is merged there.)
|
|
147
|
+
let composed;
|
|
148
|
+
try {
|
|
149
|
+
composed = composeElementDataSource(config, resolved.view);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (!(error instanceof FilterOperatorError))
|
|
153
|
+
throw error;
|
|
154
|
+
return { status: 'missing', config, error: error.message, filterRefusal: error };
|
|
155
|
+
}
|
|
156
|
+
return { status: 'resolved', config, view: resolved.view, composed };
|
|
140
157
|
}, [config, resolved, requestKey]);
|
|
141
158
|
}
|
|
@@ -60,56 +60,49 @@ export { toPredicateInput } from '@object-ui/core';
|
|
|
60
60
|
* Build the predicate context for a **row record** — the bag to hand
|
|
61
61
|
* {@link useCondition} when the thing being gated is scoped to one record.
|
|
62
62
|
*
|
|
63
|
-
* The row is bound
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* ##
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* `
|
|
85
|
-
*
|
|
86
|
-
* `
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* `
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* full measurement and the layer scoping (`data` stays canonical one layer
|
|
107
|
-
* over, in a metadata-editing form — ADR-0089 D3).
|
|
108
|
-
*
|
|
109
|
-
* `record` and `data` are written AFTER the spread deliberately: a row that
|
|
110
|
-
* happens to carry a field literally named `record` or `data` must not shadow
|
|
111
|
-
* the root every predicate is written against. Same precedence as
|
|
112
|
-
* `evalRowPredicate`.
|
|
63
|
+
* The row is bound ONE way: `record.status` — the canon (objectui#5330, ruled
|
|
64
|
+
* 2026-08-20; Phase 2 executed by `83fe6e741`). This is not a dialect choice;
|
|
65
|
+
* it is one rule, and it is `evalRowPredicate`'s rule
|
|
66
|
+
* (`core/evaluator/listConditional.ts` — the record header, list rows, the row
|
|
67
|
+
* kebab and conditional formatting all evaluate through it), restated here for
|
|
68
|
+
* the `useCondition` tier so both tiers answer an author's `visible:` the same
|
|
69
|
+
* way.
|
|
70
|
+
*
|
|
71
|
+
* ## The two retired spellings (`83fe6e741`)
|
|
72
|
+
*
|
|
73
|
+
* Until Phase 2 this bag also carried the row spread flat (bare `status`, the
|
|
74
|
+
* row-action shorthand) and as `data` (legacy), and `useCondition` below warned
|
|
75
|
+
* once per non-canonical spelling (Phase 1, `d1ab06f0f`). Both bindings and the
|
|
76
|
+
* warning are gone. A bare-field or `data.*` predicate against this bag now
|
|
77
|
+
* faults exactly as it always did on the server (`buildScope({ record })`
|
|
78
|
+
* mounts exactly `['record']`: `Unknown variable: status` / `Unknown variable:
|
|
79
|
+
* data`), and each `useCondition` leg applies its EXISTING fault policy — the
|
|
80
|
+
* throwing legs (`throwOnError`: every action `visible` — `action:button`,
|
|
81
|
+
* `action:menu`, `action:bar`, `DeclaredActionsBar`, and since objectui#11212
|
|
82
|
+
* `action:group`, `action:icon` and the related-list toolbar) hide and report
|
|
83
|
+
* `was hidden/disabled: its predicate threw`, naming the variable; the
|
|
84
|
+
* non-throwing legs fail soft to `true`. The same holds for a legacy `${data.x}` / `${x}` string: one bag
|
|
85
|
+
* shape, both dialects. Nothing detects a retired spelling here; it is simply
|
|
86
|
+
* unbound. `@object-ui/core`'s `evaluator/rowPredicateCanon.ts` carries the
|
|
87
|
+
* canon statement, the server measurement, the layer scoping (`data` stays
|
|
88
|
+
* canonical one layer over, in a metadata-editing form — ADR-0089 D3) and the
|
|
89
|
+
* offline detector.
|
|
90
|
+
*
|
|
91
|
+
* ## Why a helper and not "just `{ record }`" (objectui#4075 / #4080)
|
|
92
|
+
*
|
|
93
|
+
* Because the rule has two halves and every predicate face has to get both:
|
|
94
|
+
* the SHAPE — `record` is the row's one name, so `record.viewer.can_act`, what
|
|
95
|
+
* every declared action on framework's `sys_approval_request` gates on
|
|
96
|
+
* (framework#3310 / objectstack-ai/objectstack#3424), reaches the row — and the NO-ROW case below.
|
|
97
|
+
* `DeclaredActionsBar` and the four generic action renderers once carried
|
|
98
|
+
* inline copies of this bag and drifted (objectui#4077 / #4079); one named
|
|
99
|
+
* helper is what keeps a fifth copy from drifting again.
|
|
100
|
+
*
|
|
101
|
+
* Note the direction of the merge: `useCondition` evaluates on
|
|
102
|
+
* `new ExpressionEvaluator({ ...scope, ...context })`, so this bag's `record`
|
|
103
|
+
* shadows an ambient `record` a host put in the predicate scope — the row is
|
|
104
|
+
* the subject — while every OTHER ambient key (`features`, `user`, a host's own
|
|
105
|
+
* `data`) survives to the predicate as the host's own.
|
|
113
106
|
*
|
|
114
107
|
* This is the BINDING rule only. It deliberately does not touch the evaluation
|
|
115
108
|
* entry — `useCondition` / `toPredicateInput` / `hasDeclaredVisibilityGate`
|
|
@@ -121,10 +114,10 @@ export { toPredicateInput } from '@object-ui/core';
|
|
|
121
114
|
* ## No row → bind NOTHING, do not bind an empty one
|
|
122
115
|
*
|
|
123
116
|
* `null` / `undefined` / a non-object returns an EMPTY bag, not
|
|
124
|
-
* `{ record: {}
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
117
|
+
* `{ record: {} }`. The difference is load-bearing: `useCondition` merges this
|
|
118
|
+
* context OVER the ambient predicate scope, so binding an empty record would
|
|
119
|
+
* blank out a `record` that a host had put in the scope itself — which is
|
|
120
|
+
* exactly how `action:group`'s dropdown leaf is driven in
|
|
128
121
|
* `action-group-dropdown-visible.test.tsx`, and it is a legitimate way for a
|
|
129
122
|
* host to supply the row. "This surface has no row of its own" must stay
|
|
130
123
|
* distinct from "this surface's row is empty"; only the latter is entitled to
|
|
@@ -159,11 +152,11 @@ export declare function useCondition(condition: string | boolean | undefined | {
|
|
|
159
152
|
* routes to `@object-ui/core`'s `evalRowPredicate`: a bare string is CEL (the
|
|
160
153
|
* spec contract for `ActionSchema.visible`), a `{ dialect: 'cel', source }`
|
|
161
154
|
* envelope is always CEL, and only a legacy-dialect string falls back to the
|
|
162
|
-
* old engine (with a deprecation warning). The row is bound as `record.*`
|
|
163
|
-
*
|
|
164
|
-
* `
|
|
165
|
-
* (`features` / `user` / …) is merged alongside so
|
|
166
|
-
* resolving.
|
|
155
|
+
* old engine (with a deprecation warning). The row is bound as `record.*` only
|
|
156
|
+
* — the canon (objectui#5330; the bare-field and `data.*` spellings retired in
|
|
157
|
+
* `83fe6e741` and fault here like any unknown variable); the ambient
|
|
158
|
+
* predicate scope (`features` / `user` / …) is merged alongside so
|
|
159
|
+
* deployment-level gates keep resolving.
|
|
167
160
|
*
|
|
168
161
|
* @param pred The raw predicate: `boolean` (returned as-is), a CEL string,
|
|
169
162
|
* an `{ dialect, source }` envelope, or `null`/`undefined`/`''`.
|
|
@@ -6,12 +6,14 @@
|
|
|
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
|
|
9
|
+
import { ExpressionEvaluator, evalRowPredicate } 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
|
|
13
|
-
* deployment-level flags (`features`, `user`,
|
|
14
|
-
* to import the host shell directly.
|
|
13
|
+
* deployment-level flags (`features`, `user`, …) without needing
|
|
14
|
+
* to import the host shell directly. (⛔ Not `app`: objectui#8155 removed that
|
|
15
|
+
* root from `buildExpressionScope`, so no host shell in this repo publishes
|
|
16
|
+
* one.)
|
|
15
17
|
*
|
|
16
18
|
* `useCondition` / `useExpression` merge this scope under the
|
|
17
19
|
* locally-passed `context`, so per-row `record` overrides still win.
|
|
@@ -74,56 +76,49 @@ export { toPredicateInput } from '@object-ui/core';
|
|
|
74
76
|
* Build the predicate context for a **row record** — the bag to hand
|
|
75
77
|
* {@link useCondition} when the thing being gated is scoped to one record.
|
|
76
78
|
*
|
|
77
|
-
* The row is bound
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
79
|
+
* The row is bound ONE way: `record.status` — the canon (objectui#5330, ruled
|
|
80
|
+
* 2026-08-20; Phase 2 executed by `83fe6e741`). This is not a dialect choice;
|
|
81
|
+
* it is one rule, and it is `evalRowPredicate`'s rule
|
|
82
|
+
* (`core/evaluator/listConditional.ts` — the record header, list rows, the row
|
|
83
|
+
* kebab and conditional formatting all evaluate through it), restated here for
|
|
84
|
+
* the `useCondition` tier so both tiers answer an author's `visible:` the same
|
|
85
|
+
* way.
|
|
84
86
|
*
|
|
85
|
-
* ##
|
|
87
|
+
* ## The two retired spellings (`83fe6e741`)
|
|
86
88
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
89
|
+
* Until Phase 2 this bag also carried the row spread flat (bare `status`, the
|
|
90
|
+
* row-action shorthand) and as `data` (legacy), and `useCondition` below warned
|
|
91
|
+
* once per non-canonical spelling (Phase 1, `d1ab06f0f`). Both bindings and the
|
|
92
|
+
* warning are gone. A bare-field or `data.*` predicate against this bag now
|
|
93
|
+
* faults exactly as it always did on the server (`buildScope({ record })`
|
|
94
|
+
* mounts exactly `['record']`: `Unknown variable: status` / `Unknown variable:
|
|
95
|
+
* data`), and each `useCondition` leg applies its EXISTING fault policy — the
|
|
96
|
+
* throwing legs (`throwOnError`: every action `visible` — `action:button`,
|
|
97
|
+
* `action:menu`, `action:bar`, `DeclaredActionsBar`, and since objectui#11212
|
|
98
|
+
* `action:group`, `action:icon` and the related-list toolbar) hide and report
|
|
99
|
+
* `was hidden/disabled: its predicate threw`, naming the variable; the
|
|
100
|
+
* non-throwing legs fail soft to `true`. The same holds for a legacy `${data.x}` / `${x}` string: one bag
|
|
101
|
+
* shape, both dialects. Nothing detects a retired spelling here; it is simply
|
|
102
|
+
* unbound. `@object-ui/core`'s `evaluator/rowPredicateCanon.ts` carries the
|
|
103
|
+
* canon statement, the server measurement, the layer scoping (`data` stays
|
|
104
|
+
* canonical one layer over, in a metadata-editing form — ADR-0089 D3) and the
|
|
105
|
+
* offline detector.
|
|
96
106
|
*
|
|
97
|
-
*
|
|
98
|
-
* `sys_approval_request` gates on `record.viewer.*` (framework#3310 / #3424),
|
|
99
|
-
* so the whole server-declared approval decision set was invisible wherever
|
|
100
|
-
* `DeclaredActionsBar` rendered until objectui#4077 bound the row this way —
|
|
101
|
-
* and the four generic action renderers carried the same root-only binding
|
|
102
|
-
* until objectui#4075.
|
|
107
|
+
* ## Why a helper and not "just `{ record }`" (objectui#4075 / #4080)
|
|
103
108
|
*
|
|
104
|
-
*
|
|
109
|
+
* Because the rule has two halves and every predicate face has to get both:
|
|
110
|
+
* the SHAPE — `record` is the row's one name, so `record.viewer.can_act`, what
|
|
111
|
+
* every declared action on framework's `sys_approval_request` gates on
|
|
112
|
+
* (framework#3310 / objectstack-ai/objectstack#3424), reaches the row — and the NO-ROW case below.
|
|
113
|
+
* `DeclaredActionsBar` and the four generic action renderers once carried
|
|
114
|
+
* inline copies of this bag and drifted (objectui#4077 / #4079); one named
|
|
115
|
+
* helper is what keeps a fifth copy from drifting again.
|
|
105
116
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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
|
-
*
|
|
123
|
-
* `record` and `data` are written AFTER the spread deliberately: a row that
|
|
124
|
-
* happens to carry a field literally named `record` or `data` must not shadow
|
|
125
|
-
* the root every predicate is written against. Same precedence as
|
|
126
|
-
* `evalRowPredicate`.
|
|
117
|
+
* Note the direction of the merge: `useCondition` evaluates on
|
|
118
|
+
* `new ExpressionEvaluator({ ...scope, ...context })`, so this bag's `record`
|
|
119
|
+
* shadows an ambient `record` a host put in the predicate scope — the row is
|
|
120
|
+
* the subject — while every OTHER ambient key (`features`, `user`, a host's own
|
|
121
|
+
* `data`) survives to the predicate as the host's own.
|
|
127
122
|
*
|
|
128
123
|
* This is the BINDING rule only. It deliberately does not touch the evaluation
|
|
129
124
|
* entry — `useCondition` / `toPredicateInput` / `hasDeclaredVisibilityGate`
|
|
@@ -135,10 +130,10 @@ export { toPredicateInput } from '@object-ui/core';
|
|
|
135
130
|
* ## No row → bind NOTHING, do not bind an empty one
|
|
136
131
|
*
|
|
137
132
|
* `null` / `undefined` / a non-object returns an EMPTY bag, not
|
|
138
|
-
* `{ record: {}
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
133
|
+
* `{ record: {} }`. The difference is load-bearing: `useCondition` merges this
|
|
134
|
+
* context OVER the ambient predicate scope, so binding an empty record would
|
|
135
|
+
* blank out a `record` that a host had put in the scope itself — which is
|
|
136
|
+
* exactly how `action:group`'s dropdown leaf is driven in
|
|
142
137
|
* `action-group-dropdown-visible.test.tsx`, and it is a legitimate way for a
|
|
143
138
|
* host to supply the row. "This surface has no row of its own" must stay
|
|
144
139
|
* distinct from "this surface's row is empty"; only the latter is entitled to
|
|
@@ -150,8 +145,7 @@ export function usePredicateRecordContext(record) {
|
|
|
150
145
|
return useMemo(() => {
|
|
151
146
|
if (record == null || typeof record !== 'object' || Array.isArray(record))
|
|
152
147
|
return {};
|
|
153
|
-
|
|
154
|
-
return { ...row, record: row, data: row };
|
|
148
|
+
return { record: record };
|
|
155
149
|
}, [record]);
|
|
156
150
|
}
|
|
157
151
|
/**
|
|
@@ -188,37 +182,11 @@ export function useCondition(condition, context = {}, options) {
|
|
|
188
182
|
const scope = usePredicateScope();
|
|
189
183
|
// We evaluate directly without caching the evaluator to avoid issues with context changes
|
|
190
184
|
return useMemo(() => {
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// `usePredicateRecordContext`
|
|
194
|
-
//
|
|
195
|
-
//
|
|
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
|
-
}
|
|
185
|
+
// No row-spelling detector on this tier (`83fe6e741` removed the
|
|
186
|
+
// Phase-1 warning with the bindings): a retired bare-field / `data.*`
|
|
187
|
+
// spelling against a `usePredicateRecordContext` bag is simply unbound
|
|
188
|
+
// and faults like any other unknown variable, and each leg's existing
|
|
189
|
+
// fault report below is what names it.
|
|
222
190
|
const evaluator = new ExpressionEvaluator({ ...scope, ...context });
|
|
223
191
|
if (options?.throwOnError) {
|
|
224
192
|
// Fail-closed: a predicate that can't be evaluated hides/disables
|
|
@@ -256,11 +224,11 @@ export function useCondition(condition, context = {}, options) {
|
|
|
256
224
|
* routes to `@object-ui/core`'s `evalRowPredicate`: a bare string is CEL (the
|
|
257
225
|
* spec contract for `ActionSchema.visible`), a `{ dialect: 'cel', source }`
|
|
258
226
|
* envelope is always CEL, and only a legacy-dialect string falls back to the
|
|
259
|
-
* old engine (with a deprecation warning). The row is bound as `record.*`
|
|
260
|
-
*
|
|
261
|
-
* `
|
|
262
|
-
* (`features` / `user` / …) is merged alongside so
|
|
263
|
-
* resolving.
|
|
227
|
+
* old engine (with a deprecation warning). The row is bound as `record.*` only
|
|
228
|
+
* — the canon (objectui#5330; the bare-field and `data.*` spellings retired in
|
|
229
|
+
* `83fe6e741` and fault here like any unknown variable); the ambient
|
|
230
|
+
* predicate scope (`features` / `user` / …) is merged alongside so
|
|
231
|
+
* deployment-level gates keep resolving.
|
|
264
232
|
*
|
|
265
233
|
* @param pred The raw predicate: `boolean` (returned as-is), a CEL string,
|
|
266
234
|
* an `{ dialect, source }` envelope, or `null`/`undefined`/`''`.
|