@object-ui/react 17.5.0 → 17.7.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 +4100 -0
- package/README.md +303 -46
- package/dist/LazyPluginLoader.d.ts +14 -7
- package/dist/LazyPluginLoader.js +14 -7
- package/dist/SchemaRenderer.d.ts +38 -7
- package/dist/SchemaRenderer.js +1603 -60
- package/dist/context/ActionContext.js +40 -1
- package/dist/context/DiscussionContext.d.ts +12 -0
- package/dist/context/DiscussionContext.js +2 -0
- package/dist/context/DrillNavigationContext.d.ts +13 -0
- package/dist/context/HostNavigationContext.d.ts +82 -0
- package/dist/context/HostNavigationContext.js +65 -0
- package/dist/context/InlineEditContext.d.ts +81 -1
- package/dist/context/InlineEditContext.js +8 -2
- package/dist/context/RecordContext.d.ts +48 -5
- package/dist/context/RecordContext.js +10 -1
- package/dist/context/RelatedRecordActionsContext.d.ts +20 -1
- package/dist/context/SchemaRendererContext.d.ts +54 -2
- package/dist/context/SchemaRendererContext.js +38 -5
- 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 +13 -12
- package/dist/context/index.js +13 -12
- package/dist/element-data-source/ElementDataSourceGate.d.ts +163 -3
- package/dist/element-data-source/ElementDataSourceGate.js +407 -15
- package/dist/hooks/index.d.ts +37 -35
- package/dist/hooks/index.js +44 -35
- package/dist/hooks/useActionEngine.js +47 -4
- package/dist/hooks/useActionRunner.js +4 -0
- package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
- package/dist/hooks/useActionTextLocalizer.js +33 -8
- package/dist/hooks/useCapabilityGate.js +6 -5
- package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
- package/dist/hooks/useConfigBagEvaluator.js +39 -0
- package/dist/hooks/useCrudShortcuts.js +1 -1
- package/dist/hooks/useDatasetDimensionLabels.js +1 -1
- package/dist/hooks/useDensityMode.d.ts +6 -2
- package/dist/hooks/useDensityMode.js +6 -2
- package/dist/hooks/useDiscovery.d.ts +1 -10
- package/dist/hooks/useDiscovery.js +1 -1
- package/dist/hooks/useETagCache.js +25 -4
- package/dist/hooks/useElementDataSource.d.ts +15 -2
- package/dist/hooks/useElementDataSource.js +29 -9
- package/dist/hooks/useExpression.d.ts +52 -38
- package/dist/hooks/useExpression.js +62 -42
- 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 +77 -12
- package/dist/hooks/useNavigationOverlay.js +119 -9
- package/dist/hooks/useOffline.js +65 -11
- package/dist/hooks/usePageAssignment.d.ts +8 -5
- package/dist/hooks/usePageAssignment.js +38 -17
- package/dist/hooks/usePageTransition.js +1 -1
- package/dist/hooks/usePageVariableActionBridge.js +2 -2
- 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 +7 -2
- package/dist/hooks/useViewData.js +1 -1
- package/dist/hooks/useViewTransition.js +1 -1
- package/dist/index.d.ts +18 -11
- package/dist/index.js +54 -10
- package/dist/schema-input.d.ts +55 -14
- package/dist/schema-input.js +55 -12
- package/dist/utils/configBag.d.ts +68 -0
- package/dist/utils/configBag.js +70 -0
- package/dist/utils/error-message.d.ts +59 -0
- package/dist/utils/error-message.js +116 -1
- package/dist/utils/inlineAria.d.ts +61 -0
- package/dist/utils/inlineAria.js +70 -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 +421 -0
- package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
- package/dist/utils/refusedDataPropDiagnostic.js +94 -0
- package/dist/utils/unevaluatedExpression.d.ts +110 -0
- package/dist/utils/unevaluatedExpression.js +257 -0
- package/dist/utils/visibilityDiagnostic.d.ts +371 -0
- package/dist/utils/visibilityDiagnostic.js +551 -0
- 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 -82
- package/dist/spec-bridge/bridges/form-view.js +0 -126
- package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
- package/dist/spec-bridge/bridges/list-view.js +0 -241
- 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
|
@@ -12,6 +12,40 @@
|
|
|
12
12
|
* usable can be extracted, so callers fall back to a generic localized string.
|
|
13
13
|
*/
|
|
14
14
|
export declare function extractWriteErrorMessage(err: unknown): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* The refusal text the PRODUCER explicitly marked as addressed to the end user,
|
|
17
|
+
* or `null` when the refusal carries no marking.
|
|
18
|
+
*
|
|
19
|
+
* `userMessage` is the opt-in channel added by objectstack `79c46da90` (maintainer
|
|
20
|
+
* ruling 2026-08-19 on objectui#5210), declared on `ApiErrorSchema` /
|
|
21
|
+
* `EnhancedApiErrorSchema` in the pinned `@objectstack/spec`.
|
|
22
|
+
* Three properties of the contract decide this reader's whole shape:
|
|
23
|
+
*
|
|
24
|
+
* - **Presence IS the marking.** The mark and the marked text are ONE value —
|
|
25
|
+
* a field carrying the text, not a boolean beside `message` — so no boundary
|
|
26
|
+
* that rewraps or substitutes `message` can promote platform prose into the
|
|
27
|
+
* marked channel. Platform and driver code never set it.
|
|
28
|
+
* - **Status-agnostic.** Not a 403 special case; any refusal status may carry
|
|
29
|
+
* it. Callers must not gate this read on a status.
|
|
30
|
+
* - **Verbatim.** The author wrote it for their user, already localized. It is
|
|
31
|
+
* returned untouched — none of {@link extractWriteErrorMessage}'s prefix
|
|
32
|
+
* stripping applies, because there is no machine prefix to strip.
|
|
33
|
+
*
|
|
34
|
+
* Read from the two places the adapter boundary parks the envelope, both under
|
|
35
|
+
* the SAME declared key: the error itself (`@objectstack/client` lifts a marked
|
|
36
|
+
* body onto `err.userMessage`, from either wire dialect) and `details`, which
|
|
37
|
+
* the client falls back to the whole response body — the identical pair
|
|
38
|
+
* {@link isPermissionError} reads `code` from. Deliberately NOT read: the raw
|
|
39
|
+
* JSON tail `ApiDataSource` bakes into its message string. Scraping a marking
|
|
40
|
+
* out of a transport's prose is the consumer-side guessing objectstack#3821
|
|
41
|
+
* exists to prevent; a transport that flattens the envelope should carry the
|
|
42
|
+
* field instead.
|
|
43
|
+
*
|
|
44
|
+
* Everything unmarked answers `null`, so callers keep their generic localized
|
|
45
|
+
* substitution and objectstack#3821's protection holds BY CONSTRUCTION: this
|
|
46
|
+
* function can never return text the producer did not opt in.
|
|
47
|
+
*/
|
|
48
|
+
export declare function declaredUserMessage(err: unknown): string | null;
|
|
15
49
|
/** One field the server rejected, normalised for `form.setError`. */
|
|
16
50
|
export interface WriteFieldError {
|
|
17
51
|
/** Field name, matching the form control's `name`. */
|
|
@@ -50,3 +84,28 @@ export declare function extractFieldErrors(err: unknown): WriteFieldError[] | nu
|
|
|
50
84
|
* the raw server text.
|
|
51
85
|
*/
|
|
52
86
|
export declare function isPermissionError(err: unknown): boolean;
|
|
87
|
+
/** What {@link classifyLoadError} answers. */
|
|
88
|
+
export type LoadErrorKind = 'api-disabled' | 'forbidden' | 'unauthorized' | 'rejected' | 'network';
|
|
89
|
+
/**
|
|
90
|
+
* Classify a failed data FETCH (as opposed to a failed write, above) by HTTP
|
|
91
|
+
* status / error code, so an error panel can say what actually happened. A
|
|
92
|
+
* 403 rendered as "check your connection" is indistinguishable from a real
|
|
93
|
+
* outage — users were told to debug their network when the server had
|
|
94
|
+
* (correctly) denied them access.
|
|
95
|
+
*
|
|
96
|
+
* Originally `ListView`'s module-local `classifyLoadError`
|
|
97
|
+
* (`packages/plugin-list/src/ListView.tsx`); lifted here (objectui#4693) so
|
|
98
|
+
* `RecordAttachmentsPanel` can reuse the same "is this retry-invariant"
|
|
99
|
+
* verdict for the `sys_attachment` list read instead of re-deriving it. Both
|
|
100
|
+
* `@object-ui/plugin-list` and `@object-ui/app-shell` already depend on this
|
|
101
|
+
* package, so the lift adds no new dependency edge to either.
|
|
102
|
+
*
|
|
103
|
+
* Purely a classifier — it returns a KIND, never rendered text. Each caller
|
|
104
|
+
* owns its own copy for what a kind means on its surface (`ListView` shows a
|
|
105
|
+
* title + message per kind and a Retry for every kind but `api-disabled`;
|
|
106
|
+
* `RecordAttachmentsPanel` only distinguishes `api-disabled` from everything
|
|
107
|
+
* else, folding `forbidden`/`unauthorized`/`rejected`/`network` into its own
|
|
108
|
+
* coarser `denied`/`unavailable` split via {@link isPermissionError}) — so
|
|
109
|
+
* lifting this function does not couple the two surfaces' i18n or copy.
|
|
110
|
+
*/
|
|
111
|
+
export declare function classifyLoadError(err: unknown): LoadErrorKind;
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
-
* Helpers for turning a failed data-source write into user-facing
|
|
9
|
+
* Helpers for turning a failed data-source read or write into user-facing
|
|
10
|
+
* feedback.
|
|
10
11
|
*
|
|
11
12
|
* Different DataSource adapters throw differently shaped errors:
|
|
12
13
|
* - the ObjectStack adapter (`@object-ui/data-objectstack`) decorates the
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
* These helpers normalise across both so a caller's `catch` can surface the
|
|
19
20
|
* real reason (e.g. a row-level-security denial) instead of swallowing it.
|
|
20
21
|
*/
|
|
22
|
+
import { isApiAccessDeniedError } from '@object-ui/data-objectstack';
|
|
21
23
|
function firstString(...vals) {
|
|
22
24
|
for (const v of vals) {
|
|
23
25
|
if (typeof v === 'string' && v.trim())
|
|
@@ -68,6 +70,54 @@ export function extractWriteErrorMessage(err) {
|
|
|
68
70
|
.trim();
|
|
69
71
|
return clean || null;
|
|
70
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The refusal text the PRODUCER explicitly marked as addressed to the end user,
|
|
75
|
+
* or `null` when the refusal carries no marking.
|
|
76
|
+
*
|
|
77
|
+
* `userMessage` is the opt-in channel added by objectstack `79c46da90` (maintainer
|
|
78
|
+
* ruling 2026-08-19 on objectui#5210), declared on `ApiErrorSchema` /
|
|
79
|
+
* `EnhancedApiErrorSchema` in the pinned `@objectstack/spec`.
|
|
80
|
+
* Three properties of the contract decide this reader's whole shape:
|
|
81
|
+
*
|
|
82
|
+
* - **Presence IS the marking.** The mark and the marked text are ONE value —
|
|
83
|
+
* a field carrying the text, not a boolean beside `message` — so no boundary
|
|
84
|
+
* that rewraps or substitutes `message` can promote platform prose into the
|
|
85
|
+
* marked channel. Platform and driver code never set it.
|
|
86
|
+
* - **Status-agnostic.** Not a 403 special case; any refusal status may carry
|
|
87
|
+
* it. Callers must not gate this read on a status.
|
|
88
|
+
* - **Verbatim.** The author wrote it for their user, already localized. It is
|
|
89
|
+
* returned untouched — none of {@link extractWriteErrorMessage}'s prefix
|
|
90
|
+
* stripping applies, because there is no machine prefix to strip.
|
|
91
|
+
*
|
|
92
|
+
* Read from the two places the adapter boundary parks the envelope, both under
|
|
93
|
+
* the SAME declared key: the error itself (`@objectstack/client` lifts a marked
|
|
94
|
+
* body onto `err.userMessage`, from either wire dialect) and `details`, which
|
|
95
|
+
* the client falls back to the whole response body — the identical pair
|
|
96
|
+
* {@link isPermissionError} reads `code` from. Deliberately NOT read: the raw
|
|
97
|
+
* JSON tail `ApiDataSource` bakes into its message string. Scraping a marking
|
|
98
|
+
* out of a transport's prose is the consumer-side guessing objectstack#3821
|
|
99
|
+
* exists to prevent; a transport that flattens the envelope should carry the
|
|
100
|
+
* field instead.
|
|
101
|
+
*
|
|
102
|
+
* Everything unmarked answers `null`, so callers keep their generic localized
|
|
103
|
+
* substitution and objectstack#3821's protection holds BY CONSTRUCTION: this
|
|
104
|
+
* function can never return text the producer did not opt in.
|
|
105
|
+
*/
|
|
106
|
+
export function declaredUserMessage(err) {
|
|
107
|
+
if (!err || typeof err !== 'object')
|
|
108
|
+
return null;
|
|
109
|
+
const e = err;
|
|
110
|
+
// Non-empty after trimming, returned untrimmed — the same predicate
|
|
111
|
+
// `@objectstack/client` applies when it lifts the field off the wire.
|
|
112
|
+
if (typeof e.userMessage === 'string' && e.userMessage.trim())
|
|
113
|
+
return e.userMessage;
|
|
114
|
+
const details = e.details;
|
|
115
|
+
if (details && typeof details === 'object'
|
|
116
|
+
&& typeof details.userMessage === 'string' && details.userMessage.trim()) {
|
|
117
|
+
return details.userMessage;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
71
121
|
/**
|
|
72
122
|
* Per-field errors from a rejected write, or `null` when the failure was not
|
|
73
123
|
* field-scoped.
|
|
@@ -136,3 +186,68 @@ export function isPermissionError(err) {
|
|
|
136
186
|
const text = `${typeof e.message === 'string' ? e.message : ''} ${e.details?.error ?? e.error ?? ''}`;
|
|
137
187
|
return /row-level security|access denied|permission denied|not permitted/i.test(text);
|
|
138
188
|
}
|
|
189
|
+
/** 400-class error codes the data API returns for a request it will never accept. */
|
|
190
|
+
const REJECTED_REQUEST_CODES = new Set([
|
|
191
|
+
'INVALID_FILTER',
|
|
192
|
+
'UNSUPPORTED_QUERY_PARAM',
|
|
193
|
+
'INVALID_QUERY',
|
|
194
|
+
]);
|
|
195
|
+
/**
|
|
196
|
+
* Classify a failed data FETCH (as opposed to a failed write, above) by HTTP
|
|
197
|
+
* status / error code, so an error panel can say what actually happened. A
|
|
198
|
+
* 403 rendered as "check your connection" is indistinguishable from a real
|
|
199
|
+
* outage — users were told to debug their network when the server had
|
|
200
|
+
* (correctly) denied them access.
|
|
201
|
+
*
|
|
202
|
+
* Originally `ListView`'s module-local `classifyLoadError`
|
|
203
|
+
* (`packages/plugin-list/src/ListView.tsx`); lifted here (objectui#4693) so
|
|
204
|
+
* `RecordAttachmentsPanel` can reuse the same "is this retry-invariant"
|
|
205
|
+
* verdict for the `sys_attachment` list read instead of re-deriving it. Both
|
|
206
|
+
* `@object-ui/plugin-list` and `@object-ui/app-shell` already depend on this
|
|
207
|
+
* package, so the lift adds no new dependency edge to either.
|
|
208
|
+
*
|
|
209
|
+
* Purely a classifier — it returns a KIND, never rendered text. Each caller
|
|
210
|
+
* owns its own copy for what a kind means on its surface (`ListView` shows a
|
|
211
|
+
* title + message per kind and a Retry for every kind but `api-disabled`;
|
|
212
|
+
* `RecordAttachmentsPanel` only distinguishes `api-disabled` from everything
|
|
213
|
+
* else, folding `forbidden`/`unauthorized`/`rejected`/`network` into its own
|
|
214
|
+
* coarser `denied`/`unavailable` split via {@link isPermissionError}) — so
|
|
215
|
+
* lifting this function does not couple the two surfaces' i18n or copy.
|
|
216
|
+
*/
|
|
217
|
+
export function classifyLoadError(err) {
|
|
218
|
+
const e = err;
|
|
219
|
+
// The ObjectStack client decorates errors with `httpStatus`; raw fetch
|
|
220
|
+
// wrappers surface `status` / `statusCode`; some adapters only embed the
|
|
221
|
+
// status in the message ("HTTP 403 Forbidden — …").
|
|
222
|
+
let status = [e?.httpStatus, e?.status, e?.statusCode]
|
|
223
|
+
.find((s) => typeof s === 'number');
|
|
224
|
+
if (status === undefined) {
|
|
225
|
+
const m = /HTTP (\d{3})\b/.exec(String(e?.message ?? ''));
|
|
226
|
+
if (m)
|
|
227
|
+
status = Number(m[1]);
|
|
228
|
+
}
|
|
229
|
+
const code = typeof e?.code === 'string' ? e.code.toUpperCase() : '';
|
|
230
|
+
// The object's `enable` block withholds this operation — checked FIRST, on
|
|
231
|
+
// the CODE alone (delegated to the shared predicate so this file and
|
|
232
|
+
// `@object-ui/data-objectstack` cannot drift on which codes count). Status
|
|
233
|
+
// cannot carry this verdict: the denial is a 404, the same status a missing
|
|
234
|
+
// collection and a missing record answer with, and its 405 sibling is the
|
|
235
|
+
// same status any other method rejection uses. Unlike every kind below it
|
|
236
|
+
// this one is not about the request, the session or the network — it is a
|
|
237
|
+
// permanent property of the object, identical for every persona and every
|
|
238
|
+
// retry (objectui#4408).
|
|
239
|
+
if (isApiAccessDeniedError(err))
|
|
240
|
+
return 'api-disabled';
|
|
241
|
+
if (status === 403 || code === 'PERMISSION_DENIED' || code === 'FORBIDDEN')
|
|
242
|
+
return 'forbidden';
|
|
243
|
+
if (status === 401 || code === 'UNAUTHORIZED' || code === 'UNAUTHENTICATED')
|
|
244
|
+
return 'unauthorized';
|
|
245
|
+
// The server understood the request and refused it as malformed. Retrying
|
|
246
|
+
// sends the identical bad request, so "check your connection and try again"
|
|
247
|
+
// is the same wrong advice this function exists to stop giving — one status
|
|
248
|
+
// code over. The server now rejects a `$filter` that is not a filter AST
|
|
249
|
+
// (objectstack#4121) and an unsupported `$`-parameter, both as 400.
|
|
250
|
+
if (status === 400 || REJECTED_REQUEST_CODES.has(code))
|
|
251
|
+
return 'rejected';
|
|
252
|
+
return 'network';
|
|
253
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
import type { AriaProps } from '@objectstack/spec/ui';
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the spec's NESTED `aria` bag to the DOM attributes it declares
|
|
11
|
+
* (objectui#11051).
|
|
12
|
+
*
|
|
13
|
+
* The bag is `@objectstack/spec`'s `AriaPropsSchema`, the `aria` member a
|
|
14
|
+
* block's own props schema carries (`element:text`, `element:button`,
|
|
15
|
+
* `element:image` and `element:number` among them). It declares three keys,
|
|
16
|
+
* and each maps to one attribute:
|
|
17
|
+
*
|
|
18
|
+
* `ariaLabel` → `aria-label`, resolved with the spec's own
|
|
19
|
+
* `resolveI18nLabel` against `locale`: a plain string is
|
|
20
|
+
* used as written, and an inline locale map
|
|
21
|
+
* (`{ en: 'Order total', 'zh-CN': '订单合计' }`) gives the
|
|
22
|
+
* entry for the viewer's language;
|
|
23
|
+
* `ariaDescribedBy` → `aria-describedby`;
|
|
24
|
+
* `role` → `role`.
|
|
25
|
+
*
|
|
26
|
+
* A key that resolves to nothing, or to an empty string, is left out of the
|
|
27
|
+
* result instead of being written as an empty or `undefined` attribute. A key
|
|
28
|
+
* the bag does not declare is not read: the spec refuses such keys when the
|
|
29
|
+
* document is parsed, so this function adds no alias for them.
|
|
30
|
+
*
|
|
31
|
+
* ## Two vocabularies, two readers (objectui#4580 Q2-B)
|
|
32
|
+
*
|
|
33
|
+
* This is NOT `SchemaRenderer`'s `resolveAriaProps`. That one reads the FLAT
|
|
34
|
+
* node keys (`BaseSchema.ariaLabel` / `ariaDescribedBy` / `role`) and resolves
|
|
35
|
+
* `ariaLabel` in objectui's KEYED vocabulary (`{ key, defaultValue?, params? }`,
|
|
36
|
+
* via `resolveKeyedI18nLabel`). The nested bag uses the INLINE locale map.
|
|
37
|
+
* Neither resolver accepts the other's shape: the keyed one returns `undefined`
|
|
38
|
+
* for a locale map, and the spec's one treats `key` / `defaultValue` as locale
|
|
39
|
+
* tags. So the two readers stay separate, as objectui#4580 Q2-B ruled.
|
|
40
|
+
*
|
|
41
|
+
* ## Why a pure function and not a hook
|
|
42
|
+
*
|
|
43
|
+
* The caller passes the locale; this function reads no React context. That
|
|
44
|
+
* lets a renderer call it anywhere in its body, including after an early
|
|
45
|
+
* return, and lets code outside React call it too. Renderers pass
|
|
46
|
+
* `useDisplayLocale()` from `@object-ui/i18n`, the locale every
|
|
47
|
+
* `resolveI18nLabel` read site in this repo resolves against (objectui#4580
|
|
48
|
+
* revised Q1-A). Like the spec's resolver, `locale` is a required parameter
|
|
49
|
+
* that may be `undefined` (which resolves as `en`), so a caller cannot forget
|
|
50
|
+
* it by accident.
|
|
51
|
+
*
|
|
52
|
+
* @param aria - The authored bag, taken directly from the node's config. A
|
|
53
|
+
* missing bag, or a value that is not an object, gives `{}`.
|
|
54
|
+
* @param locale - BCP-47 tag of the viewer's language, e.g. `zh-CN`.
|
|
55
|
+
* @returns The attributes to spread onto the element that carries the name.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveInlineAriaProps(aria: AriaProps | null | undefined, locale: string | undefined): {
|
|
58
|
+
'aria-label'?: string;
|
|
59
|
+
'aria-describedby'?: string;
|
|
60
|
+
role?: string;
|
|
61
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
import { resolveI18nLabel as resolveInlineI18nLabel } from '@objectstack/spec/ui';
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the spec's NESTED `aria` bag to the DOM attributes it declares
|
|
11
|
+
* (objectui#11051).
|
|
12
|
+
*
|
|
13
|
+
* The bag is `@objectstack/spec`'s `AriaPropsSchema`, the `aria` member a
|
|
14
|
+
* block's own props schema carries (`element:text`, `element:button`,
|
|
15
|
+
* `element:image` and `element:number` among them). It declares three keys,
|
|
16
|
+
* and each maps to one attribute:
|
|
17
|
+
*
|
|
18
|
+
* `ariaLabel` → `aria-label`, resolved with the spec's own
|
|
19
|
+
* `resolveI18nLabel` against `locale`: a plain string is
|
|
20
|
+
* used as written, and an inline locale map
|
|
21
|
+
* (`{ en: 'Order total', 'zh-CN': '订单合计' }`) gives the
|
|
22
|
+
* entry for the viewer's language;
|
|
23
|
+
* `ariaDescribedBy` → `aria-describedby`;
|
|
24
|
+
* `role` → `role`.
|
|
25
|
+
*
|
|
26
|
+
* A key that resolves to nothing, or to an empty string, is left out of the
|
|
27
|
+
* result instead of being written as an empty or `undefined` attribute. A key
|
|
28
|
+
* the bag does not declare is not read: the spec refuses such keys when the
|
|
29
|
+
* document is parsed, so this function adds no alias for them.
|
|
30
|
+
*
|
|
31
|
+
* ## Two vocabularies, two readers (objectui#4580 Q2-B)
|
|
32
|
+
*
|
|
33
|
+
* This is NOT `SchemaRenderer`'s `resolveAriaProps`. That one reads the FLAT
|
|
34
|
+
* node keys (`BaseSchema.ariaLabel` / `ariaDescribedBy` / `role`) and resolves
|
|
35
|
+
* `ariaLabel` in objectui's KEYED vocabulary (`{ key, defaultValue?, params? }`,
|
|
36
|
+
* via `resolveKeyedI18nLabel`). The nested bag uses the INLINE locale map.
|
|
37
|
+
* Neither resolver accepts the other's shape: the keyed one returns `undefined`
|
|
38
|
+
* for a locale map, and the spec's one treats `key` / `defaultValue` as locale
|
|
39
|
+
* tags. So the two readers stay separate, as objectui#4580 Q2-B ruled.
|
|
40
|
+
*
|
|
41
|
+
* ## Why a pure function and not a hook
|
|
42
|
+
*
|
|
43
|
+
* The caller passes the locale; this function reads no React context. That
|
|
44
|
+
* lets a renderer call it anywhere in its body, including after an early
|
|
45
|
+
* return, and lets code outside React call it too. Renderers pass
|
|
46
|
+
* `useDisplayLocale()` from `@object-ui/i18n`, the locale every
|
|
47
|
+
* `resolveI18nLabel` read site in this repo resolves against (objectui#4580
|
|
48
|
+
* revised Q1-A). Like the spec's resolver, `locale` is a required parameter
|
|
49
|
+
* that may be `undefined` (which resolves as `en`), so a caller cannot forget
|
|
50
|
+
* it by accident.
|
|
51
|
+
*
|
|
52
|
+
* @param aria - The authored bag, taken directly from the node's config. A
|
|
53
|
+
* missing bag, or a value that is not an object, gives `{}`.
|
|
54
|
+
* @param locale - BCP-47 tag of the viewer's language, e.g. `zh-CN`.
|
|
55
|
+
* @returns The attributes to spread onto the element that carries the name.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveInlineAriaProps(aria, locale) {
|
|
58
|
+
if (!aria || typeof aria !== 'object')
|
|
59
|
+
return {};
|
|
60
|
+
const out = {};
|
|
61
|
+
const label = resolveInlineI18nLabel(aria.ariaLabel, locale);
|
|
62
|
+
if (label)
|
|
63
|
+
out['aria-label'] = label;
|
|
64
|
+
if (typeof aria.ariaDescribedBy === 'string' && aria.ariaDescribedBy) {
|
|
65
|
+
out['aria-describedby'] = aria.ariaDescribedBy;
|
|
66
|
+
}
|
|
67
|
+
if (typeof aria.role === 'string' && aria.role)
|
|
68
|
+
out.role = aria.role;
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import type { NonGridCeilingResult } from '@object-ui/core';
|
|
10
|
+
/**
|
|
11
|
+
* The loud footnote a non-grid view shows when it drew only the first
|
|
12
|
+
* `NON_GRID_ROW_CEILING` rows of a larger result set (objectui#7210).
|
|
13
|
+
*
|
|
14
|
+
* Placement and tone follow objectui#7148's chart footnote — a `role="note"`
|
|
15
|
+
* line in muted small type directly under the visualisation, naming BOTH
|
|
16
|
+
* numbers, because the count is the half a reader cannot recover from the
|
|
17
|
+
* picture. A truncated schedule renders as a healthy, confident schedule of a
|
|
18
|
+
* fraction of itself; "some rows are missing" leaves it indistinguishable from
|
|
19
|
+
* a complete one, and `2,000 of 40,000` is the bit that was missing.
|
|
20
|
+
*
|
|
21
|
+
* Two sentences because there are two conditions: a known total states the
|
|
22
|
+
* fact with both numbers; an adapter that reported no `total` still gets a
|
|
23
|
+
* DEFINITE sentence (the probe row proves more rows exist), it simply cannot
|
|
24
|
+
* name how many.
|
|
25
|
+
*
|
|
26
|
+
* ## It takes the RESULT, and nothing else (objectui#7508, ruling A′)
|
|
27
|
+
*
|
|
28
|
+
* The only prop is the `NonGridCeilingResult` that `applyNonGridRowCeiling`
|
|
29
|
+
* returned. Every number the note prints is read off that one object — the
|
|
30
|
+
* drawn count is `result.rows.length`, the total is `result.total`, the
|
|
31
|
+
* verdict is `result.truncated` — so a footnote whose numbers disagree with
|
|
32
|
+
* the rows actually drawn is not something a caller can write. The earlier
|
|
33
|
+
* shape took `drawn` / `total` / `truncated` as three loose props, and every
|
|
34
|
+
* call site hand-wrote `drawn={NON_GRID_ROW_CEILING}` next to a `total` from
|
|
35
|
+
* somewhere else: an incoherent pair was expressible and nothing refused it.
|
|
36
|
+
* ⛔ No deprecated path keeps those props; there were no released consumers.
|
|
37
|
+
*
|
|
38
|
+
* Renders `null` when nothing was truncated, so a caller holding a result can
|
|
39
|
+
* mount it unconditionally and gains no wrapper element on the healthy path.
|
|
40
|
+
*/
|
|
41
|
+
export declare function NonGridRowCeilingNote({ result, }: {
|
|
42
|
+
/** What `applyNonGridRowCeiling` returned for the rows being drawn. */
|
|
43
|
+
result: NonGridCeilingResult;
|
|
44
|
+
}): React.JSX.Element | null;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useObjectTranslation, en } from '@object-ui/i18n';
|
|
3
|
+
// The non-React half of the non-grid row ceiling — `NON_GRID_ROW_CEILING`,
|
|
4
|
+
// `nonGridRowCeilingQuery`, `applyNonGridRowCeiling` and `NonGridCeilingResult`
|
|
5
|
+
// — lives in `@object-ui/core` beside the `extractRecords` it wraps
|
|
6
|
+
// (objectui#7508, ruling A′). Only the footnote is React, so only the footnote
|
|
7
|
+
// is here.
|
|
8
|
+
/**
|
|
9
|
+
* The loud footnote a non-grid view shows when it drew only the first
|
|
10
|
+
* `NON_GRID_ROW_CEILING` rows of a larger result set (objectui#7210).
|
|
11
|
+
*
|
|
12
|
+
* Placement and tone follow objectui#7148's chart footnote — a `role="note"`
|
|
13
|
+
* line in muted small type directly under the visualisation, naming BOTH
|
|
14
|
+
* numbers, because the count is the half a reader cannot recover from the
|
|
15
|
+
* picture. A truncated schedule renders as a healthy, confident schedule of a
|
|
16
|
+
* fraction of itself; "some rows are missing" leaves it indistinguishable from
|
|
17
|
+
* a complete one, and `2,000 of 40,000` is the bit that was missing.
|
|
18
|
+
*
|
|
19
|
+
* Two sentences because there are two conditions: a known total states the
|
|
20
|
+
* fact with both numbers; an adapter that reported no `total` still gets a
|
|
21
|
+
* DEFINITE sentence (the probe row proves more rows exist), it simply cannot
|
|
22
|
+
* name how many.
|
|
23
|
+
*
|
|
24
|
+
* ## It takes the RESULT, and nothing else (objectui#7508, ruling A′)
|
|
25
|
+
*
|
|
26
|
+
* The only prop is the `NonGridCeilingResult` that `applyNonGridRowCeiling`
|
|
27
|
+
* returned. Every number the note prints is read off that one object — the
|
|
28
|
+
* drawn count is `result.rows.length`, the total is `result.total`, the
|
|
29
|
+
* verdict is `result.truncated` — so a footnote whose numbers disagree with
|
|
30
|
+
* the rows actually drawn is not something a caller can write. The earlier
|
|
31
|
+
* shape took `drawn` / `total` / `truncated` as three loose props, and every
|
|
32
|
+
* call site hand-wrote `drawn={NON_GRID_ROW_CEILING}` next to a `total` from
|
|
33
|
+
* somewhere else: an incoherent pair was expressible and nothing refused it.
|
|
34
|
+
* ⛔ No deprecated path keeps those props; there were no released consumers.
|
|
35
|
+
*
|
|
36
|
+
* Renders `null` when nothing was truncated, so a caller holding a result can
|
|
37
|
+
* mount it unconditionally and gains no wrapper element on the healthy path.
|
|
38
|
+
*/
|
|
39
|
+
export function NonGridRowCeilingNote({ result, }) {
|
|
40
|
+
// ⚠️ A HOOK AT RENDER, never a module-scope `createSafeTranslation(...)`
|
|
41
|
+
// factory, and that is a correctness constraint rather than taste. This
|
|
42
|
+
// module is re-exported from `@object-ui/react`'s entry, so anything at its
|
|
43
|
+
// module scope runs on IMPORT for every consumer that touches the barrel —
|
|
44
|
+
// and a factory call there throws inside any test that partially mocks
|
|
45
|
+
// `@object-ui/i18n` with an object literal instead of `importOriginal`,
|
|
46
|
+
// before a single assertion runs. Two such files went red in CI on exactly
|
|
47
|
+
// that, and the population is repo-wide rather than knowable from here, so
|
|
48
|
+
// the call moved to where it cannot fire at import time. `en` is likewise
|
|
49
|
+
// dereferenced HERE, at render, not in a module-scope constant.
|
|
50
|
+
//
|
|
51
|
+
// `useObjectTranslation` is the one that interpolates on the provider-less
|
|
52
|
+
// path too (objectui#6219), so `{{shown}}` / `{{total}}` are filled whether
|
|
53
|
+
// or not the host mounted an `I18nProvider`. The `defaultValue` is read from
|
|
54
|
+
// the `en` pack rather than retyped: both sit in the same eagerly-loaded
|
|
55
|
+
// chunk, so a hand copy would ship identical bytes twice, and reading the
|
|
56
|
+
// pack makes an inline default that disagrees with `en` unrepresentable
|
|
57
|
+
// instead of merely policed.
|
|
58
|
+
const { t } = useObjectTranslation();
|
|
59
|
+
if (!result.truncated)
|
|
60
|
+
return null;
|
|
61
|
+
const shown = result.rows.length;
|
|
62
|
+
const text = typeof result.total === 'number'
|
|
63
|
+
? t('common.rowCeilingNote', {
|
|
64
|
+
shown,
|
|
65
|
+
total: result.total,
|
|
66
|
+
defaultValue: en.common.rowCeilingNote,
|
|
67
|
+
})
|
|
68
|
+
: t('common.rowCeilingNoteUnknownTotal', {
|
|
69
|
+
shown,
|
|
70
|
+
defaultValue: en.common.rowCeilingNoteUnknownTotal,
|
|
71
|
+
});
|
|
72
|
+
// `shrink-0` is the note's own, not a caller's: under a `flex-1` view pane in
|
|
73
|
+
// a fixed-height host a plain sibling can be clipped out of view — the
|
|
74
|
+
// construction objectui#7148's `ChartFootnote` measured, and the reason the
|
|
75
|
+
// gantt used to pass this exact class in. Outside a flex container it does
|
|
76
|
+
// nothing, so all four views take it and no caller needs a styling prop.
|
|
77
|
+
return (_jsx("p", { role: "note", "data-row-ceiling-note": "non-grid", className: "shrink-0 px-1 py-1 text-xs text-muted-foreground", children: text }));
|
|
78
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/** The key this rule is about. One spelling, shared by both readers. */
|
|
9
|
+
export declare const PARAMS_KEY = "params";
|
|
10
|
+
/**
|
|
11
|
+
* Is this value a `params` BAG this rule walks? A plain object only - see the
|
|
12
|
+
* module header for why an array `params` (the definition list) is not.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isParamsBag(value: unknown): value is Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Visit one string leaf. `path` spells where it sits the way an author would
|
|
17
|
+
* search for it: `params.target.id`, `params.ids[0]`.
|
|
18
|
+
*/
|
|
19
|
+
export type ParamsLeafVisitor = (leaf: string, path: string) => unknown;
|
|
20
|
+
/**
|
|
21
|
+
* Map every string leaf of a `params` bag through `visit`, at any depth, and
|
|
22
|
+
* return the result - the bag itself when nothing changed (copy on write).
|
|
23
|
+
*
|
|
24
|
+
* The evaluation memo passes the evaluator; the dev diagnostic passes a visitor
|
|
25
|
+
* that records and returns the leaf unchanged, which is what makes the two
|
|
26
|
+
* radii one radius by construction.
|
|
27
|
+
*/
|
|
28
|
+
export declare function mapParamsLeaves(bag: Record<string, unknown>, visit: ParamsLeafVisitor, basePath?: string): Record<string, unknown>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* An action's `params` values are templates, evaluated where `properties` are
|
|
10
|
+
* (objectui#7867, ruling A, maintainer 「其他同意」 2026-09-20).
|
|
11
|
+
*
|
|
12
|
+
* ## The rule
|
|
13
|
+
*
|
|
14
|
+
* Every STRING LEAF of a node's `params` bag - the node-level `params` and
|
|
15
|
+
* `properties.params` alike, at any depth - is template-evaluated by the
|
|
16
|
+
* `SchemaRenderer` evaluation memo, with the same evaluator and the same
|
|
17
|
+
* scope (`record`, `current_user`, `page`, the host's ambient roots) that
|
|
18
|
+
* already evaluates `properties`. Before it, `params` was the one argument bag
|
|
19
|
+
* an `action:button` hands the runner, and nothing walked it: the node-level
|
|
20
|
+
* bag was never evaluated at all, and `properties.params` was ONE value of a
|
|
21
|
+
* per-value, shallow loop, so a template one level inside it was never
|
|
22
|
+
* visited. A metadata-authored `navigate_edit` could not name the record it
|
|
23
|
+
* sits on.
|
|
24
|
+
*
|
|
25
|
+
* ## Why this module exists, and why it is not in `SchemaRenderer.tsx`
|
|
26
|
+
*
|
|
27
|
+
* Two readers must agree on exactly one radius: the memo that EVALUATES the
|
|
28
|
+
* leaves, and the dev diagnostic (`unevaluatedExpression.ts`) that REPORTS the
|
|
29
|
+
* leaves still carrying `${…}` afterwards. That diagnostic's header states its
|
|
30
|
+
* own rule - "Depth: exactly the evaluator's own radius, and not one level
|
|
31
|
+
* more" - so a second copy of this walk would be a second definition of that
|
|
32
|
+
* radius, free to drift. Both import this one. It lives in its own module
|
|
33
|
+
* rather than in the diagnostic's because the memo runs in production and the
|
|
34
|
+
* diagnostic module is meant to be dead code there.
|
|
35
|
+
*
|
|
36
|
+
* ## What counts as a bag, a container and a leaf
|
|
37
|
+
*
|
|
38
|
+
* - **The bag** is a PLAIN object: an object literal or JSON object, whose
|
|
39
|
+
* prototype is `Object.prototype` or `null`. An ARRAY `params` is not a bag:
|
|
40
|
+
* it is the `ActionParam[]` definition list `action:button` routes to
|
|
41
|
+
* `actionParams` for the params dialog, whose members carry predicates and
|
|
42
|
+
* formats (`visible`, `titleFormat`, option `visibleWhen`) that are evaluated
|
|
43
|
+
* later, against another scope. It is left untouched, as before.
|
|
44
|
+
* - **Containers** inside the bag - plain objects and arrays - are walked.
|
|
45
|
+
* - **String leaves** are handed to the visitor. Everything else is passed
|
|
46
|
+
* through by IDENTITY and never looked inside: numbers, booleans, `null`,
|
|
47
|
+
* `undefined`, functions, and non-plain objects (`Date`, `Map`, class
|
|
48
|
+
* instances). Those are host-built values, not authored templates.
|
|
49
|
+
* - **Keys are never visited**, only values. A key that looks like a template
|
|
50
|
+
* stays exactly as written.
|
|
51
|
+
* - **An expression envelope** (`{ dialect, source }`) is a plain object like
|
|
52
|
+
* any other: it is walked, and its `source` string is a leaf. It is never
|
|
53
|
+
* collapsed to its `source` the way a bare top-level config value is.
|
|
54
|
+
* - **Cycles** end the walk: a container already on the current path is passed
|
|
55
|
+
* through unwalked. JSON metadata cannot carry one; a host-built schema can.
|
|
56
|
+
*
|
|
57
|
+
* ## Copy on write
|
|
58
|
+
*
|
|
59
|
+
* A container is copied only when one of its descendants changed, so a bag
|
|
60
|
+
* with no template in it comes back as the very object that went in, and a
|
|
61
|
+
* changed bag never mutates the authored one (the memo's input is the
|
|
62
|
+
* caller's schema). Nothing may depend on that identity (AGENTS.md #10); it is
|
|
63
|
+
* there so that a template-free bag stays byte-for-byte and reference-for-
|
|
64
|
+
* reference what it was.
|
|
65
|
+
*/
|
|
66
|
+
import { isConfigBag } from './configBag.js';
|
|
67
|
+
/** The key this rule is about. One spelling, shared by both readers. */
|
|
68
|
+
export const PARAMS_KEY = 'params';
|
|
69
|
+
/**
|
|
70
|
+
* A plain object: an object literal / JSON object, and nothing more exotic.
|
|
71
|
+
*
|
|
72
|
+
* Built ON the package's one "is this a config bag?" answer (objectui#6761)
|
|
73
|
+
* and narrowed by prototype, rather than re-spelling the object/array test:
|
|
74
|
+
* this rule asks that question plus one more (a `Date` or class instance is a
|
|
75
|
+
* bag to {@link isConfigBag}, and is not something this walk looks inside).
|
|
76
|
+
*/
|
|
77
|
+
function isPlainObject(value) {
|
|
78
|
+
if (!isConfigBag(value))
|
|
79
|
+
return false;
|
|
80
|
+
const proto = Object.getPrototypeOf(value);
|
|
81
|
+
return proto === Object.prototype || proto === null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Is this value a `params` BAG this rule walks? A plain object only - see the
|
|
85
|
+
* module header for why an array `params` (the definition list) is not.
|
|
86
|
+
*/
|
|
87
|
+
export function isParamsBag(value) {
|
|
88
|
+
return isPlainObject(value);
|
|
89
|
+
}
|
|
90
|
+
function walk(value, path, visit, onPath) {
|
|
91
|
+
if (typeof value === 'string')
|
|
92
|
+
return visit(value, path);
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
if (onPath.has(value))
|
|
95
|
+
return value;
|
|
96
|
+
onPath.add(value);
|
|
97
|
+
let copy;
|
|
98
|
+
for (let i = 0; i < value.length; i++) {
|
|
99
|
+
const next = walk(value[i], path + '[' + i + ']', visit, onPath);
|
|
100
|
+
if (next !== value[i]) {
|
|
101
|
+
if (!copy)
|
|
102
|
+
copy = value.slice();
|
|
103
|
+
copy[i] = next;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
onPath.delete(value);
|
|
107
|
+
return copy ?? value;
|
|
108
|
+
}
|
|
109
|
+
if (isPlainObject(value)) {
|
|
110
|
+
if (onPath.has(value))
|
|
111
|
+
return value;
|
|
112
|
+
onPath.add(value);
|
|
113
|
+
let copy;
|
|
114
|
+
for (const key of Object.keys(value)) {
|
|
115
|
+
const leaf = value[key];
|
|
116
|
+
const next = walk(leaf, path + '.' + key, visit, onPath);
|
|
117
|
+
if (next !== leaf) {
|
|
118
|
+
if (!copy)
|
|
119
|
+
copy = { ...value };
|
|
120
|
+
// `defineProperty`, not assignment: an own `__proto__` key (which
|
|
121
|
+
// `JSON.parse` does produce) must stay an own data property rather than
|
|
122
|
+
// reach the prototype setter.
|
|
123
|
+
Object.defineProperty(copy, key, {
|
|
124
|
+
value: next,
|
|
125
|
+
writable: true,
|
|
126
|
+
enumerable: true,
|
|
127
|
+
configurable: true,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
onPath.delete(value);
|
|
132
|
+
return copy ?? value;
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Map every string leaf of a `params` bag through `visit`, at any depth, and
|
|
138
|
+
* return the result - the bag itself when nothing changed (copy on write).
|
|
139
|
+
*
|
|
140
|
+
* The evaluation memo passes the evaluator; the dev diagnostic passes a visitor
|
|
141
|
+
* that records and returns the leaf unchanged, which is what makes the two
|
|
142
|
+
* radii one radius by construction.
|
|
143
|
+
*/
|
|
144
|
+
export function mapParamsLeaves(bag, visit, basePath = PARAMS_KEY) {
|
|
145
|
+
return walk(bag, basePath, visit, new Set());
|
|
146
|
+
}
|