@object-ui/react 17.5.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +4100 -0
  2. package/README.md +303 -46
  3. package/dist/LazyPluginLoader.d.ts +14 -7
  4. package/dist/LazyPluginLoader.js +14 -7
  5. package/dist/SchemaRenderer.d.ts +38 -7
  6. package/dist/SchemaRenderer.js +1603 -60
  7. package/dist/context/ActionContext.js +40 -1
  8. package/dist/context/DiscussionContext.d.ts +12 -0
  9. package/dist/context/DiscussionContext.js +2 -0
  10. package/dist/context/DrillNavigationContext.d.ts +13 -0
  11. package/dist/context/HostNavigationContext.d.ts +82 -0
  12. package/dist/context/HostNavigationContext.js +65 -0
  13. package/dist/context/InlineEditContext.d.ts +81 -1
  14. package/dist/context/InlineEditContext.js +8 -2
  15. package/dist/context/RecordContext.d.ts +48 -5
  16. package/dist/context/RecordContext.js +10 -1
  17. package/dist/context/RelatedRecordActionsContext.d.ts +20 -1
  18. package/dist/context/SchemaRendererContext.d.ts +54 -2
  19. package/dist/context/SchemaRendererContext.js +38 -5
  20. package/dist/context/ThemeContext.js +1 -1
  21. package/dist/context/actionRunnerTranslator.d.ts +27 -0
  22. package/dist/context/actionRunnerTranslator.js +37 -0
  23. package/dist/context/index.d.ts +13 -12
  24. package/dist/context/index.js +13 -12
  25. package/dist/element-data-source/ElementDataSourceGate.d.ts +163 -3
  26. package/dist/element-data-source/ElementDataSourceGate.js +407 -15
  27. package/dist/hooks/index.d.ts +37 -35
  28. package/dist/hooks/index.js +44 -35
  29. package/dist/hooks/useActionEngine.js +47 -4
  30. package/dist/hooks/useActionRunner.js +4 -0
  31. package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
  32. package/dist/hooks/useActionTextLocalizer.js +33 -8
  33. package/dist/hooks/useCapabilityGate.js +6 -5
  34. package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
  35. package/dist/hooks/useConfigBagEvaluator.js +39 -0
  36. package/dist/hooks/useCrudShortcuts.js +1 -1
  37. package/dist/hooks/useDatasetDimensionLabels.js +1 -1
  38. package/dist/hooks/useDensityMode.d.ts +6 -2
  39. package/dist/hooks/useDensityMode.js +6 -2
  40. package/dist/hooks/useDiscovery.d.ts +1 -10
  41. package/dist/hooks/useDiscovery.js +1 -1
  42. package/dist/hooks/useETagCache.js +25 -4
  43. package/dist/hooks/useElementDataSource.d.ts +15 -2
  44. package/dist/hooks/useElementDataSource.js +29 -9
  45. package/dist/hooks/useExpression.d.ts +52 -38
  46. package/dist/hooks/useExpression.js +62 -42
  47. package/dist/hooks/useFilterScope.d.ts +22 -2
  48. package/dist/hooks/useFilterScope.js +29 -4
  49. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  50. package/dist/hooks/useGlobalUndo.js +112 -46
  51. package/dist/hooks/useNavigationOverlay.d.ts +77 -12
  52. package/dist/hooks/useNavigationOverlay.js +119 -9
  53. package/dist/hooks/useOffline.js +65 -11
  54. package/dist/hooks/usePageAssignment.d.ts +8 -5
  55. package/dist/hooks/usePageAssignment.js +38 -17
  56. package/dist/hooks/usePageTransition.js +1 -1
  57. package/dist/hooks/usePageVariableActionBridge.js +2 -2
  58. package/dist/hooks/useRecordSearch.d.ts +34 -1
  59. package/dist/hooks/useRecordSearch.js +42 -7
  60. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  61. package/dist/hooks/useResolvedFilter.js +130 -0
  62. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  63. package/dist/hooks/useSchemaPersistence.js +144 -5
  64. package/dist/hooks/useSettledSchema.d.ts +123 -0
  65. package/dist/hooks/useSettledSchema.js +186 -0
  66. package/dist/hooks/useViewData.d.ts +7 -2
  67. package/dist/hooks/useViewData.js +1 -1
  68. package/dist/hooks/useViewTransition.js +1 -1
  69. package/dist/index.d.ts +18 -11
  70. package/dist/index.js +54 -10
  71. package/dist/schema-input.d.ts +55 -14
  72. package/dist/schema-input.js +55 -12
  73. package/dist/utils/configBag.d.ts +68 -0
  74. package/dist/utils/configBag.js +70 -0
  75. package/dist/utils/error-message.d.ts +59 -0
  76. package/dist/utils/error-message.js +116 -1
  77. package/dist/utils/inlineAria.d.ts +61 -0
  78. package/dist/utils/inlineAria.js +70 -0
  79. package/dist/utils/nonGridRowCeiling.d.ts +44 -0
  80. package/dist/utils/nonGridRowCeiling.js +78 -0
  81. package/dist/utils/paramsBag.d.ts +28 -0
  82. package/dist/utils/paramsBag.js +146 -0
  83. package/dist/utils/propsBagDiagnostic.d.ts +196 -0
  84. package/dist/utils/propsBagDiagnostic.js +421 -0
  85. package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
  86. package/dist/utils/refusedDataPropDiagnostic.js +94 -0
  87. package/dist/utils/unevaluatedExpression.d.ts +110 -0
  88. package/dist/utils/unevaluatedExpression.js +257 -0
  89. package/dist/utils/visibilityDiagnostic.d.ts +371 -0
  90. package/dist/utils/visibilityDiagnostic.js +551 -0
  91. package/package.json +10 -10
  92. package/dist/hooks/useClientNotifications.d.ts +0 -47
  93. package/dist/hooks/useClientNotifications.js +0 -162
  94. package/dist/spec-bridge/SpecBridge.d.ts +0 -28
  95. package/dist/spec-bridge/SpecBridge.js +0 -56
  96. package/dist/spec-bridge/bridges/form-view.d.ts +0 -82
  97. package/dist/spec-bridge/bridges/form-view.js +0 -126
  98. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  99. package/dist/spec-bridge/bridges/list-view.js +0 -241
  100. package/dist/spec-bridge/index.d.ts +0 -11
  101. package/dist/spec-bridge/index.js +0 -10
  102. package/dist/spec-bridge/types.d.ts +0 -55
  103. 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
  }
@@ -0,0 +1,82 @@
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
+ * HostNavigationContext — the host's own in-app navigate, offered to renderers.
10
+ *
11
+ * A published renderer package cannot navigate correctly on its own. Its only
12
+ * dependency-free option is a browser-level navigation
13
+ * (`window.location.assign`), which resolves a rooted path such as `/thanks`
14
+ * against the ORIGIN. Under a host mounted at a sub-path — the framework CLI
15
+ * configures one for every embedded deployment — that leaves the application
16
+ * entirely. This is objectui#4989 defect 4, and the same class as
17
+ * objectui#4190 / #4181.
18
+ *
19
+ * The host is the only party that knows its mount, so the host supplies the
20
+ * navigate. This context is that seam: **optional injection**, ruled by the
21
+ * maintainer on 2026-08-17. Two mechanisms were rejected in that ruling and are
22
+ * named here so they are not re-proposed:
23
+ *
24
+ * - **router-presence sniffing** (a renderer reading React Router's context
25
+ * when it happens to be there) — implicit magic, and it forces every
26
+ * renderer that wants mount-awareness to import `react-router`;
27
+ * - **a `react-router` peer requirement** — it imposes module resolution on
28
+ * router-less consumers. Two real ones exist in this repo (`apps/site`,
29
+ * `packages/plugin-view`), and a published renderer's whole property is
30
+ * that it drops into ANY React application.
31
+ *
32
+ * Explicit injection keeps every renderer dependency-free and puts
33
+ * mount-awareness on the type face where a host can see it is missing.
34
+ *
35
+ * ## Absent is a supported state, not a degraded one
36
+ *
37
+ * The default value is an empty object, so `useHostNavigation()` outside a
38
+ * provider answers `{ navigate: undefined }` and never throws. A consumer reads
39
+ * it as "no host navigate — fall back to whatever this renderer did before",
40
+ * which for a router-less host is already the correct behaviour: with no
41
+ * router there is no basename, so origin-rooted resolution is right. The seam
42
+ * changes what a MOUNTED host gets, and changes nothing else.
43
+ *
44
+ * ## Relationship to the other navigation seams in this package
45
+ *
46
+ * `DrillNavigationContext.openRecordList` is a SEMANTIC navigation — "show me
47
+ * the records behind this aggregate" — that only the console can turn into a
48
+ * URL. `ActionProvider`'s `onNavigate` is the ActionRunner's handler for
49
+ * `type: 'navigate'` actions, reachable only through an action dispatch. This
50
+ * one is neither: it is the plain "go to this already-resolved in-app path"
51
+ * that a renderer holds a destination for and cannot perform correctly itself.
52
+ * All three are supplied by the same shell from the same router `navigate`.
53
+ */
54
+ import React from 'react';
55
+ export interface HostNavigationValue {
56
+ /**
57
+ * Navigate to an in-app path, resolved by the HOST — so a mounted host's
58
+ * basename is applied and the transition stays inside the application.
59
+ *
60
+ * `to` is an already-resolved, application-relative path (e.g. `/thanks`,
61
+ * `/apps/crm/accounts/r1`), never an absolute URL: a renderer that holds an
62
+ * external destination must not launder it through the host's router. It is
63
+ * the CALLER's job to have judged the destination — this seam performs a
64
+ * navigation, it does not authorize one.
65
+ *
66
+ * Absent when no host wired it; consumers fall back to their own default.
67
+ */
68
+ navigate?: (to: string, options?: {
69
+ replace?: boolean;
70
+ }) => void;
71
+ }
72
+ export declare const HostNavigationContext: React.Context<HostNavigationValue>;
73
+ export declare const HostNavigationProvider: React.FC<{
74
+ value: HostNavigationValue;
75
+ children: React.ReactNode;
76
+ }>;
77
+ /**
78
+ * Consume the host-provided in-app navigate. Answers `{}` (i.e. `navigate`
79
+ * undefined) when no host wired one — that is a supported state, see the module
80
+ * docblock.
81
+ */
82
+ export declare function useHostNavigation(): HostNavigationValue;
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ /**
10
+ * HostNavigationContext — the host's own in-app navigate, offered to renderers.
11
+ *
12
+ * A published renderer package cannot navigate correctly on its own. Its only
13
+ * dependency-free option is a browser-level navigation
14
+ * (`window.location.assign`), which resolves a rooted path such as `/thanks`
15
+ * against the ORIGIN. Under a host mounted at a sub-path — the framework CLI
16
+ * configures one for every embedded deployment — that leaves the application
17
+ * entirely. This is objectui#4989 defect 4, and the same class as
18
+ * objectui#4190 / #4181.
19
+ *
20
+ * The host is the only party that knows its mount, so the host supplies the
21
+ * navigate. This context is that seam: **optional injection**, ruled by the
22
+ * maintainer on 2026-08-17. Two mechanisms were rejected in that ruling and are
23
+ * named here so they are not re-proposed:
24
+ *
25
+ * - **router-presence sniffing** (a renderer reading React Router's context
26
+ * when it happens to be there) — implicit magic, and it forces every
27
+ * renderer that wants mount-awareness to import `react-router`;
28
+ * - **a `react-router` peer requirement** — it imposes module resolution on
29
+ * router-less consumers. Two real ones exist in this repo (`apps/site`,
30
+ * `packages/plugin-view`), and a published renderer's whole property is
31
+ * that it drops into ANY React application.
32
+ *
33
+ * Explicit injection keeps every renderer dependency-free and puts
34
+ * mount-awareness on the type face where a host can see it is missing.
35
+ *
36
+ * ## Absent is a supported state, not a degraded one
37
+ *
38
+ * The default value is an empty object, so `useHostNavigation()` outside a
39
+ * provider answers `{ navigate: undefined }` and never throws. A consumer reads
40
+ * it as "no host navigate — fall back to whatever this renderer did before",
41
+ * which for a router-less host is already the correct behaviour: with no
42
+ * router there is no basename, so origin-rooted resolution is right. The seam
43
+ * changes what a MOUNTED host gets, and changes nothing else.
44
+ *
45
+ * ## Relationship to the other navigation seams in this package
46
+ *
47
+ * `DrillNavigationContext.openRecordList` is a SEMANTIC navigation — "show me
48
+ * the records behind this aggregate" — that only the console can turn into a
49
+ * URL. `ActionProvider`'s `onNavigate` is the ActionRunner's handler for
50
+ * `type: 'navigate'` actions, reachable only through an action dispatch. This
51
+ * one is neither: it is the plain "go to this already-resolved in-app path"
52
+ * that a renderer holds a destination for and cannot perform correctly itself.
53
+ * All three are supplied by the same shell from the same router `navigate`.
54
+ */
55
+ import { createContext, useContext } from 'react';
56
+ export const HostNavigationContext = createContext({});
57
+ export const HostNavigationProvider = ({ value, children }) => (_jsx(HostNavigationContext.Provider, { value: value, children: children }));
58
+ /**
59
+ * Consume the host-provided in-app navigate. Answers `{}` (i.e. `navigate`
60
+ * undefined) when no host wired one — that is a supported state, see the module
61
+ * docblock.
62
+ */
63
+ export function useHostNavigation() {
64
+ return useContext(HostNavigationContext);
65
+ }
@@ -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, [
@@ -61,6 +61,21 @@ export interface RelatedRecordHandlers {
61
61
  * argument is needed here (增).
62
62
  */
63
63
  onCreate?: () => void;
64
+ /**
65
+ * [#4646] Offer "+ New", but GREYED — the `userActions.create.disabledWhen`
66
+ * verdict for the scope this toolbar sits in (the host parent record on a
67
+ * record page's related list).
68
+ *
69
+ * A separate channel from omitting {@link onCreate} because hidden and
70
+ * disabled are different answers, exactly as they are for the row Edit/Delete
71
+ * predicates (objectui#2614) and for the record header's (objectui#4419 /
72
+ * PR #4515): `visibleWhen` decides whether the affordance exists at all,
73
+ * `disabledWhen` decides whether the existing affordance can be pressed. The
74
+ * host still supplies `onCreate` in the disabled case, so the button keeps
75
+ * its label and tooltip and the user can see that creating is a thing this
76
+ * list does — just not right now.
77
+ */
78
+ createDisabled?: boolean;
64
79
  /** Open an edit form for an existing child record (改). */
65
80
  onEdit?: (recordId: string | number, record?: unknown) => void;
66
81
  /**
@@ -68,7 +83,11 @@ export interface RelatedRecordHandlers {
68
83
  * the deletion resolves.
69
84
  */
70
85
  onDelete?: (recordId: string | number, record?: unknown) => void | Promise<void>;
71
- /** 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
+ */
72
91
  rowActions?: RelatedRowActionDef[];
73
92
  /** Execute one of {@link rowActions} against a specific child row. */
74
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;