@object-ui/react 17.6.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +3766 -0
  2. package/README.md +294 -38
  3. package/dist/SchemaRenderer.d.ts +38 -7
  4. package/dist/SchemaRenderer.js +1218 -87
  5. package/dist/context/ActionContext.js +40 -1
  6. package/dist/context/DiscussionContext.d.ts +12 -0
  7. package/dist/context/DiscussionContext.js +2 -0
  8. package/dist/context/DrillNavigationContext.d.ts +13 -0
  9. package/dist/context/InlineEditContext.d.ts +81 -1
  10. package/dist/context/InlineEditContext.js +8 -2
  11. package/dist/context/RecordContext.d.ts +48 -5
  12. package/dist/context/RecordContext.js +10 -1
  13. package/dist/context/RelatedRecordActionsContext.d.ts +5 -1
  14. package/dist/context/SchemaRendererContext.d.ts +54 -2
  15. package/dist/context/SchemaRendererContext.js +38 -5
  16. package/dist/context/ThemeContext.js +1 -1
  17. package/dist/context/actionRunnerTranslator.d.ts +27 -0
  18. package/dist/context/actionRunnerTranslator.js +37 -0
  19. package/dist/element-data-source/ElementDataSourceGate.d.ts +88 -1
  20. package/dist/element-data-source/ElementDataSourceGate.js +326 -13
  21. package/dist/hooks/index.d.ts +3 -1
  22. package/dist/hooks/index.js +10 -1
  23. package/dist/hooks/useActionEngine.js +46 -3
  24. package/dist/hooks/useActionRunner.js +4 -0
  25. package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
  26. package/dist/hooks/useActionTextLocalizer.js +33 -8
  27. package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
  28. package/dist/hooks/useConfigBagEvaluator.js +39 -0
  29. package/dist/hooks/useDensityMode.d.ts +6 -2
  30. package/dist/hooks/useDensityMode.js +6 -2
  31. package/dist/hooks/useDiscovery.d.ts +1 -10
  32. package/dist/hooks/useETagCache.js +25 -4
  33. package/dist/hooks/useElementDataSource.d.ts +15 -2
  34. package/dist/hooks/useElementDataSource.js +24 -7
  35. package/dist/hooks/useExpression.d.ts +52 -59
  36. package/dist/hooks/useExpression.js +58 -90
  37. package/dist/hooks/useFilterScope.d.ts +22 -2
  38. package/dist/hooks/useFilterScope.js +29 -4
  39. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  40. package/dist/hooks/useGlobalUndo.js +112 -46
  41. package/dist/hooks/useNavigationOverlay.d.ts +76 -11
  42. package/dist/hooks/useNavigationOverlay.js +119 -9
  43. package/dist/hooks/useOffline.js +65 -11
  44. package/dist/hooks/usePageAssignment.d.ts +8 -5
  45. package/dist/hooks/usePageAssignment.js +37 -16
  46. package/dist/hooks/useRecordSearch.d.ts +34 -1
  47. package/dist/hooks/useRecordSearch.js +42 -7
  48. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  49. package/dist/hooks/useResolvedFilter.js +130 -0
  50. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  51. package/dist/hooks/useSchemaPersistence.js +144 -5
  52. package/dist/hooks/useSettledSchema.d.ts +123 -0
  53. package/dist/hooks/useSettledSchema.js +186 -0
  54. package/dist/hooks/useViewData.d.ts +7 -2
  55. package/dist/index.d.ts +8 -1
  56. package/dist/index.js +45 -1
  57. package/dist/schema-input.d.ts +38 -8
  58. package/dist/schema-input.js +39 -7
  59. package/dist/utils/configBag.d.ts +68 -0
  60. package/dist/utils/configBag.js +70 -0
  61. package/dist/utils/error-message.d.ts +1 -1
  62. package/dist/utils/error-message.js +1 -1
  63. package/dist/utils/inlineAria.d.ts +61 -0
  64. package/dist/utils/inlineAria.js +70 -0
  65. package/dist/utils/nonGridRowCeiling.d.ts +44 -0
  66. package/dist/utils/nonGridRowCeiling.js +78 -0
  67. package/dist/utils/paramsBag.d.ts +28 -0
  68. package/dist/utils/paramsBag.js +146 -0
  69. package/dist/utils/propsBagDiagnostic.d.ts +196 -0
  70. package/dist/utils/propsBagDiagnostic.js +421 -0
  71. package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
  72. package/dist/utils/refusedDataPropDiagnostic.js +94 -0
  73. package/dist/utils/unevaluatedExpression.d.ts +42 -1
  74. package/dist/utils/unevaluatedExpression.js +74 -11
  75. package/dist/utils/visibilityDiagnostic.d.ts +271 -19
  76. package/dist/utils/visibilityDiagnostic.js +311 -44
  77. package/package.json +10 -10
  78. package/dist/hooks/useClientNotifications.d.ts +0 -47
  79. package/dist/hooks/useClientNotifications.js +0 -162
  80. package/dist/spec-bridge/SpecBridge.d.ts +0 -28
  81. package/dist/spec-bridge/SpecBridge.js +0 -56
  82. package/dist/spec-bridge/bridges/form-view.d.ts +0 -158
  83. package/dist/spec-bridge/bridges/form-view.js +0 -170
  84. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  85. package/dist/spec-bridge/bridges/list-view.js +0 -268
  86. package/dist/spec-bridge/index.d.ts +0 -11
  87. package/dist/spec-bridge/index.js +0 -10
  88. package/dist/spec-bridge/types.d.ts +0 -55
  89. package/dist/spec-bridge/types.js +0 -8
@@ -13,7 +13,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
13
13
  * Components can consume actions without creating their own runner instances.
14
14
  */
15
15
  import { createContext, useCallback, useContext, useMemo, useState } from 'react';
16
- import { ActionRunner, } from '@object-ui/core';
16
+ import { ActionRunner, subjectPermissionsOf, } from '@object-ui/core';
17
+ import { useActionRunnerTranslator } from './actionRunnerTranslator.js';
18
+ import { usePredicateScope } from '../hooks/useExpression.js';
17
19
  export const ActionCtxReact = createContext(null);
18
20
  /**
19
21
  * ActionProvider — Provides a shared ActionRunner to the tree.
@@ -34,6 +36,8 @@ export const ActionProvider = ({ children, context = {}, onConfirm, onToast, onM
34
36
  const [loading, setLoading] = useState(false);
35
37
  const [error, setError] = useState(null);
36
38
  const [result, setResult] = useState(null);
39
+ // The runner's own copy (the generic success toast) in the session's language.
40
+ const translate = useActionRunnerTranslator();
37
41
  const runner = useMemo(() => {
38
42
  // Normalize the evaluator scope so predicates can use *either* flat
39
43
  // (`record`, `user`, `objectName`) *or* the canonical `ctx.*` namespace
@@ -49,6 +53,7 @@ export const ActionProvider = ({ children, context = {}, onConfirm, onToast, onM
49
53
  : { ...context },
50
54
  };
51
55
  const r = new ActionRunner(normalizedContext);
56
+ r.setTranslator(translate);
52
57
  if (onConfirm)
53
58
  r.setConfirmHandler(onConfirm);
54
59
  if (onToast)
@@ -69,6 +74,40 @@ export const ActionProvider = ({ children, context = {}, onConfirm, onToast, onM
69
74
  return r;
70
75
  // eslint-disable-next-line react-hooks/exhaustive-deps
71
76
  }, [JSON.stringify(context)]);
77
+ // The acting subject the surrounding predicate scope binds as `current_user`
78
+ // (objectui#11242): the ONE object `ExpressionProvider` publishes under
79
+ // `current_user` / `user` / `ctx.user` / `os.user`, carrying the caller's
80
+ // effective object permissions once they have loaded. The runner evaluates
81
+ // its own gates — `execute`'s `disabled` re-check, `params[].visible`, the
82
+ // engine's `visible` filter — against ITS bag, which a host seeds with its
83
+ // own `user` and never with `current_user`. Bound here, every `execute` gate
84
+ // under this provider answers `current_user.can(…)` and every other
85
+ // `current_user.*` predicate, whatever else the page mounts. Before, only
86
+ // `useActionEngine` wrote it onto a shared runner, so the answer depended on
87
+ // whether the page happened to mount `record:quick_actions`, `record:alert`
88
+ // or a dashboard (the objectui#6493 "one bag" rule, broken per page).
89
+ //
90
+ // The same object, not a copy: the engine answers `can` only for a receiver
91
+ // IDENTICAL to the bound `current_user`. Only `current_user` is added — the
92
+ // runner's `user` / `ctx.user` / `os.user` stay the host's object, which
93
+ // carries the `systemPermissions` the capability gate reads — so on this bag
94
+ // `user.can(…)` still faults while `current_user.can(…)` answers. With no
95
+ // scope above this provider (`{}`) nothing is bound, and a host's own
96
+ // `context.current_user`, if it passed one, is left as it is.
97
+ //
98
+ // Bound during render, before any child renders or executes, and keyed on
99
+ // what the binding READS, never on the subject's identity (a memoised value
100
+ // upstream — AGENTS.md #10): its serialisable fields, and the permissions map
101
+ // it carries, which the upstream adapter caches per payload outside React. A
102
+ // new runner (the host `context` changed) is bound again.
103
+ const subject = usePredicateScope().current_user;
104
+ const subjectKey = subject === undefined ? '' : JSON.stringify(subject);
105
+ const subjectPermissions = subjectPermissionsOf(subject);
106
+ useMemo(() => {
107
+ if (subject !== undefined)
108
+ runner.updateContext({ current_user: subject });
109
+ // eslint-disable-next-line react-hooks/exhaustive-deps
110
+ }, [runner, subjectKey, subjectPermissions]);
72
111
  const execute = useCallback(async (action) => {
73
112
  setLoading(true);
74
113
  setError(null);
@@ -50,6 +50,18 @@ export interface DiscussionContextValue {
50
50
  onUploadAttachments?: (files: FileList) => Promise<DiscussionAttachment[]>;
51
51
  loading?: boolean;
52
52
  error?: Error | null;
53
+ /**
54
+ * The host's `sys_activity` read was REFUSED: 401 / 403, or a permission
55
+ * envelope (objectui#11195). The feed renderers then show a no-permission
56
+ * state rather than claiming the record has no activity. Produced by the
57
+ * host that owns the fetch (app-shell `RecordDetailView`), which judges the
58
+ * rejection with plugin-detail's `isRefusedFeedRead`. A read that answered
59
+ * with zero rows, a 404, or any other failure is not a refusal and leaves
60
+ * this unset.
61
+ */
62
+ activityDenied?: boolean;
63
+ /** The same, for the host's `sys_comment` read. */
64
+ commentsDenied?: boolean;
53
65
  }
54
66
  export interface DiscussionContextProviderProps extends DiscussionContextValue {
55
67
  children: React.ReactNode;
@@ -28,6 +28,8 @@ export const DiscussionContextProvider = ({ children, ...value }) => {
28
28
  value.onUploadAttachments,
29
29
  value.loading,
30
30
  value.error,
31
+ value.activityDenied,
32
+ value.commentsDenied,
31
33
  ]);
32
34
  return (_jsx(DiscussionContext.Provider, { value: memo, children: children }));
33
35
  };
@@ -27,6 +27,19 @@ export interface DrillNavigationValue {
27
27
  * Open the full list page for `objectName`, scoped by `filter` (an object
28
28
  * FIELD name → raw stored value map). Implemented by the app shell as a
29
29
  * SPA navigation to the object's list route. Absent when no host wired it.
30
+ *
31
+ * ⚠️ `filter` is the widget's AUTHORED filter composed with the click
32
+ * context, so it may still carry unresolved placeholders — `{current_user_id}`,
33
+ * `{current_org_id}`, and the relative-date macros. **Resolving them is the
34
+ * host implementation's job**: call `resolveFilterPlaceholders(filter, scope)`
35
+ * from `@object-ui/core` with the session scope the host holds
36
+ * (`useFilterScope`), exactly once, before the value reaches a query or a URL.
37
+ *
38
+ * A host that skips it gives ONE drill TWO scopes: the in-place drawer arm is
39
+ * already scoped by the resolved value, because the `object-data-table` it
40
+ * renders resolves `schema.filter` in its own fetch, so only the navigate arm
41
+ * would carry the literal — and a literal placeholder matches no record
42
+ * (objectui#9022).
30
43
  */
31
44
  openRecordList?: (objectName: string, filter?: Record<string, unknown>) => void;
32
45
  }
@@ -95,6 +95,37 @@ export interface InlineEditContextValue {
95
95
  * doesn't resolve approvals at all.
96
96
  */
97
97
  approvalProgress?: ApprovalProgress;
98
+ /**
99
+ * Whether the viewer is the SUBMITTER of the pending approval request
100
+ * (`830ed5803`). Recall is the submitter's lever and the server enforces
101
+ * exactly that — a non-submitter's recall is refused — so a recall button
102
+ * lit for everyone who can read the record is a button whose click must
103
+ * fail. The host resolves the identity (server-resolved `viewer.is_submitter`
104
+ * on the pending request, with an id comparison as the pre-framework#3310
105
+ * fallback) for the same reason it resolves `locked`: the renderer stays
106
+ * DataSource-agnostic and never re-derives who submitted what.
107
+ *
108
+ * TRI-STATE, and the third state is the load-bearing one:
109
+ * `true` — the viewer submitted this approval; offer recall.
110
+ * `false` — the host consulted the pending request and this viewer is
111
+ * not its submitter; withdraw the recall affordance.
112
+ * `undefined` — the host does not resolve submitter identity at all
113
+ * (no approvals API, band driven by the record's
114
+ * `approval_status` mirror alone). Behaviour is UNCHANGED
115
+ * from before this signal existed: recall stays offered.
116
+ *
117
+ * The `undefined` case defaults to *showing* on purpose, mirroring how
118
+ * `approvalPending` falls back to `locked`: omission preserves a host's
119
+ * previous behaviour. Defaulting it to "hide" would silently take the
120
+ * submitter's only unlock lever away from every host that never learns of
121
+ * this prop — trading a cosmetic defect for a functional loss.
122
+ *
123
+ * ⚠️ FEEDBACK ONLY. This decides who is SHOWN the entry, never who MAY
124
+ * recall: the server authorizes the recall itself and rejects a
125
+ * non-submitter regardless of what any client renders. Nothing may treat
126
+ * this as an authorization verdict.
127
+ */
128
+ approvalIsSubmitter?: boolean;
98
129
  /**
99
130
  * Human-readable reason for the approval lock, surfaced as the band's
100
131
  * tooltip. Optional — consumers fall back to their own localized default
@@ -104,7 +135,15 @@ export interface InlineEditContextValue {
104
135
  /**
105
136
  * Draft of user-edited values. Holds ONLY the keys the user actually
106
137
  * changed, so the save path never writes computed / read-only / untouched
107
- * fields. Read a field's live value as `draft[name] ?? data[name]`.
138
+ * fields. Read a field's live value from the staged record
139
+ * `{ ...data, ...draft }`, where `data` is the saved record: an OWN draft
140
+ * key wins even when its value is empty (`null`, `undefined`, `''`), and
141
+ * `data[name]` is read only when the key is absent. An own key with an
142
+ * empty value is a field the user, or a cascade clear, emptied, not an
143
+ * untouched one. A read that falls back to the saved value there shows the
144
+ * value the user just removed, and hands an option widget that pruned it
145
+ * the same value to prune again on every render (objectui#7190,
146
+ * objectui#10466).
108
147
  */
109
148
  draft: Record<string, any>;
110
149
  /** Field to auto-focus when edit was entered from a specific field. */
@@ -113,6 +152,34 @@ export interface InlineEditContextValue {
113
152
  saving: boolean;
114
153
  /** Last save error message, or null (driven by the save bar). */
115
154
  error: string | null;
155
+ /**
156
+ * Per-field reasons from the last rejected save, keyed by field MACHINE NAME
157
+ * — or `null` when the refusal was not field-scoped (objectui#6868).
158
+ *
159
+ * The record-level companion to `error`, and driven by the same component:
160
+ * `<InlineEditSaveBar>` reads the server's `VALIDATION_FAILED` envelope
161
+ * through `extractFieldErrors` and publishes the result here, so the field
162
+ * renderers (`DetailSection`, `HeaderHighlight`) can draw the reason beside
163
+ * the input the server actually refused. Before this slot existed the save
164
+ * bar and the field rows had no channel between them — they are SIBLINGS
165
+ * under this provider in both persistence modes — so an attributed refusal
166
+ * could only be shown record-level, which is not the in-place hint the
167
+ * objectui#6868 ruling asked for.
168
+ *
169
+ * ⚠️ PRESENTATION ONLY. These are the SERVER's verdicts, transported. The
170
+ * ruling makes the server the validation authority on the inline-edit
171
+ * surface: nothing may populate this slot from a client-side rule check, and
172
+ * nothing may read it as an authorization or acceptance decision. It is text
173
+ * to render beside an input, nothing more.
174
+ *
175
+ * Keyed by machine name because that is what both ends already use — the
176
+ * draft key the field renderers set, and the `field` the server names — so
177
+ * no spelling translation sits between the refusal and the input.
178
+ *
179
+ * Cleared by `enter()` and by teardown, exactly like `error`, so an
180
+ * attribution can never outlive the session that produced it.
181
+ */
182
+ fieldErrors: Record<string, string> | null;
116
183
  /** Enter inline-edit mode, optionally focused on `field`. No-op when `!canEdit`. */
117
184
  enter: (field?: string) => void;
118
185
  /** Stage a single field edit into the draft. */
@@ -125,6 +192,11 @@ export interface InlineEditContextValue {
125
192
  setSaving: (saving: boolean) => void;
126
193
  /** Set or clear the save error message (used by the save bar). */
127
194
  setError: (error: string | null) => void;
195
+ /**
196
+ * Publish or clear the per-field reasons for a rejected save (used by the
197
+ * save bar). Pass `null` when the refusal was not field-scoped.
198
+ */
199
+ setFieldErrors: (errors: Record<string, string> | null) => void;
128
200
  }
129
201
  export interface InlineEditProviderProps {
130
202
  /**
@@ -152,6 +224,14 @@ export interface InlineEditProviderProps {
152
224
  * for `first_response` nodes and for hosts that don't read approvals.
153
225
  */
154
226
  approvalProgress?: ApprovalProgress;
227
+ /**
228
+ * Whether the viewer submitted the pending approval request
229
+ * (`830ed5803`). Surfaced verbatim so the recall affordance is offered to
230
+ * the submitter only. Omitted ⇒ `undefined` ⇒ recall is offered exactly as
231
+ * it was before this prop existed (see `InlineEditContextValue`), so a host
232
+ * that resolves no approval identity is unchanged.
233
+ */
234
+ approvalIsSubmitter?: boolean;
155
235
  /** Optional human-readable lock reason, surfaced as the band tooltip. */
156
236
  lockedReason?: string;
157
237
  children: React.ReactNode;
@@ -24,7 +24,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
24
24
  */
25
25
  import React from 'react';
26
26
  const InlineEditContext = React.createContext(null);
27
- export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPending, approvalProgress, lockedReason, children, }) => {
27
+ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPending, approvalProgress, approvalIsSubmitter, lockedReason, children, }) => {
28
28
  // A locked record always has an approval running; the converse is what
29
29
  // #2902 added. Defaulting this way means an un-migrated host keeps its
30
30
  // previous behavior with no code change.
@@ -34,6 +34,7 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPen
34
34
  const [autoFocusField, setAutoFocusField] = React.useState(null);
35
35
  const [saving, setSaving] = React.useState(false);
36
36
  const [error, setError] = React.useState(null);
37
+ const [fieldErrors, setFieldErrors] = React.useState(null);
37
38
  const enter = React.useCallback((field) => {
38
39
  // Object-lifecycle / permission gate: never enter edit on a record the
39
40
  // host has marked non-editable, even if a stray affordance fires.
@@ -42,6 +43,7 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPen
42
43
  setAutoFocusField(field ?? null);
43
44
  setEditing(true);
44
45
  setError(null);
46
+ setFieldErrors(null);
45
47
  }, [canEdit]);
46
48
  const setField = React.useCallback((field, value) => {
47
49
  setDraft((prev) => ({ ...prev, [field]: value }));
@@ -56,6 +58,7 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPen
56
58
  setAutoFocusField(null);
57
59
  setSaving(false);
58
60
  setError(null);
61
+ setFieldErrors(null);
59
62
  }, []);
60
63
  const value = React.useMemo(() => ({
61
64
  editing,
@@ -63,18 +66,21 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPen
63
66
  locked,
64
67
  approvalPending: pending,
65
68
  approvalProgress,
69
+ approvalIsSubmitter,
66
70
  lockedReason,
67
71
  draft,
68
72
  autoFocusField,
69
73
  saving,
70
74
  error,
75
+ fieldErrors,
71
76
  enter,
72
77
  setField,
73
78
  cancel: teardown,
74
79
  reset: teardown,
75
80
  setSaving,
76
81
  setError,
77
- }), [editing, canEdit, locked, pending, approvalProgress, lockedReason, draft, autoFocusField, saving, error, enter, setField, teardown]);
82
+ setFieldErrors,
83
+ }), [editing, canEdit, locked, pending, approvalProgress, approvalIsSubmitter, lockedReason, draft, autoFocusField, saving, error, fieldErrors, enter, setField, teardown]);
78
84
  return _jsx(InlineEditContext.Provider, { value: value, children: children });
79
85
  };
80
86
  /**
@@ -13,6 +13,7 @@
13
13
  * receive props from the schema tree.
14
14
  */
15
15
  import React from 'react';
16
+ import type { DataSource } from '@object-ui/types';
16
17
  export declare const HighlightFieldsProvider: React.FC<{
17
18
  children: React.ReactNode;
18
19
  }>;
@@ -27,10 +28,42 @@ export declare function useRegisterHighlightFields(instanceId: string, names: st
27
28
  export interface RecordContextValue<TData = any, TObjectSchema = any> {
28
29
  /** Object machine name, e.g. "crm_opportunity". */
29
30
  objectName: string;
30
- /** Primary key value of the record being displayed. */
31
- recordId: string | number | null | undefined;
32
- /** Optional datasource id; mirrors the page-level datasource override. */
33
- dataSource?: string;
31
+ /**
32
+ * Primary key value of the record being displayed, as a `string` -- the one
33
+ * spelling `@objectstack/spec` declares on every record door (get, update,
34
+ * delete and the batch operation are all `z.string()`), so this consumer
35
+ * type is no longer wider than the protocol (objectui#9333).
36
+ *
37
+ * A host whose primary keys are numeric does NOT stringify at its call
38
+ * sites: `RecordContextProviderProps` still accepts `string | number`, and
39
+ * the provider pays the conversion once, below.
40
+ *
41
+ * `LineItemsPanel` reads this member as a `string` and hands it to a
42
+ * `string` parameter with no assertion in between, which is what
43
+ * objectui#9304 had to leave behind. That is a statement about that one
44
+ * reader, not about every reader: three `record:*` renderers in
45
+ * `@object-ui/plugin-detail` -- `record-details`, `record-quick-actions` and
46
+ * `record-alert` -- still read it through an `as any`. Those casts are
47
+ * redundant now rather than load-bearing, and removing them was left outside
48
+ * objectui#9333 deliberately.
49
+ */
50
+ recordId: string | null | undefined;
51
+ /**
52
+ * The data adapter the page is bound to, as the host resolved it — the same
53
+ * object the host hands `SchemaRendererProvider`, forwarded so that
54
+ * `record:*` renderers can self-fetch (related lists, activity, the
55
+ * reference rail) without re-plumbing it through the schema tree.
56
+ *
57
+ * Typed `DataSource`, not an id: every producer has always passed the
58
+ * adapter, and every reader has always called adapter methods on it
59
+ * (objectui#9197). `getObjectSchema` is a REQUIRED member of `DataSource`,
60
+ * so a reader gets the compiler's answer about it instead of a cast.
61
+ *
62
+ * Optional: hosts that render a record with no adapter bound (the Studio
63
+ * designer and palette previews) leave it undefined, and renderers treat
64
+ * that as "cannot self-fetch" rather than throwing.
65
+ */
66
+ dataSource?: DataSource;
34
67
  /** Loaded record data (flat record map). Undefined while loading. */
35
68
  data?: TData;
36
69
  /** Resolved object metadata schema (fields, label, etc.). */
@@ -68,7 +101,17 @@ export interface RecordContextValue<TData = any, TObjectSchema = any> {
68
101
  */
69
102
  embedded?: boolean;
70
103
  }
71
- export interface RecordContextProviderProps extends RecordContextValue {
104
+ /**
105
+ * Props of `RecordContextProvider` -- THE injection boundary (objectui#9333).
106
+ *
107
+ * Identical to `RecordContextValue` except for `recordId`, the one member a
108
+ * host may still hand over in the wider shape its backend actually holds. The
109
+ * provider narrows it to the protocol's `string` exactly once, here, so no
110
+ * consumer downstream carries a `String(...)` or a cast.
111
+ */
112
+ export interface RecordContextProviderProps extends Omit<RecordContextValue, 'recordId'> {
113
+ /** Primary key as the host holds it; narrowed to `string` on the way in. */
114
+ recordId: string | number | null | undefined;
72
115
  children: React.ReactNode;
73
116
  }
74
117
  export declare const RecordContextProvider: React.FC<RecordContextProviderProps>;
@@ -87,7 +87,16 @@ const NOOP_SUBSCRIBE = (_) => () => { };
87
87
  const GET_EMPTY = () => EMPTY_SET;
88
88
  const EMPTY_SET = new Set();
89
89
  const RecordContext = React.createContext(null);
90
- export const RecordContextProvider = ({ children, ...value }) => {
90
+ export const RecordContextProvider = ({ children, recordId, ...rest }) => {
91
+ // The whole conversion for this contract, in one typed place. Gated on
92
+ // `typeof` rather than written as an unconditional `String(...)`: `null` and
93
+ // `undefined` mean "no record bound", and stringifying them would hand
94
+ // consumers the literal ids "null" / "undefined". A falsiness gate would be
95
+ // wrong in the other direction -- a numeric `0` is a real primary key.
96
+ const value = {
97
+ ...rest,
98
+ recordId: typeof recordId === 'number' ? String(recordId) : recordId,
99
+ };
91
100
  // Memoize so consumers that rely on referential equality don't re-render
92
101
  // on unrelated parent renders.
93
102
  const memo = React.useMemo(() => value, [
@@ -83,7 +83,11 @@ export interface RelatedRecordHandlers {
83
83
  * the deletion resolves.
84
84
  */
85
85
  onDelete?: (recordId: string | number, record?: unknown) => void | Promise<void>;
86
- /** Child object row-level actions (`locations: ['list_item']`), localized. */
86
+ /**
87
+ * Child object row-level actions, localized: its `list_item` actions, and
88
+ * its `record_related` ones when the list sits inside a parent record
89
+ * (objectui#11270).
90
+ */
87
91
  rowActions?: RelatedRowActionDef[];
88
92
  /** Execute one of {@link rowActions} against a specific child row. */
89
93
  onRowAction?: (action: RelatedRowActionDef, record: unknown) => void | Promise<void>;
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { DebugFlags } from '@object-ui/core';
3
+ import type { DataSource } from '@object-ui/types';
3
4
  /**
4
5
  * Host-provided fetch used for `provider: 'api'` view data sources so custom
5
6
  * endpoints carry the same credentials (Authorization, tenant, locale headers)
@@ -8,7 +9,29 @@ import type { DebugFlags } from '@object-ui/core';
8
9
  */
9
10
  export type ApiFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
10
11
  interface SchemaRendererContextType {
11
- dataSource: any;
12
+ /**
13
+ * The adapter the host injected, as the published `DataSource` contract
14
+ * declares it (objectui#7912).
15
+ *
16
+ * Typed, not `any`: this is the injection point for the ENTIRE renderer tree,
17
+ * and `useSchemaContext()` hands whatever is declared here to every consumer
18
+ * that reads it back. While it was `any`, a bare string passed where the
19
+ * host's adapter belongs raised nothing at compile time and failed at runtime
20
+ * on the first `find()` — the shape of a config value read from the wrong
21
+ * place. The type was never unknown: `@object-ui/types` exports `DataSource`,
22
+ * `useSettledSchema` in this same package already declares its parameter as
23
+ * `DataSource<any> | null | undefined`, and `app-shell`'s README writes
24
+ * `DataSource` against this very seam.
25
+ *
26
+ * `null | undefined` are part of the contract, not a weakening of it: a host
27
+ * renders with no adapter bound (a Studio preview, a react page before the
28
+ * AdapterProvider connects, a widget test that drives `apiFetch` alone), and
29
+ * every reader in the tree already guards for it. The union states the three
30
+ * states that actually occur and refuses everything else — a string, `{}`, a
31
+ * plain data bag, a partial adapter missing a required member. It is spelled
32
+ * exactly as `useSettledSchema` spells it, one hop away.
33
+ */
34
+ dataSource: DataSource | null | undefined;
12
35
  debug?: boolean;
13
36
  debugFlags?: DebugFlags;
14
37
  apiFetch?: ApiFetch;
@@ -17,10 +40,39 @@ declare const SchemaRendererContext: React.Context<SchemaRendererContextType | n
17
40
  export { SchemaRendererContext };
18
41
  export declare const SchemaRendererProvider: ({ children, dataSource, debug, debugFlags, apiFetch, }: {
19
42
  children: React.ReactNode;
20
- dataSource: any;
43
+ /** The host's adapter — see {@link SchemaRendererContextType.dataSource}. */
44
+ dataSource: DataSource | null | undefined;
21
45
  debug?: boolean;
22
46
  debugFlags?: DebugFlags;
23
47
  apiFetch?: ApiFetch;
24
48
  }) => React.JSX.Element;
25
49
  export declare const useSchemaContext: () => SchemaRendererContextType;
50
+ /**
51
+ * Resolve a `bind` path against the ambient predicate scope.
52
+ *
53
+ * ## What it reads, and what it deliberately does not (objectui#9308)
54
+ *
55
+ * The scope a host publishes through `PredicateScopeProvider` — the same
56
+ * channel `useCondition` / `useExpression` merge under a locally-passed
57
+ * context, and the one app-shell's `ExpressionProvider` already feeds. It is
58
+ * NOT `SchemaRendererContext.dataSource`.
59
+ *
60
+ * `dataSource` is the host's injected ADAPTER, declared as the published
61
+ * `DataSource` contract (objectui#7912). An adapter has no `users` member, no
62
+ * `value` member, no member a `bind` path names — so walking it resolved
63
+ * `undefined` for every conformant host, and the nine production readers of
64
+ * this hook have all been running their fallback (`boundData || schema.items`,
65
+ * `|| schema.nodes`, or a fall-through to their own fetch chain) ever since.
66
+ * The only hosts it answered were ones injecting a data bag through a key the
67
+ * contract says is an adapter, which has been a compile error since
68
+ * objectui#7912.
69
+ *
70
+ * Maintainer ruling 2026-09-13 (option B) points it at the channel that can
71
+ * actually answer it. No published key is added: the provider, the hook and
72
+ * the host wiring all already exist.
73
+ *
74
+ * Returns `undefined` for an absent or empty path, and for any path the scope
75
+ * does not carry — the readers' fallbacks are what run then, exactly as
76
+ * before.
77
+ */
26
78
  export declare const useDataScope: (path?: string) => any;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useMemo } from 'react';
3
+ import { usePredicateScope } from '../hooks/useExpression.js';
3
4
  const SchemaRendererContext = createContext(null);
4
5
  export { SchemaRendererContext };
5
6
  export const SchemaRendererProvider = ({ children, dataSource, debug, debugFlags, apiFetch, }) => {
@@ -18,13 +19,45 @@ export const useSchemaContext = () => {
18
19
  }
19
20
  return context;
20
21
  };
22
+ /**
23
+ * Resolve a `bind` path against the ambient predicate scope.
24
+ *
25
+ * ## What it reads, and what it deliberately does not (objectui#9308)
26
+ *
27
+ * The scope a host publishes through `PredicateScopeProvider` — the same
28
+ * channel `useCondition` / `useExpression` merge under a locally-passed
29
+ * context, and the one app-shell's `ExpressionProvider` already feeds. It is
30
+ * NOT `SchemaRendererContext.dataSource`.
31
+ *
32
+ * `dataSource` is the host's injected ADAPTER, declared as the published
33
+ * `DataSource` contract (objectui#7912). An adapter has no `users` member, no
34
+ * `value` member, no member a `bind` path names — so walking it resolved
35
+ * `undefined` for every conformant host, and the nine production readers of
36
+ * this hook have all been running their fallback (`boundData || schema.items`,
37
+ * `|| schema.nodes`, or a fall-through to their own fetch chain) ever since.
38
+ * The only hosts it answered were ones injecting a data bag through a key the
39
+ * contract says is an adapter, which has been a compile error since
40
+ * objectui#7912.
41
+ *
42
+ * Maintainer ruling 2026-09-13 (option B) points it at the channel that can
43
+ * actually answer it. No published key is added: the provider, the hook and
44
+ * the host wiring all already exist.
45
+ *
46
+ * Returns `undefined` for an absent or empty path, and for any path the scope
47
+ * does not carry — the readers' fallbacks are what run then, exactly as
48
+ * before.
49
+ */
21
50
  export const useDataScope = (path) => {
22
- const context = useContext(SchemaRendererContext);
23
- const dataSource = context?.dataSource;
51
+ const scope = usePredicateScope();
24
52
  if (!path)
25
53
  return undefined;
26
- if (!dataSource)
54
+ if (!scope)
27
55
  return undefined;
28
- // Simple path resolution for now. In real app might be more complex
29
- return path.split('.').reduce((acc, part) => acc && acc[part], dataSource);
56
+ // Simple path resolution for now. In real app might be more complex.
57
+ //
58
+ // The accumulator is `any` BY DECLARATION: this walk addresses arbitrary
59
+ // member names on a host-published bag, and `Record<string, any>` declares
60
+ // none of them beyond the first segment. The hook's published return type is
61
+ // unchanged — `any` before and after — so no reader of `useDataScope` moves.
62
+ return path.split('.').reduce((acc, part) => acc && acc[part], scope);
30
63
  };
@@ -131,7 +131,7 @@ export function ThemeProvider({ themes: themesInput = [], defaultTheme, defaultM
131
131
  // Apply mode class
132
132
  el.classList.remove('light', 'dark');
133
133
  el.classList.add(resolvedMode);
134
- // No favicon here: spec v17 (#3494) pruned `Theme.logo`, so the key is
134
+ // No favicon here: spec v17 (objectstack-ai/objectstack#3494) pruned `Theme.logo`, so the key is
135
135
  // stripped at parse and could never arrive. The live favicon comes from
136
136
  // operator branding (`getFaviconUrl()`), applied in the console's
137
137
  // index.html, main.tsx, and on route change.
@@ -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
+ /** What `ActionRunner.setTranslator` takes. */
9
+ type ActionRunnerTranslate = (key: string, options: {
10
+ defaultValue: string;
11
+ }) => string;
12
+ /**
13
+ * The translator both React owners of an `ActionRunner` — `<ActionProvider>`
14
+ * and `useActionRunner` — install on the runner they build, so the text the
15
+ * runner writes itself (the generic success toast an action with no
16
+ * `successMessage` ends in) renders in the session's language instead of
17
+ * English. Internal to this package; not re-exported from its entry.
18
+ *
19
+ * The function is created once per mount and reads the latest `t` when it is
20
+ * CALLED, so a language switch reaches a runner that was built before it. Both
21
+ * owners rebuild their runner only when their `context` changes, and a
22
+ * translator captured at build time would keep answering in the language that
23
+ * was active then. The identity comes from `useState`'s initializer, not from a
24
+ * memo, so nothing depends on a `useMemo` result staying put (AGENTS.md #10).
25
+ */
26
+ export declare function useActionRunnerTranslator(): ActionRunnerTranslate;
27
+ export {};
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import { useEffect, useRef, useState } from 'react';
9
+ import { useObjectTranslation } from '@object-ui/i18n';
10
+ /**
11
+ * The translator both React owners of an `ActionRunner` — `<ActionProvider>`
12
+ * and `useActionRunner` — install on the runner they build, so the text the
13
+ * runner writes itself (the generic success toast an action with no
14
+ * `successMessage` ends in) renders in the session's language instead of
15
+ * English. Internal to this package; not re-exported from its entry.
16
+ *
17
+ * The function is created once per mount and reads the latest `t` when it is
18
+ * CALLED, so a language switch reaches a runner that was built before it. Both
19
+ * owners rebuild their runner only when their `context` changes, and a
20
+ * translator captured at build time would keep answering in the language that
21
+ * was active then. The identity comes from `useState`'s initializer, not from a
22
+ * memo, so nothing depends on a `useMemo` result staying put (AGENTS.md #10).
23
+ */
24
+ export function useActionRunnerTranslator() {
25
+ const { t } = useObjectTranslation();
26
+ const latest = useRef(t);
27
+ // No dependency list: after every render, the ref holds that render's `t`.
28
+ // Actions run from user events, which arrive after the commit.
29
+ useEffect(() => {
30
+ latest.current = t;
31
+ });
32
+ const [translate] = useState(() => {
33
+ const read = (key, options) => String(latest.current(key, options));
34
+ return read;
35
+ });
36
+ return translate;
37
+ }