@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +497 -0
  2. package/README.md +89 -4
  3. package/dist/SchemaRenderer.d.ts +27 -1
  4. package/dist/SchemaRenderer.js +139 -50
  5. package/dist/context/AppShellContext.d.ts +27 -0
  6. package/dist/context/DiscussionContext.d.ts +15 -0
  7. package/dist/context/DiscussionContext.js +2 -0
  8. package/dist/context/InlineEditContext.d.ts +70 -0
  9. package/dist/context/InlineEditContext.js +30 -2
  10. package/dist/context/PrintPageContext.d.ts +60 -0
  11. package/dist/context/PrintPageContext.js +32 -0
  12. package/dist/context/RelatedRecordActionsContext.d.ts +67 -1
  13. package/dist/context/RelatedRecordActionsContext.js +4 -0
  14. package/dist/context/SchemaRendererContext.d.ts +7 -4
  15. package/dist/context/SchemaRendererContext.js +1 -1
  16. package/dist/context/index.d.ts +1 -0
  17. package/dist/context/index.js +1 -0
  18. package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
  19. package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
  20. package/dist/element-data-source/ElementDataSourceGate.d.ts +30 -2
  21. package/dist/element-data-source/ElementDataSourceGate.js +75 -6
  22. package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
  23. package/dist/element-data-source/useElementApiDataSource.js +198 -0
  24. package/dist/hooks/useActionTextLocalizer.d.ts +20 -7
  25. package/dist/hooks/useActionTextLocalizer.js +43 -12
  26. package/dist/hooks/useConfigBagEvaluator.d.ts +5 -4
  27. package/dist/hooks/useConfigBagEvaluator.js +5 -4
  28. package/dist/hooks/useDatasetDimensionLabels.js +25 -10
  29. package/dist/hooks/useDensityMode.d.ts +4 -2
  30. package/dist/hooks/useDensityMode.js +4 -2
  31. package/dist/hooks/usePageAssignment.d.ts +31 -8
  32. package/dist/hooks/usePageAssignment.js +68 -25
  33. package/dist/hooks/useViewData.d.ts +3 -1
  34. package/dist/hooks/useViewData.js +5 -14
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +3 -0
  37. package/dist/utils/localeMap.d.ts +27 -0
  38. package/dist/utils/localeMap.js +106 -0
  39. package/dist/utils/propsBagDiagnostic.js +3 -2
  40. package/dist/utils/unevaluatedExpression.d.ts +3 -1
  41. package/dist/utils/unevaluatedExpression.js +18 -0
  42. package/dist/utils/visibilityDiagnostic.d.ts +14 -5
  43. package/dist/utils/visibilityDiagnostic.js +10 -2
  44. 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
- * and returns the first PageSchema whose `type === 'record'` and `object`
11
- * matches the requested name.
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 DECLARATION ORDER: the first matching page wins, so callers
22
- * can deterministically fall back to the auto-generated DetailView when no
23
- * record Page is authored.
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 `PageSchema` in
27
- * `@objectstack/spec` first — see the note on the selection itself below.
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.type !== 'record')
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
- // Declaration order decides: the first match wins.
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
- // This used to sort on a `priority` key read off the candidate. No author
105
- // could ever set it: `PageSchema` is a `strictObject` and does not declare
106
- // `priority`, so a page carrying one is a HARD PARSE ERROR, not a page that
107
- // sorts first. Every candidate therefore read `0`, the comparator returned
108
- // `0` for every pair, and a stable sort left declaration order untouched —
109
- // the sort documented an affordance the schema refuses (objectui#7298).
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
- // `isDefault` is not consulted here either. It IS declared on `PageSchema`,
112
- // so unlike `priority` it is writable — but nothing in this decision reads
113
- // it, which makes the flag on a shipped page (e.g. the platform's
114
- // `sys_user_detail`) inert while looking decisive. Reading it would be new
115
- // behaviour, not a repair; it is recorded here so the next reader does not
116
- // mistake the flag for the thing that picks the page.
117
- return candidates[0];
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 + host-authenticated fetch
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
- // Default the 'api' provider's fetch to the host's authenticated fetch
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
- * Every gate accepts the `props` spelling — `BaseSchema` is `.passthrough()`
38
- * with `[key: string]: any` — and `props` is documented as the annotated legacy
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 four negated legs (`visible` /
18
- * `visibleWhen` / `visibleOn` / `visibility`) that `true` means SHOWN — so "this
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 four
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 third case to answer. That is a type-level change only: no runtime
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 four negated legs (`visible` /
18
- * `visibleWhen` / `visibleOn` / `visibility`) that `true` means SHOWN — so "this
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": "17.7.0",
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": "^17.1.0",
28
- "@object-ui/data-objectstack": "17.7.0",
29
- "@object-ui/types": "17.7.0",
30
- "@object-ui/core": "17.7.0",
31
- "@object-ui/i18n": "17.7.0"
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",