@object-ui/react 17.7.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 +497 -0
- package/README.md +89 -4
- package/dist/SchemaRenderer.d.ts +27 -1
- package/dist/SchemaRenderer.js +139 -50
- package/dist/context/AppShellContext.d.ts +27 -0
- package/dist/context/DiscussionContext.d.ts +15 -0
- package/dist/context/DiscussionContext.js +2 -0
- package/dist/context/InlineEditContext.d.ts +70 -0
- package/dist/context/InlineEditContext.js +30 -2
- package/dist/context/PrintPageContext.d.ts +60 -0
- package/dist/context/PrintPageContext.js +32 -0
- package/dist/context/RelatedRecordActionsContext.d.ts +67 -1
- package/dist/context/RelatedRecordActionsContext.js +4 -0
- package/dist/context/SchemaRendererContext.d.ts +7 -4
- package/dist/context/SchemaRendererContext.js +1 -1
- 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 +30 -2
- package/dist/element-data-source/ElementDataSourceGate.js +75 -6
- package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
- package/dist/element-data-source/useElementApiDataSource.js +198 -0
- package/dist/hooks/useActionTextLocalizer.d.ts +20 -7
- package/dist/hooks/useActionTextLocalizer.js +43 -12
- package/dist/hooks/useConfigBagEvaluator.d.ts +5 -4
- package/dist/hooks/useConfigBagEvaluator.js +5 -4
- package/dist/hooks/useDatasetDimensionLabels.js +25 -10
- package/dist/hooks/useDensityMode.d.ts +4 -2
- package/dist/hooks/useDensityMode.js +4 -2
- package/dist/hooks/usePageAssignment.d.ts +31 -8
- package/dist/hooks/usePageAssignment.js +68 -25
- package/dist/hooks/useViewData.d.ts +3 -1
- package/dist/hooks/useViewData.js +5 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/utils/localeMap.d.ts +27 -0
- package/dist/utils/localeMap.js +106 -0
- package/dist/utils/propsBagDiagnostic.js +3 -2
- package/dist/utils/unevaluatedExpression.d.ts +3 -1
- package/dist/utils/unevaluatedExpression.js +18 -0
- package/dist/utils/visibilityDiagnostic.d.ts +14 -5
- package/dist/utils/visibilityDiagnostic.js +10 -2
- package/package.json +6 -6
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*
|
|
8
8
|
* `usePageAssignment` — resolve the record PageSchema that should be rendered
|
|
9
|
-
* for a given object. Walks the metadata cache exposed by `<MetadataProvider
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* for a given object. Walks the metadata cache exposed by `<MetadataProvider>`,
|
|
10
|
+
* keeps the PageSchemas whose `type === 'record'` and `object` matches the
|
|
11
|
+
* requested name, and returns the object's DEFAULT record page among them
|
|
12
|
+
* (`isDefault: true`), else the first of them.
|
|
12
13
|
*
|
|
13
14
|
* Returns a discriminated result by `PageSchema.kind`:
|
|
14
15
|
* - `kind === 'full'` (default): the schema fully describes the page;
|
|
@@ -18,16 +19,44 @@
|
|
|
18
19
|
* `buildDefaultPageSchema(objectDef, { slots })` so omitted slots
|
|
19
20
|
* fall through to synthesized defaults.
|
|
20
21
|
*
|
|
21
|
-
* Selection is by
|
|
22
|
-
*
|
|
23
|
-
* record
|
|
22
|
+
* Selection is by the declared `PageSchema.isDefault`, then by DECLARATION
|
|
23
|
+
* ORDER among pages that are not the default: the spec allows one default
|
|
24
|
+
* record page per object, so the flag needs no tie-break here, and with no
|
|
25
|
+
* default the first matching page wins. No match returns null, so callers can
|
|
26
|
+
* deterministically fall back to the auto-generated DetailView when no record
|
|
27
|
+
* Page is authored.
|
|
24
28
|
*
|
|
25
29
|
* Future work (deferred): recordType / profile / app / formFactor filtering.
|
|
26
|
-
* Any author-driven ordering would have to be DECLARED on
|
|
27
|
-
* `@objectstack/spec` first — see the note on the selection
|
|
30
|
+
* Any further author-driven ordering would have to be DECLARED on
|
|
31
|
+
* `PageSchema` in `@objectstack/spec` first — see the note on the selection
|
|
32
|
+
* itself below.
|
|
28
33
|
*/
|
|
29
34
|
import { useEffect, useMemo, useState } from 'react';
|
|
35
|
+
import { defaultRecordPageObject } from '@objectstack/spec/ui';
|
|
30
36
|
import { useMetadata } from '../context/AppShellContext.js';
|
|
37
|
+
/**
|
|
38
|
+
* True when `page` is a record page bound to `objectName`: its `type` is
|
|
39
|
+
* `'record'` and its `object` names that object.
|
|
40
|
+
*
|
|
41
|
+
* This is the ONE reading of "a record page of object O" in the console. The
|
|
42
|
+
* object-based lookup below filters its candidates with it, and a route that
|
|
43
|
+
* names a page explicitly (the console's named record page route,
|
|
44
|
+
* objectui#12715) asks it before rendering the named page for a record of O,
|
|
45
|
+
* so a name that is a page of another object, or not a record page at all, is
|
|
46
|
+
* refused rather than rendered. The `pageName` override of
|
|
47
|
+
* {@link usePageAssignment} does not ask it: that option returns the page with
|
|
48
|
+
* that name whatever it is, and a caller that renders a record on the page it
|
|
49
|
+
* gets back asks this first.
|
|
50
|
+
*
|
|
51
|
+
* `type` is the one discriminator, as the lookup's own comment explains
|
|
52
|
+
* (objectui#9674): `pageType` is a key `PageSchema` refuses, so it is not read.
|
|
53
|
+
*/
|
|
54
|
+
export function isRecordPageOf(page, objectName) {
|
|
55
|
+
if (!page || typeof page !== 'object' || !objectName)
|
|
56
|
+
return false;
|
|
57
|
+
const p = page;
|
|
58
|
+
return p.type === 'record' && p.object === objectName;
|
|
59
|
+
}
|
|
31
60
|
function matchesAssignment(_page, _opts) {
|
|
32
61
|
// Placeholder — every page matches until rules-based assignment lands.
|
|
33
62
|
return true;
|
|
@@ -91,30 +120,44 @@ export function usePageAssignment(objectName, opts = {}) {
|
|
|
91
120
|
// designer-side `'record_detail'` is not expressible either: it left
|
|
92
121
|
// `PageTypeSchema` (framework#2265), so `type: 'record_detail'` is an
|
|
93
122
|
// invalid enum value.
|
|
94
|
-
if (p
|
|
95
|
-
return false;
|
|
96
|
-
if (p.object !== objectName)
|
|
123
|
+
if (!isRecordPageOf(p, objectName))
|
|
97
124
|
return false;
|
|
98
125
|
return matchesAssignment(p, opts);
|
|
99
126
|
});
|
|
100
127
|
if (!candidates.length)
|
|
101
128
|
return null;
|
|
102
|
-
//
|
|
129
|
+
// The object's DEFAULT record page decides; with none, declaration order.
|
|
130
|
+
//
|
|
131
|
+
// At most one `isDefault: true` record page may exist for an object in a
|
|
132
|
+
// deployment. That is the spec's rule on `PageSchema.isDefault` (maintainer
|
|
133
|
+
// ruling A on objectstack#23015, record 6134056653), and the platform
|
|
134
|
+
// refuses a second default, naming both pages, at every door that can add
|
|
135
|
+
// one: `defineStack` / `composeStacks` (so `os validate` and a source boot),
|
|
136
|
+
// package registration, and metadata save. So this hook honours the flag
|
|
137
|
+
// with NO tie-break of its own. It asks `defaultRecordPageObject`, the one
|
|
138
|
+
// reading of "the default record page of an object" each of those doors
|
|
139
|
+
// asks, so the page the platform judges to be the default is the page that
|
|
140
|
+
// opens. Replacing a platform record page stays an ADR-0005 overlay by name
|
|
141
|
+
// (the same page, not a second default) or host composition; neither a
|
|
142
|
+
// second default nor an earlier position in the served list overrides one.
|
|
103
143
|
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
144
|
+
// Boundary, stated rather than left implicit: at a cold boot every package
|
|
145
|
+
// registers before the environment's stored pages hydrate, so a stored
|
|
146
|
+
// default saved while no package held that object's default, and a package
|
|
147
|
+
// that adds one later, both reach the registry unrefused
|
|
148
|
+
// (objectstack#23213, closed not_planned). This hook invents no precedence
|
|
149
|
+
// for that pair (no layer, package or recency rung); served order stays the
|
|
150
|
+
// only order it has, so the first default served wins.
|
|
110
151
|
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// `
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
152
|
+
// Among pages that are not the default, declaration order is the last
|
|
153
|
+
// resort: the first match wins. This used to sort on a `priority` key read
|
|
154
|
+
// off the candidate. No author could ever set it: `PageSchema` is a
|
|
155
|
+
// `strictObject` and does not declare `priority`, so a page carrying one is
|
|
156
|
+
// a HARD PARSE ERROR, not a page that sorts first. Every candidate therefore
|
|
157
|
+
// read `0`, the comparator returned `0` for every pair, and a stable sort
|
|
158
|
+
// left declaration order untouched — the sort documented an affordance the
|
|
159
|
+
// schema refuses (objectui#7298).
|
|
160
|
+
return candidates.find(p => defaultRecordPageObject(p) === objectName) ?? candidates[0];
|
|
118
161
|
}, [meta.pages, objectName, opts.pageName, opts.recordType, opts.profile, opts.app, opts.formFactor]);
|
|
119
162
|
// Discriminate by `kind`: full pages populate `page`, slotted pages
|
|
120
163
|
// populate `slots`. Missing `kind` defaults to 'full' for backwards
|
|
@@ -55,8 +55,10 @@ export interface UseViewDataResult<T = any> {
|
|
|
55
55
|
*
|
|
56
56
|
* 1. Resolves the ViewData config to the right DataSource adapter
|
|
57
57
|
* - `provider: 'object'` → context DataSource (ObjectStackAdapter)
|
|
58
|
-
* - `provider: 'api'` → ApiDataSource (raw HTTP)
|
|
59
58
|
* - `provider: 'value'` → ValueDataSource (in-memory)
|
|
59
|
+
* The view-level `provider: 'api'` is retired in `@objectstack/spec`
|
|
60
|
+
* (objectstack#23082) and has no arm in `resolveDataSource`: a block that
|
|
61
|
+
* still names it is an unknown provider there (objectui#12325).
|
|
60
62
|
* 2. Fetches data automatically on mount
|
|
61
63
|
* 3. Provides loading/error states and refresh capability
|
|
62
64
|
*
|
|
@@ -22,8 +22,10 @@ import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
|
22
22
|
*
|
|
23
23
|
* 1. Resolves the ViewData config to the right DataSource adapter
|
|
24
24
|
* - `provider: 'object'` → context DataSource (ObjectStackAdapter)
|
|
25
|
-
* - `provider: 'api'` → ApiDataSource (raw HTTP)
|
|
26
25
|
* - `provider: 'value'` → ValueDataSource (in-memory)
|
|
26
|
+
* The view-level `provider: 'api'` is retired in `@objectstack/spec`
|
|
27
|
+
* (objectstack#23082) and has no arm in `resolveDataSource`: a block that
|
|
28
|
+
* still names it is an unknown provider there (objectui#12325).
|
|
27
29
|
* 2. Fetches data automatically on mount
|
|
28
30
|
* 3. Provides loading/error states and refresh capability
|
|
29
31
|
*
|
|
@@ -44,30 +46,19 @@ import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
|
|
|
44
46
|
*/
|
|
45
47
|
export function useViewData(options) {
|
|
46
48
|
const { viewData, resource = '', params, dataSource: explicitDataSource, adapterOptions, skip = false, } = options;
|
|
47
|
-
// Get context DataSource
|
|
49
|
+
// Get context DataSource
|
|
48
50
|
const context = useContext(SchemaRendererContext);
|
|
49
51
|
const contextDataSource = context?.dataSource ?? null;
|
|
50
|
-
const contextApiFetch = context?.apiFetch;
|
|
51
52
|
// Resolve the DataSource — memoize to prevent re-creation on every render.
|
|
52
53
|
// We key on the viewData provider + config identity.
|
|
53
54
|
const resolvedDataSource = useMemo(() => {
|
|
54
55
|
if (explicitDataSource)
|
|
55
56
|
return explicitDataSource;
|
|
56
|
-
|
|
57
|
-
// (SchemaRendererContext.apiFetch) so custom endpoints carry the same
|
|
58
|
-
// credentials as native requests; explicit adapterOptions still win.
|
|
59
|
-
return resolveDataSource(viewData, contextDataSource, {
|
|
60
|
-
fetch: contextApiFetch,
|
|
61
|
-
...adapterOptions,
|
|
62
|
-
});
|
|
57
|
+
return resolveDataSource(viewData, contextDataSource, adapterOptions);
|
|
63
58
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
64
59
|
}, [
|
|
65
60
|
explicitDataSource,
|
|
66
|
-
contextApiFetch,
|
|
67
61
|
viewData?.provider,
|
|
68
|
-
// For 'api' provider, key on read/write URLs
|
|
69
|
-
viewData?.provider === 'api' ? viewData.read?.url : undefined,
|
|
70
|
-
viewData?.provider === 'api' ? viewData.write?.url : undefined,
|
|
71
62
|
// For 'value' provider, key on items reference (or length as proxy)
|
|
72
63
|
viewData?.provider === 'value' ? viewData.items?.length : undefined,
|
|
73
64
|
contextDataSource,
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './context/index.js';
|
|
|
12
12
|
export * from './LazyPluginLoader.js';
|
|
13
13
|
export * from './data-invalidation.js';
|
|
14
14
|
export * from './element-data-source/ElementDataSourceGate.js';
|
|
15
|
+
export * from './element-data-source/useElementApiDataSource.js';
|
|
15
16
|
export { resolveKeyedI18nLabel } from './utils/i18n.js';
|
|
16
17
|
export { resolveInlineAriaProps } from './utils/inlineAria.js';
|
|
17
18
|
export { isConfigBag } from './utils/configBag.js';
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,9 @@ export * from './data-invalidation.js';
|
|
|
14
14
|
// PageComponentSchema.dataSource — mapping the spec's per-element data binding
|
|
15
15
|
// onto the schema keys each object-bound block reads (objectstack#6953).
|
|
16
16
|
export * from './element-data-source/ElementDataSourceGate.js';
|
|
17
|
+
// PageComponentSchema.dataSource as the spec's API binding — the read behind
|
|
18
|
+
// the `data` root `SchemaRenderer` binds for it (objectui#12197).
|
|
19
|
+
export * from './element-data-source/useElementApiDataSource.js';
|
|
17
20
|
// i18n utilities
|
|
18
21
|
export { resolveKeyedI18nLabel } from './utils/i18n.js';
|
|
19
22
|
// The spec's NESTED `aria` bag (`AriaPropsSchema`) → `aria-label` /
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
* Is this value an inline locale map, in `InlineLocaleMapSchema`'s sense?
|
|
10
|
+
*
|
|
11
|
+
* The member loop first is a fast reject and not a second definition: the
|
|
12
|
+
* schema requires every value to be a string, so the loop can only turn away
|
|
13
|
+
* what the schema turns away, and it does so without the schema's per-key
|
|
14
|
+
* issue reporting. Most data objects carry a non-string member.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isInlineLocaleMap(value: unknown): value is Record<string, string>;
|
|
17
|
+
/** Visit one member. `locale` is the member's key (`en`, `zh-CN`, `default`). */
|
|
18
|
+
export type LocaleMapMemberVisitor = (member: string, locale: string) => unknown;
|
|
19
|
+
/**
|
|
20
|
+
* Map every member of an inline locale map through `visit` and return the
|
|
21
|
+
* result, spelled as a string: the map itself when nothing changed (copy on
|
|
22
|
+
* write).
|
|
23
|
+
*
|
|
24
|
+
* The evaluation memo passes the evaluator; the dev diagnostic passes a visitor
|
|
25
|
+
* that records the member and returns it unchanged.
|
|
26
|
+
*/
|
|
27
|
+
export declare function mapLocaleMapMembers(map: Record<string, string>, visit: LocaleMapMemberVisitor): Record<string, string>;
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
* Each member of an inline locale map is evaluated like a plain string on the
|
|
10
|
+
* same key (objectui#12936).
|
|
11
|
+
*
|
|
12
|
+
* ## The rule
|
|
13
|
+
*
|
|
14
|
+
* A node's config-bag value that is an INLINE LOCALE MAP - the second
|
|
15
|
+
* authorized form of the spec's `I18nLabel`, for example
|
|
16
|
+
* `title: { en: 'Published ${data.at}', 'zh-CN': '...${data.at}' }` - has
|
|
17
|
+
* every member evaluated by the `SchemaRenderer` evaluation memo, with the same
|
|
18
|
+
* evaluator and so against the same scope as a plain string on that key:
|
|
19
|
+
* `record`, `current_user` / `user`, `page`, the host's ambient roots, and
|
|
20
|
+
* `data` on a node with an API binding. The block then picks one member for
|
|
21
|
+
* the viewer's language (`pickLocalized`) and draws an evaluated string.
|
|
22
|
+
*
|
|
23
|
+
* Before it, the map was a data object to the per-value loops, which hand a
|
|
24
|
+
* data object over as authored, so the member the block picked kept its
|
|
25
|
+
* `${...}` and drew it as text. A plain string with the same hole on the same
|
|
26
|
+
* key was evaluated, so localizing a line was what broke it.
|
|
27
|
+
*
|
|
28
|
+
* ## What a locale map is: the spec's answer, not a third one
|
|
29
|
+
*
|
|
30
|
+
* {@link isInlineLocaleMap} asks `@objectstack/spec`'s `InlineLocaleMapSchema`,
|
|
31
|
+
* the schema the contract declares the form with: every key a BCP-47 tag or
|
|
32
|
+
* `default` (never `key` / `defaultValue`), every value a string. A value with
|
|
33
|
+
* a non-string member, or with a key that is not a locale tag, is NOT a locale
|
|
34
|
+
* map: it stays a data object and is handed over as authored, with nothing
|
|
35
|
+
* inside it evaluated. No other object is walked.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ The boundary that follows from asking the contract: an object whose keys
|
|
38
|
+
* all parse as locale tags (any two or three letters do, so `{ id, url }`
|
|
39
|
+
* qualifies) and whose values are all strings IS an inline locale map to the
|
|
40
|
+
* spec, and so it is one here too, and a `${...}` in its members is evaluated.
|
|
41
|
+
*
|
|
42
|
+
* ## Why this module exists
|
|
43
|
+
*
|
|
44
|
+
* Two readers must agree on one radius, as they do for `params`
|
|
45
|
+
* (`paramsBag.ts`): the memo that EVALUATES the members, and the dev
|
|
46
|
+
* diagnostic (`unevaluatedExpression.ts`) that REPORTS a member still carrying
|
|
47
|
+
* `${...}` afterwards because its expression threw. Both import this module, so
|
|
48
|
+
* a member that fails to evaluate is reported exactly as a plain string that
|
|
49
|
+
* fails is.
|
|
50
|
+
*
|
|
51
|
+
* ## The result is still a locale map
|
|
52
|
+
*
|
|
53
|
+
* A member's result is spelled as a string. A member that is one whole hole,
|
|
54
|
+
* `'${data.count}'`, evaluates to the number itself, as a plain string does;
|
|
55
|
+
* left a number, the member would be skipped by every `pickLocalized` limb
|
|
56
|
+
* (each reads string entries only) and the viewer would get another locale's
|
|
57
|
+
* line, or nothing. `String(result ?? '')` is the spelling the evaluator
|
|
58
|
+
* already gives an interpolated part of a plain string.
|
|
59
|
+
*
|
|
60
|
+
* ## Copy on write
|
|
61
|
+
*
|
|
62
|
+
* The map is copied only when a member changed, so a map without a hole comes
|
|
63
|
+
* back as the very object that went in, and the authored map is never mutated.
|
|
64
|
+
* Nothing may depend on that identity (AGENTS.md #10).
|
|
65
|
+
*/
|
|
66
|
+
import { InlineLocaleMapSchema } from '@objectstack/spec/ui';
|
|
67
|
+
import { isConfigBag } from './configBag.js';
|
|
68
|
+
/**
|
|
69
|
+
* Is this value an inline locale map, in `InlineLocaleMapSchema`'s sense?
|
|
70
|
+
*
|
|
71
|
+
* The member loop first is a fast reject and not a second definition: the
|
|
72
|
+
* schema requires every value to be a string, so the loop can only turn away
|
|
73
|
+
* what the schema turns away, and it does so without the schema's per-key
|
|
74
|
+
* issue reporting. Most data objects carry a non-string member.
|
|
75
|
+
*/
|
|
76
|
+
export function isInlineLocaleMap(value) {
|
|
77
|
+
if (!isConfigBag(value))
|
|
78
|
+
return false;
|
|
79
|
+
for (const member of Object.values(value)) {
|
|
80
|
+
if (typeof member !== 'string')
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return InlineLocaleMapSchema.safeParse(value).success;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Map every member of an inline locale map through `visit` and return the
|
|
87
|
+
* result, spelled as a string: the map itself when nothing changed (copy on
|
|
88
|
+
* write).
|
|
89
|
+
*
|
|
90
|
+
* The evaluation memo passes the evaluator; the dev diagnostic passes a visitor
|
|
91
|
+
* that records the member and returns it unchanged.
|
|
92
|
+
*/
|
|
93
|
+
export function mapLocaleMapMembers(map, visit) {
|
|
94
|
+
let copy;
|
|
95
|
+
for (const locale of Object.keys(map)) {
|
|
96
|
+
const member = map[locale];
|
|
97
|
+
const result = visit(member, locale);
|
|
98
|
+
const next = typeof result === 'string' ? result : String(result ?? '');
|
|
99
|
+
if (next !== member) {
|
|
100
|
+
if (!copy)
|
|
101
|
+
copy = { ...map };
|
|
102
|
+
copy[locale] = next;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return copy ?? map;
|
|
106
|
+
}
|
|
@@ -34,8 +34,9 @@ import { isConfigBag } from './configBag.js';
|
|
|
34
34
|
* (objectui#6665): the `props` leg renders `No results found`, the `properties`
|
|
35
35
|
* leg renders the rows.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* The tolerant gates accept the `props` spelling — the zod `BaseSchema` is
|
|
38
|
+
* `.passthrough()`, and the TS face admitted it through `[key: string]: any`
|
|
39
|
+
* until objectui#8347 (a typed literal refuses it now) — and `props` is documented as the annotated legacy
|
|
39
40
|
* alias of the config bag, so nothing between the author and the screen says a
|
|
40
41
|
* word. That is the success-receipt shape objectui#6575 and objectui#6665 exist
|
|
41
42
|
* to remove; this is the third instance, and the first at a tier that covers
|
|
@@ -18,7 +18,9 @@ export type UnevaluatedChannel = 'schema' | 'properties' | 'props';
|
|
|
18
18
|
export interface UnevaluatedExpressionFinding {
|
|
19
19
|
/**
|
|
20
20
|
* The key holding the raw source — or, for a leaf of a `params` bag, that
|
|
21
|
-
* leaf's path from the bag (`params.target.id`, `params.ids[0]`)
|
|
21
|
+
* leaf's path from the bag (`params.target.id`, `params.ids[0]`), or, for a
|
|
22
|
+
* member of an inline locale map, the key and the member's locale
|
|
23
|
+
* (`title.zh-CN`).
|
|
22
24
|
*/
|
|
23
25
|
key: string;
|
|
24
26
|
/** The authored channel the key was read from. */
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import { isConfigBag } from './configBag.js';
|
|
9
|
+
import { isInlineLocaleMap, mapLocaleMapMembers } from './localeMap.js';
|
|
9
10
|
import { PARAMS_KEY, isParamsBag, mapParamsLeaves } from './paramsBag.js';
|
|
10
11
|
/**
|
|
11
12
|
* Dev-build diagnostic: an UNEVALUATED template expression reached the DOM.
|
|
@@ -65,6 +66,13 @@ import { PARAMS_KEY, isParamsBag, mapParamsLeaves } from './paramsBag.js';
|
|
|
65
66
|
* that still cannot resolve is reported by its path (`params.target.id`,
|
|
66
67
|
* `properties.params.ids[0]`). Radius equal to evaluation by construction,
|
|
67
68
|
* not by two copies agreeing; every other key stays shallow on both sides.
|
|
69
|
+
*
|
|
70
|
+
* And for exactly ONE value shape, one level deep: an inline locale map
|
|
71
|
+
* (objectui#12936). Each member is evaluated like a plain string on the same
|
|
72
|
+
* key, so each member is scanned like one, through the same recogniser and
|
|
73
|
+
* walker (`localeMap.ts`), and a member whose expression threw is reported by
|
|
74
|
+
* its locale (`properties.title.zh-CN`). Any other object stays unscanned, as
|
|
75
|
+
* it stays unevaluated.
|
|
68
76
|
*/
|
|
69
77
|
/** What counts as an expression — the evaluator's own definition. */
|
|
70
78
|
const EXPRESSION_PATTERN = /\$\{([^}]+)\}/g;
|
|
@@ -111,6 +119,16 @@ function scanBag(bag, channel, into, skip) {
|
|
|
111
119
|
});
|
|
112
120
|
continue;
|
|
113
121
|
}
|
|
122
|
+
// objectui#12936 — an inline locale map's members are evaluated like plain
|
|
123
|
+
// strings, so they are scanned like them, through the evaluator's own
|
|
124
|
+
// recogniser and walker. The visitor returns each member unchanged.
|
|
125
|
+
if (isInlineLocaleMap(value)) {
|
|
126
|
+
mapLocaleMapMembers(value, (member, locale) => {
|
|
127
|
+
pushIfUnevaluated(key + '.' + locale, channel, member, into);
|
|
128
|
+
return member;
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
114
132
|
if (typeof value !== 'string')
|
|
115
133
|
continue;
|
|
116
134
|
pushIfUnevaluated(key, channel, value, into);
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* ## The defect this names
|
|
15
15
|
*
|
|
16
16
|
* `SchemaRenderer`'s visibility chain is FAIL-SOFT: `evaluateCondition` answers
|
|
17
|
-
* an unevaluable predicate with `true`. On the
|
|
18
|
-
* `visibleWhen` / `visibleOn`
|
|
17
|
+
* an unevaluable predicate with `true`. On the negated legs (`visible` /
|
|
18
|
+
* `visibleWhen` / `visibleOn`) that `true` means SHOWN — so "this
|
|
19
19
|
* predicate is broken" and "this predicate said yes" reach the screen as the
|
|
20
20
|
* same pixel. An author who mistyped a root, or who wrote a `record.*` gate
|
|
21
21
|
* before objectui#5454 bound the row, saw their block render and had no signal
|
|
@@ -136,7 +136,7 @@ export type PredicateScopeTier = 'page-component' | 'app-shell';
|
|
|
136
136
|
* to the node is not a property of the predicate - it is a property of the gate
|
|
137
137
|
* it was authored on, and the consequence paragraph has to say which:
|
|
138
138
|
*
|
|
139
|
-
* `'visibility'` - the visibility chain. Its `true` means SHOWN on the
|
|
139
|
+
* `'visibility'` - the visibility chain. Its `true` means SHOWN on the
|
|
140
140
|
* negated legs, so the safe default does NOT bite: a predicate that could not be
|
|
141
141
|
* evaluated reads on screen exactly like one that said yes, which is why the
|
|
142
142
|
* console line is the only signal an author gets.
|
|
@@ -180,10 +180,19 @@ export type PredicateScopeTier = 'page-component' | 'app-shell';
|
|
|
180
180
|
* package entry, and the chain stops there (no sibling package re-exports it,
|
|
181
181
|
* and no file outside `packages/react` names it). A consumer switching
|
|
182
182
|
* EXHAUSTIVELY over the union, or holding a `Record<PredicateGateKind, ...>`,
|
|
183
|
-
* gains a
|
|
183
|
+
* gains a case to answer. That is a type-level change only: no runtime
|
|
184
184
|
* signature moved, and every value that was accepted before still is.
|
|
185
|
+
*
|
|
186
|
+
* `'withholding'` - the app-shell's navigation `visible` gate (objectui#12700).
|
|
187
|
+
* A visibility predicate on a navigation entry, but one whose FAULT is answered
|
|
188
|
+
* fail-CLOSED: the shell withholds an entry whose gate it cannot evaluate (most
|
|
189
|
+
* often a `current_user.can(...)` asked before the permissions payload is
|
|
190
|
+
* there) instead of drawing it for everyone. So the default BITES, and the
|
|
191
|
+
* sentence `'visibility'` prints - "reads on screen exactly like one that said
|
|
192
|
+
* yes" - would be the opposite of what the member sees. Same opening line as
|
|
193
|
+
* `'visibility'`, for the same reason `'concealment'` keeps it.
|
|
185
194
|
*/
|
|
186
|
-
export type PredicateGateKind = 'visibility' | 'enablement' | 'concealment';
|
|
195
|
+
export type PredicateGateKind = 'visibility' | 'enablement' | 'concealment' | 'withholding';
|
|
187
196
|
/**
|
|
188
197
|
* Build the message. Separate from the emit so a test can assert the words,
|
|
189
198
|
* not merely that something was logged.
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* ## The defect this names
|
|
15
15
|
*
|
|
16
16
|
* `SchemaRenderer`'s visibility chain is FAIL-SOFT: `evaluateCondition` answers
|
|
17
|
-
* an unevaluable predicate with `true`. On the
|
|
18
|
-
* `visibleWhen` / `visibleOn`
|
|
17
|
+
* an unevaluable predicate with `true`. On the negated legs (`visible` /
|
|
18
|
+
* `visibleWhen` / `visibleOn`) that `true` means SHOWN — so "this
|
|
19
19
|
* predicate is broken" and "this predicate said yes" reach the screen as the
|
|
20
20
|
* same pixel. An author who mistyped a root, or who wrote a `record.*` gate
|
|
21
21
|
* before objectui#5454 bound the row, saw their block render and had no signal
|
|
@@ -135,6 +135,14 @@ const GATE_KIND_COPY = {
|
|
|
135
135
|
'in the renderer: the block is missing because the predicate above could\n' +
|
|
136
136
|
'not be evaluated.\n',
|
|
137
137
|
},
|
|
138
|
+
// The app-shell navigation gate (objectui#12700): fail-CLOSED on a fault.
|
|
139
|
+
withholding: {
|
|
140
|
+
prefix: UNRESOLVABLE_VISIBILITY_PREFIX,
|
|
141
|
+
consequence: 'The entry was treated as its safe default, which on THIS gate is the one\n' +
|
|
142
|
+
'that BITES: a navigation entry whose predicate cannot be evaluated is\n' +
|
|
143
|
+
'WITHHELD - not drawn - until it can be, so a missing entry may be this\n' +
|
|
144
|
+
'fault rather than a gate that said no.\n',
|
|
145
|
+
},
|
|
138
146
|
enablement: {
|
|
139
147
|
prefix: UNRESOLVABLE_ENABLEMENT_PREFIX,
|
|
140
148
|
consequence: 'The node was treated as its safe default, which on THIS gate is the one\n' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "18.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "React bindings and SchemaRenderer component for Object UI",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@objectstack/spec": "^
|
|
28
|
-
"@object-ui/data-objectstack": "
|
|
29
|
-
"@object-ui/types": "
|
|
30
|
-
"@object-ui/core": "
|
|
31
|
-
"@object-ui/i18n": "
|
|
27
|
+
"@objectstack/spec": "^18.0.0",
|
|
28
|
+
"@object-ui/data-objectstack": "18.0.0",
|
|
29
|
+
"@object-ui/types": "18.0.0",
|
|
30
|
+
"@object-ui/core": "18.0.0",
|
|
31
|
+
"@object-ui/i18n": "18.0.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "^18.0.0 || ^19.0.0",
|