@object-ui/react 17.3.0 → 17.5.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 (36) hide show
  1. package/CHANGELOG.md +908 -0
  2. package/README.md +39 -13
  3. package/dist/SchemaRenderer.d.ts +78 -5
  4. package/dist/SchemaRenderer.js +139 -13
  5. package/dist/context/AppShellContext.d.ts +21 -2
  6. package/dist/context/RecordContext.d.ts +0 -4
  7. package/dist/context/RecordContext.js +0 -2
  8. package/dist/context/RelatedRecordActionsContext.d.ts +29 -0
  9. package/dist/context/RelatedRecordActionsContext.js +6 -0
  10. package/dist/element-data-source/ElementDataSourceGate.d.ts +199 -0
  11. package/dist/element-data-source/ElementDataSourceGate.js +195 -0
  12. package/dist/hooks/index.d.ts +3 -0
  13. package/dist/hooks/index.js +10 -0
  14. package/dist/hooks/useActionTextLocalizer.d.ts +69 -0
  15. package/dist/hooks/useActionTextLocalizer.js +91 -0
  16. package/dist/hooks/useDatasetDimensionLabels.d.ts +104 -0
  17. package/dist/hooks/useDatasetDimensionLabels.js +154 -0
  18. package/dist/hooks/useElementDataSource.d.ts +68 -0
  19. package/dist/hooks/useElementDataSource.js +138 -0
  20. package/dist/hooks/useExpression.d.ts +58 -0
  21. package/dist/hooks/useExpression.js +65 -0
  22. package/dist/hooks/useNavigationOverlay.d.ts +58 -10
  23. package/dist/hooks/useOffline.d.ts +62 -36
  24. package/dist/hooks/useOffline.js +2 -2
  25. package/dist/hooks/usePageAssignment.js +4 -4
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.js +5 -1
  28. package/dist/schema-input.d.ts +31 -0
  29. package/dist/schema-input.js +31 -0
  30. package/dist/spec-bridge/SpecBridge.d.ts +11 -5
  31. package/dist/spec-bridge/SpecBridge.js +7 -1
  32. package/dist/spec-bridge/bridges/list-view.js +106 -14
  33. package/dist/spec-bridge/types.d.ts +24 -3
  34. package/dist/utils/i18n.d.ts +43 -8
  35. package/dist/utils/i18n.js +42 -4
  36. package/package.json +8 -8
package/README.md CHANGED
@@ -121,6 +121,43 @@ function MyComponent() {
121
121
  }
122
122
  ```
123
123
 
124
+ ### useElementDataSourceSchema / ElementDataSourceGate
125
+
126
+ Consume `PageComponentSchema.dataSource` — the spec's per-element data binding
127
+ (`{ object, view?, filter?, sort?, limit? }`) — in a block that has its own key
128
+ names. `useElementDataSource` resolves the binding (fetching the object's saved
129
+ views so `view` can be matched); these two apply the composed result to the
130
+ block's schema and render the two non-final states.
131
+
132
+ ```tsx
133
+ import { ElementDataSourceGate } from '@object-ui/react'
134
+
135
+ // `mapping` names ONLY the keys this block reads. A composed value written onto
136
+ // a key the block ignores would be accepted and silently dropped — the defect
137
+ // the binding exists to remove.
138
+ const OBJECT_GRID_BINDING = {
139
+ columns: true, // the view's FIELD list may fill `schema.columns`
140
+ filter: true, // AND-combined, never replaced ("additional criteria")
141
+ sort: true,
142
+ limit: 'pagination.pageSize' as const,
143
+ }
144
+
145
+ const ObjectGridRenderer = ({ schema, ...props }) => (
146
+ <ElementDataSourceGate schema={schema} mapping={OBJECT_GRID_BINDING} testId="object-grid">
147
+ {(bound) => <ObjectGrid schema={bound} {...props} />}
148
+ </ElementDataSourceGate>
149
+ )
150
+ ```
151
+
152
+ `object` lands on `objectName` by default (pass `object: 'apiName'` for another
153
+ key, or `object: false` for a block that reads the composed binding itself).
154
+ Precedence: binding keys beat the component's own, view-sourced values are only a
155
+ baseline the component's own key overrides, and `filter` AND-combines all three.
156
+ A `view` name that does not resolve renders a configuration error rather than
157
+ falling back to the object's full scope. Use `useElementDataSourceSchema` (plus
158
+ the exported `ElementDataSourceErrorPanel` / `ElementDataSourceLoadingPanel`) when
159
+ a block cannot be wrapped — a renderer whose hooks must run before the panels.
160
+
124
161
  ### useRegistry
125
162
 
126
163
  Access the component registry:
@@ -225,22 +262,11 @@ leaves the surface on its own anchor), `visibleNotificationStack` (`maxVisible`
225
262
 
226
263
  ## API Reference
227
264
 
228
- See [full documentation](https://objectui.org/api/react) for detailed API reference.
229
-
230
- <!-- release-metadata:v3.3.0 -->
231
-
232
- ## Compatibility
233
-
234
- - **React:** 18.x or 19.x
235
- - **Node.js:** ≥ 18
236
- - **TypeScript:** ≥ 5.0 (strict mode)
237
- - **`@objectstack/spec`:** ^3.3.0
238
- - **`@objectstack/client`:** ^3.3.0
239
- - **Tailwind CSS:** ≥ 3.4 (for packages with UI)
265
+ See [full documentation](https://objectui.org/docs/core/schema-renderer) for detailed API reference.
240
266
 
241
267
  ## Links
242
268
 
243
- - 📚 [Documentation](https://www.objectui.org/docs/packages/react)
269
+ - 📚 [Documentation](https://www.objectui.org/docs/core/schema-renderer)
244
270
  - 📦 [npm package](https://www.npmjs.com/package/@object-ui/react)
245
271
  - 📝 [Changelog](./CHANGELOG.md)
246
272
  - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues)
@@ -5,8 +5,8 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- import React, { Component } from 'react';
9
- import { SchemaNode } from '@object-ui/core';
8
+ import React, { Component, type ForwardRefExoticComponent, type RefAttributes } from 'react';
9
+ import type { BaseSchema } from '@object-ui/types';
10
10
  /**
11
11
  * Per-component Error Boundary for SchemaRenderer.
12
12
  * Catches render errors in individual components, preventing one broken
@@ -30,7 +30,80 @@ export declare class SchemaErrorBoundary extends Component<{
30
30
  handleRetry: () => void;
31
31
  render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
32
32
  }
33
- export declare const SchemaRenderer: React.ForwardRefExoticComponent<Omit<{
34
- schema: SchemaNode;
35
- } & Record<string, any>, "ref"> & React.RefAttributes<any>>;
33
+ /**
34
+ * The props `SchemaRenderer` DECLARES and reads itself (objectui#4548).
35
+ *
36
+ * ## Why `schema` is spelled as this union and not as a `SchemaNode`
37
+ *
38
+ * The repo carries two competing `SchemaNode` types: `@object-ui/core`'s
39
+ * interface (which requires `type: string`) and `@object-ui/types`' union
40
+ * (`BaseSchema | string | number | boolean | null | undefined`). This component
41
+ * matched NEITHER. It declared core's — narrower than what it accepts, so every
42
+ * caller holding the types union was wrong and could not be told — while its
43
+ * runtime returns early for strings and nullish, which core's interface forbids.
44
+ * The erasure below hid the mismatch completely: with props collapsed to an
45
+ * index signature, `schema` resolved to `any` and no call site was ever checked.
46
+ *
47
+ * So the contract is STATED HERE, by this component, as what it actually
48
+ * handles: an object schema, a bare string (rendered as text), or nothing at
49
+ * all. `number` / `boolean` are deliberately excluded — the runtime tolerates
50
+ * them defensively (see the primitive guard in the evaluation memo) but no
51
+ * author should be invited to pass them. Reconciling the two repo-wide
52
+ * `SchemaNode` spellings is a separate concern and deliberately not done here.
53
+ */
54
+ export interface SchemaRendererProps {
55
+ schema: BaseSchema | string | null | undefined;
56
+ }
57
+ /**
58
+ * The open forwarding surface, named once so it reads as the decision it is.
59
+ *
60
+ * `SchemaRenderer` passes every prop it does not itself read straight through to
61
+ * the component the schema names, which is resolved at RUNTIME from a
62
+ * plugin-extensible registry — so the set of valid keys is not knowable here,
63
+ * and `packages/react/README.md` documents callers relying on it. The `any` is
64
+ * the point: this is a pass-through channel, not a typed prop.
65
+ */
66
+ type ForwardedProps = Record<string, any>;
67
+ /**
68
+ * The renderer loop.
69
+ *
70
+ * ## The explicit type annotation is the contract, and it is deliberate
71
+ *
72
+ * `forwardRef< T, P >` routes `P` through React's `PropsWithoutRef`:
73
+ *
74
+ * Props extends any ? ('ref' extends keyof Props ? Omit< Props, 'ref' > : Props) : Props
75
+ *
76
+ * A string index signature on `P` puts `string` into `keyof Props`, so
77
+ * `'ref' extends keyof Props` is ALWAYS true, the `Omit` branch always runs, and
78
+ * `Omit` over a type carrying a string index signature keeps ONLY the index
79
+ * signature. Every declared prop is erased — on both sides. This component used
80
+ * to hand `forwardRef` a props type of `{ schema: SchemaNode } & Record< string,
81
+ * any >`, so its own `schema` resolved to `any` at every one of the ~376 call
82
+ * sites in this repo, and was not even REQUIRED: `< SchemaRenderer / >` with no
83
+ * schema at all type-checked (objectui#4548, measured).
84
+ *
85
+ * The fix is NOT to close the surface. This is the renderer loop: it forwards
86
+ * every prop it does not read to the component the schema names, resolved at
87
+ * RUNTIME from a plugin-extensible `ComponentRegistry`. That forwarding is a
88
+ * documented, load-bearing feature — `packages/react/README.md` shows
89
+ * `< SchemaRenderer schema={formSchema} onSubmit={handleSubmit} / >`, and
90
+ * `@object-ui/components`' form renderer consumes that `onSubmit` as a React
91
+ * prop. A closed props type would state a FALSE contract and would force every
92
+ * leaf plugin's props into this package to stay usable.
93
+ *
94
+ * So the two halves are separated deliberately:
95
+ *
96
+ * * the `forwardRef` TYPE ARGUMENT is the honest `SchemaRendererProps`, with
97
+ * no index signature — nothing for `PropsWithoutRef` to collapse, so
98
+ * `schema` survives to the call site typed and required; and
99
+ * * the open forwarding surface is stated ONCE, here, in this export
100
+ * annotation. Because the annotation is applied to the already-built
101
+ * component, `PropsWithoutRef` never runs over it, so `Record< string, any >`
102
+ * widens the surface WITHOUT erasing anything.
103
+ *
104
+ * The repo-wide guard (`scripts/__tests__/forwardref-props-erasure.guard.test.ts`)
105
+ * judges the TYPE ARGUMENT only, for exactly this reason: an index signature
106
+ * there is an accidental eraser, whereas one here is a stated contract.
107
+ */
108
+ export declare const SchemaRenderer: ForwardRefExoticComponent<SchemaRendererProps & ForwardedProps & RefAttributes<unknown>>;
36
109
  export {};
@@ -7,11 +7,11 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
7
7
  * LICENSE file in the root directory of this source tree.
8
8
  */
9
9
  import React, { forwardRef, useContext, useMemo, useEffect, useReducer, useState, Component } from 'react';
10
- import { ComponentRegistry, ExpressionEvaluator, isObjectUIError, ERROR_CODES, debugLog, debugTime, debugTimeEnd, DebugCollector, validateSchema, hasResponsiveStyles, scopeClassFor, compileScopedStyles, } from '@object-ui/core';
10
+ import { ComponentRegistry, ExpressionEvaluator, isObjectUIError, ERROR_CODES, debugLog, debugTime, debugTimeEnd, DebugCollector, validateSchema, hasDeclaredPredicate, hasResponsiveStyles, scopeClassFor, compileScopedStyles, } from '@object-ui/core';
11
11
  import { SchemaRendererContext } from './context/SchemaRendererContext';
12
12
  import { usePredicateScope } from './hooks/useExpression';
13
13
  import { usePageVariables } from './hooks/usePageVariables';
14
- import { resolveI18nLabel } from './utils/i18n';
14
+ import { resolveKeyedI18nLabel } from './utils/i18n';
15
15
  /**
16
16
  * Dev-mode schema validation.
17
17
  *
@@ -84,7 +84,7 @@ function validateSchemaOnce(schema) {
84
84
  function resolveAriaProps(schema) {
85
85
  const aria = {};
86
86
  if (schema.ariaLabel) {
87
- aria['aria-label'] = resolveI18nLabel(schema.ariaLabel);
87
+ aria['aria-label'] = resolveKeyedI18nLabel(schema.ariaLabel);
88
88
  }
89
89
  if (schema.ariaDescribedBy) {
90
90
  aria['aria-describedby'] = schema.ariaDescribedBy;
@@ -131,7 +131,7 @@ export class SchemaErrorBoundary extends Component {
131
131
  const error = this.state.error;
132
132
  const isDev = globalThis.process?.env?.NODE_ENV !== 'production';
133
133
  const objuiError = isObjectUIError(error) ? error : null;
134
- return (_jsxs("div", { className: "p-4 border border-orange-400 rounded bg-orange-50 text-orange-700 my-2", role: "alert", children: [_jsxs("p", { className: "font-medium", children: ["Component", this.props.componentType ? ` "${this.props.componentType}"` : '', " failed to render"] }), _jsx("p", { className: "text-sm mt-1", children: error.message }), isDev && objuiError?.code && (_jsxs("p", { className: "text-xs mt-1 text-orange-500", children: ["Error code: ", objuiError.code, objuiError.details?.suggestion ? (_jsxs("span", { className: "block mt-0.5", children: ["\uD83D\uDCA1 ", String(objuiError.details.suggestion)] })) : null] })), _jsx("button", { onClick: this.handleRetry, className: "mt-2 text-sm underline hover:no-underline", children: "Retry" })] }));
134
+ return (_jsxs("div", { className: "p-4 border border-orange-400 rounded bg-orange-50 text-orange-700 my-2", role: "alert", children: [_jsxs("p", { className: "font-medium", children: ["Component", this.props.componentType ? ` "${this.props.componentType}"` : '', " failed to render"] }), _jsx("p", { className: "text-sm mt-1", children: error.message }), isDev && objuiError?.code && (_jsxs("p", { className: "text-xs mt-1 text-orange-500", children: ["Error code: ", objuiError.code, objuiError.details?.suggestion ? (_jsxs("span", { className: "block mt-0.5", children: ["\uD83D\uDCA1 ", String(objuiError.details.suggestion)] })) : null] })), _jsx("button", { type: "button", onClick: this.handleRetry, className: "mt-2 text-sm underline hover:no-underline", children: "Retry" })] }));
135
135
  }
136
136
  return this.props.children;
137
137
  }
@@ -146,6 +146,47 @@ export class SchemaErrorBoundary extends Component {
146
146
  * `useState` wiped (objectui#2954's latent hazard, made real by this line).
147
147
  */
148
148
  const NO_DATA_SOURCE = {};
149
+ /**
150
+ * The renderer loop.
151
+ *
152
+ * ## The explicit type annotation is the contract, and it is deliberate
153
+ *
154
+ * `forwardRef< T, P >` routes `P` through React's `PropsWithoutRef`:
155
+ *
156
+ * Props extends any ? ('ref' extends keyof Props ? Omit< Props, 'ref' > : Props) : Props
157
+ *
158
+ * A string index signature on `P` puts `string` into `keyof Props`, so
159
+ * `'ref' extends keyof Props` is ALWAYS true, the `Omit` branch always runs, and
160
+ * `Omit` over a type carrying a string index signature keeps ONLY the index
161
+ * signature. Every declared prop is erased — on both sides. This component used
162
+ * to hand `forwardRef` a props type of `{ schema: SchemaNode } & Record< string,
163
+ * any >`, so its own `schema` resolved to `any` at every one of the ~376 call
164
+ * sites in this repo, and was not even REQUIRED: `< SchemaRenderer / >` with no
165
+ * schema at all type-checked (objectui#4548, measured).
166
+ *
167
+ * The fix is NOT to close the surface. This is the renderer loop: it forwards
168
+ * every prop it does not read to the component the schema names, resolved at
169
+ * RUNTIME from a plugin-extensible `ComponentRegistry`. That forwarding is a
170
+ * documented, load-bearing feature — `packages/react/README.md` shows
171
+ * `< SchemaRenderer schema={formSchema} onSubmit={handleSubmit} / >`, and
172
+ * `@object-ui/components`' form renderer consumes that `onSubmit` as a React
173
+ * prop. A closed props type would state a FALSE contract and would force every
174
+ * leaf plugin's props into this package to stay usable.
175
+ *
176
+ * So the two halves are separated deliberately:
177
+ *
178
+ * * the `forwardRef` TYPE ARGUMENT is the honest `SchemaRendererProps`, with
179
+ * no index signature — nothing for `PropsWithoutRef` to collapse, so
180
+ * `schema` survives to the call site typed and required; and
181
+ * * the open forwarding surface is stated ONCE, here, in this export
182
+ * annotation. Because the annotation is applied to the already-built
183
+ * component, `PropsWithoutRef` never runs over it, so `Record< string, any >`
184
+ * widens the surface WITHOUT erasing anything.
185
+ *
186
+ * The repo-wide guard (`scripts/__tests__/forwardref-props-erasure.guard.test.ts`)
187
+ * judges the TYPE ARGUMENT only, for exactly this reason: an index signature
188
+ * there is an accidental eraser, whereas one here is a stated contract.
189
+ */
149
190
  export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
150
191
  const context = useContext(SchemaRendererContext);
151
192
  const dataSource = context?.dataSource || NO_DATA_SOURCE;
@@ -176,7 +217,19 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
176
217
  const autoStyleId = React.useId();
177
218
  // Evaluate schema expressions against the data source
178
219
  const evaluatedSchema = useMemo(() => {
179
- if (!schema || typeof schema === 'string')
220
+ // Nothing to evaluate unless the node is an OBJECT. `!schema` covers the
221
+ // nullish/empty cases and `typeof !== 'object'` covers every primitive —
222
+ // both return the value untouched for the render pass below to place.
223
+ //
224
+ // The `typeof` half is what used to be missing (objectui#4548): the guard
225
+ // named `string` only, so a `number` or `true` fell through to the
226
+ // `{ ...schema }` shallow copy on the next line, spread to an EMPTY object,
227
+ // lost its `type`, and surfaced as the red "Unknown component type:
228
+ // undefined" box — an accident of the spread, not a decision. The declared
229
+ // props type now excludes those primitives outright; this guard is the
230
+ // defence-in-depth behind it, and it is what makes the copy below provably
231
+ // an object spread.
232
+ if (!schema || typeof schema !== 'object')
180
233
  return schema;
181
234
  // `data` (record/datasource) plus the ambient host scope. `current_user`
182
235
  // is aliased to `user` so both `user.email` and `current_user.email`
@@ -248,10 +301,28 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
248
301
  if (newSchema.visibility !== undefined) {
249
302
  return !evaluator.evaluateCondition(newSchema.visibility);
250
303
  }
251
- if (newSchema.hidden !== undefined) {
304
+ // Ask "is a `hidden` gate DECLARED?" — not "is the key present?"
305
+ // (objectui#3955). These two legs are the only ones in this chain whose
306
+ // verdict is NOT negated, so the evaluator's single default for "there is
307
+ // nothing to evaluate" (`true`, meaning *visible/enabled*) arrives here
308
+ // meaning HIDE: `hidden: ''` / `null` (`null !== undefined`) / a
309
+ // whitespace-only string / the `{ dialect, source: '' }` envelope
310
+ // `objectstack build` emits for an empty predicate each made the node
311
+ // VANISH, on the generic path, for a value the metadata never used to say
312
+ // anything. Harder to diagnose than its `disabled` twin below
313
+ // (objectui#3862): a greyed-out control is still on screen, a node that
314
+ // never rendered is indistinguishable from metadata that meant it.
315
+ //
316
+ // `hasDeclaredPredicate` is the repo's one definition of "declared"
317
+ // (core's `evaluator/declaredPredicate.ts`) — a local `&& !== ''` here
318
+ // would have been the Nth dialect of one question. The verdict still reads
319
+ // the RAW value; only the gate in front of it narrowed. Not an
320
+ // equivalence, and pinned as a behaviour change: an UNDECLARED `hidden` no
321
+ // longer short-circuits, so a declared `hiddenOn` is finally consulted.
322
+ if (hasDeclaredPredicate(newSchema.hidden)) {
252
323
  return evaluator.evaluateCondition(newSchema.hidden);
253
324
  }
254
- if (newSchema.hiddenOn !== undefined) {
325
+ if (hasDeclaredPredicate(newSchema.hiddenOn)) {
255
326
  return evaluator.evaluateCondition(newSchema.hiddenOn);
256
327
  }
257
328
  return false;
@@ -260,11 +331,41 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
260
331
  newSchema._hidden = true;
261
332
  }
262
333
  // Evaluate disabled: disabled / disabledOn
334
+ //
335
+ // Ask "is a `disabled` gate DECLARED?" — not "is the key present?"
336
+ // (objectui#3862). `!== undefined` was the widest of the three spellings this
337
+ // question used to have, and this is the key where breadth is not free: the
338
+ // evaluation entry answers an empty predicate with `true`, meaning "no
339
+ // condition → visible/enabled", and on `disabled` that `true` means GREYED
340
+ // OUT. So `disabled: ''`, `disabled: null` (`null !== undefined`), a
341
+ // whitespace-only string and the `{ dialect, source: '' }` envelope
342
+ // `objectstack build` emits for an empty predicate each disabled the node —
343
+ // on the GENERIC path, since this block runs for every schema type, and not
344
+ // as an internal flag either: `_disabled` is forwarded below as a real
345
+ // `disabled` prop.
346
+ //
347
+ // The `visible` / `visibleWhen` / `visibleOn` / `visibility` legs above keep
348
+ // `!== undefined` deliberately: their `true` is NEGATED, so an empty predicate
349
+ // already lands on "shown", which is what "no gate" means anyway, and
350
+ // narrowing them would change ALIAS PRECEDENCE rather than fix anything. The
351
+ // `hidden` / `hiddenOn` legs were the exception — not negated, so they carried
352
+ // this same defect with the polarity that makes the node VANISH — and they now
353
+ // read this same definition (objectui#3955).
354
+ //
355
+ // `hasDeclaredPredicate` is the one definition of "declared" (core's
356
+ // `evaluator/declaredPredicate.ts`, objectui#3850's ruling — read there for
357
+ // the scope), shared with the action renderers' `hasDeclaredVisibilityGate`
358
+ // and `ActionRunner`'s execution gates. A local `&& !== ''` here would have
359
+ // been a fourth dialect of one question, which is what objectui#3842 /
360
+ // objectui#3849 spent two PRs merging away. The verdict still reads the RAW
361
+ // value, exactly as before — only the gate in front of it narrowed. An
362
+ // undeclared `disabled` now falls through to `disabledOn` instead of
363
+ // short-circuiting on an empty predicate.
263
364
  const isDisabled = (() => {
264
- if (newSchema.disabled !== undefined) {
365
+ if (hasDeclaredPredicate(newSchema.disabled)) {
265
366
  return evaluator.evaluateCondition(newSchema.disabled);
266
367
  }
267
- if (newSchema.disabledOn !== undefined) {
368
+ if (hasDeclaredPredicate(newSchema.disabledOn)) {
268
369
  return evaluator.evaluateCondition(newSchema.disabledOn);
269
370
  }
270
371
  return false;
@@ -276,12 +377,25 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
276
377
  }, [schema, dataSource, predicateScope, pageVariables]);
277
378
  if (!evaluatedSchema)
278
379
  return null;
279
- // Handle visibility: if evaluated schema is hidden, render nothing
280
- if (evaluatedSchema?._hidden)
281
- return null;
282
380
  // If schema is just a string, render it as text
283
381
  if (typeof evaluatedSchema === 'string')
284
382
  return _jsx(_Fragment, { children: evaluatedSchema });
383
+ // Any other primitive that reached here renders as its text too
384
+ // (objectui#4548). The declared props type does not admit `number` /
385
+ // `boolean`, so this is unreachable from typed code and exists for untyped
386
+ // callers and stored metadata; it replaces the empty-spread "Unknown
387
+ // component type: undefined" box, which said nothing true about the input.
388
+ if (typeof evaluatedSchema !== 'object')
389
+ return _jsx(_Fragment, { children: String(evaluatedSchema) });
390
+ // Handle visibility: if evaluated schema is hidden, render nothing.
391
+ //
392
+ // Reads AFTER the primitive narrowing above (objectui#4548) so the property
393
+ // access is on a known object. Behaviour is unchanged: `_hidden` is only ever
394
+ // set on the object copy built in the memo, so for a string or any other
395
+ // primitive this test was always falsy and fell through to exactly the
396
+ // branches that now precede it.
397
+ if (evaluatedSchema._hidden)
398
+ return null;
285
399
  // Dev-mode validation: log once per schema object, attach visual flag
286
400
  // when invalid. Production path returns { valid: true, messages: [] }
287
401
  // without doing any work.
@@ -308,7 +422,19 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
308
422
  // Note: We don't forward the ref to the Component because components in the registry
309
423
  // may not support refs. The SchemaRenderer itself can still receive refs for its own use.
310
424
  // Extract schema metadata properties that should NOT be passed as React props
311
- const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleWhen: _visibleWhen, visibleOn: _visibleOn, visibility: _visibility, hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn, _hidden: __hidden, // stripped: internal visibility flag
425
+ const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleWhen: _visibleWhen, visibleOn: _visibleOn, visibility: _visibility, hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn,
426
+ // stripped: `PageComponentSchema.dataSource` is the spec's per-element data
427
+ // BINDING (`{ object, view, filter, sort, limit }`) — schema metadata, like
428
+ // `visibleWhen` above, not a visual prop. Renderers that consume it read
429
+ // `schema.dataSource` (element:record_picker, list-view); it must not be
430
+ // spread as a React prop, because several data-bound blocks take the
431
+ // injected data-source ADAPTER under that very name. Spreading it shadowed
432
+ // the adapter with a plain `{ object, view }` object, so the first
433
+ // `dataSource.find(…)` threw `dataSource.find is not a function` and the
434
+ // block reported "Couldn't load records" — writing the binding the spec
435
+ // documents BROKE the component (objectstack#5576). An explicit React
436
+ // `dataSource` prop is unaffected: it arrives via `...props`, spread last.
437
+ dataSource: _dataSource, _hidden: __hidden, // stripped: internal visibility flag
312
438
  _disabled: __disabled, // stripped: internal disabled flag
313
439
  responsiveStyles: _responsiveStyles, // stripped: compiled to scoped CSS, not a DOM prop
314
440
  ...componentProps } = evaluatedSchema;
@@ -1,7 +1,26 @@
1
1
  import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
2
2
  export declare const AdapterCtx: import("react").Context<ObjectStackAdapter<unknown> | null>;
3
3
  export declare function useAdapter(): ObjectStackAdapter | null;
4
- export interface MetadataState {
4
+ /**
5
+ * The metadata PROVIDER's cache — the collections `<MetadataProvider>` holds in
6
+ * React state, plus its aggregate load flags.
7
+ *
8
+ * Renamed from `MetadataState` in objectui#4167. `@objectstack/spec` 17.0.0-rc.6
9
+ * publishes `MetadataState` from `@objectstack/spec/system`, and it names a
10
+ * different thing in the SAME domain and the SAME two words: a metadata item's
11
+ * LIFECYCLE state, `'draft' | 'active' | 'deprecated' | 'archived'`
12
+ * (`MetadataStateSchema`). That is the `AuthProviderConfig` judgement from
13
+ * objectstack#4115 batch 5, not the `AuthProvider` one — nothing about a JSX
14
+ * element can be mistaken for a zod enum, but "the metadata state" absolutely
15
+ * can be read as canonical for the lifecycle enum by the next agent, and the
16
+ * two are mutually unassignable (an object of five arrays vs a string union),
17
+ * so nothing would have caught the misreading at the point it was made.
18
+ *
19
+ * `Cache` rather than a bare disambiguating prefix because it is what this is:
20
+ * `MetadataProvider` is a TTL cache with per-type entries, and these five
21
+ * arrays are its materialized contents.
22
+ */
23
+ export interface MetadataCacheState {
5
24
  apps: any[];
6
25
  objects: any[];
7
26
  dashboards: any[];
@@ -11,7 +30,7 @@ export interface MetadataState {
11
30
  error: Error | null;
12
31
  }
13
32
  export type MetadataTypeStatus = 'idle' | 'loading' | 'ready' | 'error';
14
- export interface MetadataContextValue extends MetadataState {
33
+ export interface MetadataContextValue extends MetadataCacheState {
15
34
  refresh: (type?: string) => Promise<void>;
16
35
  invalidate: (type: string, name?: string) => void;
17
36
  ensureType: (type: string) => Promise<any[]>;
@@ -35,10 +35,6 @@ export interface RecordContextValue<TData = any, TObjectSchema = any> {
35
35
  data?: TData;
36
36
  /** Resolved object metadata schema (fields, label, etc.). */
37
37
  objectSchema?: TObjectSchema;
38
- /** True while the record is fetching. */
39
- loading?: boolean;
40
- /** Last fetch error, if any. */
41
- error?: Error | null;
42
38
  /** Re-fetch the record from the source. */
43
39
  refresh?: () => void | Promise<void>;
44
40
  /**
@@ -96,8 +96,6 @@ export const RecordContextProvider = ({ children, ...value }) => {
96
96
  value.dataSource,
97
97
  value.data,
98
98
  value.objectSchema,
99
- value.loading,
100
- value.error,
101
99
  value.refresh,
102
100
  value.embedded,
103
101
  value.headerSystemActions,
@@ -24,6 +24,12 @@
24
24
  * affordance. When no provider is present at all (e.g. the Studio designer, or
25
25
  * a standalone embedded renderer) the related list stays read-only, which is
26
26
  * the correct graceful fallback.
27
+ *
28
+ * Since objectui#4336 the same host also publishes its record-URL builder
29
+ * ({@link RelatedRecordActionsValue.recordHref} / `openRecord`) for any object
30
+ * it can route to, so the low-level renderers on a record page can turn a
31
+ * lookup VALUE into a link to the referenced record without assembling a URL
32
+ * of their own. Same provider, same builder, one route shape.
27
33
  */
28
34
  import React from 'react';
29
35
  /** A child-object action surfaced in a related list's row menu. */
@@ -92,6 +98,29 @@ export interface RelatedRecordActionsValue {
92
98
  * identity.
93
99
  */
94
100
  resolve: (input: ResolveRelatedRecordActionsInput) => RelatedRecordHandlers;
101
+ /**
102
+ * The host's record-detail URL for ANY object it can route to — the SAME
103
+ * builder {@link RelatedRecordHandlers.onView} navigates with, published as
104
+ * an href so a renderer can present the destination as a real anchor
105
+ * (middle-click, "open in new tab", "copy link address") instead of a
106
+ * click-only affordance.
107
+ *
108
+ * Added for objectui#4336: a lookup value is a reference to a record of a
109
+ * DIFFERENT object, and the renderer that displays it (`LookupCellRenderer`,
110
+ * shared by the detail body and the related list's cells) has no router and
111
+ * no knowledge of the console's route shape. Route building stays here, in
112
+ * the one place that already owns it — never re-derived renderer-side.
113
+ *
114
+ * Returns `null` when the host cannot route to that object (unknown object,
115
+ * no app context), which consumers must render as the plain value.
116
+ */
117
+ recordHref?: (objectName: string, recordId: string | number) => string | null;
118
+ /**
119
+ * SPA-navigate to the destination {@link recordHref} addresses. Consumers
120
+ * call this from a plain left click on the anchor so navigation stays
121
+ * in-app; a modifier click is left to the browser and the href.
122
+ */
123
+ openRecord?: (objectName: string, recordId: string | number) => void;
95
124
  }
96
125
  export declare const RelatedRecordActionsProvider: React.FC<{
97
126
  value: RelatedRecordActionsValue;
@@ -25,6 +25,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
25
25
  * affordance. When no provider is present at all (e.g. the Studio designer, or
26
26
  * a standalone embedded renderer) the related list stays read-only, which is
27
27
  * the correct graceful fallback.
28
+ *
29
+ * Since objectui#4336 the same host also publishes its record-URL builder
30
+ * ({@link RelatedRecordActionsValue.recordHref} / `openRecord`) for any object
31
+ * it can route to, so the low-level renderers on a record page can turn a
32
+ * lookup VALUE into a link to the referenced record without assembling a URL
33
+ * of their own. Same provider, same builder, one route shape.
28
34
  */
29
35
  import { createContext, useContext } from 'react';
30
36
  const RelatedRecordActionsContext = createContext(null);