@object-ui/react 17.6.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +4263 -0
  2. package/README.md +383 -42
  3. package/dist/SchemaRenderer.d.ts +64 -7
  4. package/dist/SchemaRenderer.js +1326 -106
  5. package/dist/context/ActionContext.js +40 -1
  6. package/dist/context/AppShellContext.d.ts +27 -0
  7. package/dist/context/DiscussionContext.d.ts +27 -0
  8. package/dist/context/DiscussionContext.js +4 -0
  9. package/dist/context/DrillNavigationContext.d.ts +13 -0
  10. package/dist/context/InlineEditContext.d.ts +151 -1
  11. package/dist/context/InlineEditContext.js +36 -2
  12. package/dist/context/PrintPageContext.d.ts +60 -0
  13. package/dist/context/PrintPageContext.js +32 -0
  14. package/dist/context/RecordContext.d.ts +48 -5
  15. package/dist/context/RecordContext.js +10 -1
  16. package/dist/context/RelatedRecordActionsContext.d.ts +72 -2
  17. package/dist/context/RelatedRecordActionsContext.js +4 -0
  18. package/dist/context/SchemaRendererContext.d.ts +61 -6
  19. package/dist/context/SchemaRendererContext.js +39 -6
  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 +1 -0
  24. package/dist/context/index.js +1 -0
  25. package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
  26. package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
  27. package/dist/element-data-source/ElementDataSourceGate.d.ts +118 -3
  28. package/dist/element-data-source/ElementDataSourceGate.js +401 -19
  29. package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
  30. package/dist/element-data-source/useElementApiDataSource.js +198 -0
  31. package/dist/hooks/index.d.ts +3 -1
  32. package/dist/hooks/index.js +10 -1
  33. package/dist/hooks/useActionEngine.js +46 -3
  34. package/dist/hooks/useActionRunner.js +4 -0
  35. package/dist/hooks/useActionTextLocalizer.d.ts +35 -9
  36. package/dist/hooks/useActionTextLocalizer.js +70 -14
  37. package/dist/hooks/useConfigBagEvaluator.d.ts +31 -0
  38. package/dist/hooks/useConfigBagEvaluator.js +40 -0
  39. package/dist/hooks/useDatasetDimensionLabels.js +25 -10
  40. package/dist/hooks/useDensityMode.d.ts +8 -2
  41. package/dist/hooks/useDensityMode.js +8 -2
  42. package/dist/hooks/useDiscovery.d.ts +1 -10
  43. package/dist/hooks/useETagCache.js +25 -4
  44. package/dist/hooks/useElementDataSource.d.ts +15 -2
  45. package/dist/hooks/useElementDataSource.js +24 -7
  46. package/dist/hooks/useExpression.d.ts +52 -59
  47. package/dist/hooks/useExpression.js +58 -90
  48. package/dist/hooks/useFilterScope.d.ts +22 -2
  49. package/dist/hooks/useFilterScope.js +29 -4
  50. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  51. package/dist/hooks/useGlobalUndo.js +112 -46
  52. package/dist/hooks/useNavigationOverlay.d.ts +76 -11
  53. package/dist/hooks/useNavigationOverlay.js +119 -9
  54. package/dist/hooks/useOffline.js +65 -11
  55. package/dist/hooks/usePageAssignment.d.ts +33 -7
  56. package/dist/hooks/usePageAssignment.js +85 -21
  57. package/dist/hooks/useRecordSearch.d.ts +34 -1
  58. package/dist/hooks/useRecordSearch.js +42 -7
  59. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  60. package/dist/hooks/useResolvedFilter.js +130 -0
  61. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  62. package/dist/hooks/useSchemaPersistence.js +144 -5
  63. package/dist/hooks/useSettledSchema.d.ts +123 -0
  64. package/dist/hooks/useSettledSchema.js +186 -0
  65. package/dist/hooks/useViewData.d.ts +10 -3
  66. package/dist/hooks/useViewData.js +5 -14
  67. package/dist/index.d.ts +9 -1
  68. package/dist/index.js +48 -1
  69. package/dist/schema-input.d.ts +38 -8
  70. package/dist/schema-input.js +39 -7
  71. package/dist/utils/configBag.d.ts +68 -0
  72. package/dist/utils/configBag.js +70 -0
  73. package/dist/utils/error-message.d.ts +1 -1
  74. package/dist/utils/error-message.js +1 -1
  75. package/dist/utils/inlineAria.d.ts +61 -0
  76. package/dist/utils/inlineAria.js +70 -0
  77. package/dist/utils/localeMap.d.ts +27 -0
  78. package/dist/utils/localeMap.js +106 -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 +422 -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 +44 -1
  88. package/dist/utils/unevaluatedExpression.js +92 -11
  89. package/dist/utils/visibilityDiagnostic.d.ts +282 -21
  90. package/dist/utils/visibilityDiagnostic.js +321 -46
  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 -158
  97. package/dist/spec-bridge/bridges/form-view.js +0 -170
  98. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  99. package/dist/spec-bridge/bridges/list-view.js +0 -268
  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);
@@ -45,6 +45,33 @@ export interface MetadataContextValue extends MetadataCacheState {
45
45
  * context values in tests keep working; absent means "always ready".
46
46
  */
47
47
  getTypeStatus?: (type: string) => MetadataTypeStatus;
48
+ /**
49
+ * The object definitions one app reads (objectui#12811): the objects of
50
+ * `packageId` (`GET /api/v1/meta/object?package=`), plus each of `names`
51
+ * that package does not hold, fetched by name. Each is shaped exactly as the
52
+ * same definition in `objects`; `served` holds the same definitions as the
53
+ * server served them, before views are merged (as `getItemsByType('object')`
54
+ * holds the whole list). `missing` lists the `names` the server answered
55
+ * without a definition; `settled` is true once the package list and every
56
+ * name have been answered.
57
+ *
58
+ * With `names` and no `packageId` it is a names-only scope (objectui#12980):
59
+ * no package list, each of `names` fetched by name, for a reader with no
60
+ * package to scope by (a page route in an app item without `_packageId`).
61
+ * With neither it is the whole registry, as `objects` is; `objects` keeps
62
+ * that meaning everywhere. Optional, as `getTypeStatus` is: a
63
+ * hand-rolled context without it has nothing narrower to offer, and its
64
+ * readers read `objects`.
65
+ */
66
+ readObjectScope?: (scope: {
67
+ packageId?: string;
68
+ names?: readonly string[];
69
+ }) => {
70
+ objects: any[];
71
+ served: any[];
72
+ missing: readonly string[];
73
+ settled: boolean;
74
+ };
48
75
  }
49
76
  export declare const MetadataCtx: import("react").Context<MetadataContextValue | null>;
50
77
  export declare function useMetadata(): MetadataContextValue;
@@ -40,6 +40,21 @@ export interface DiscussionContextValue {
40
40
  onAddComment?: (text: string, attachments?: DiscussionAttachment[]) => void | Promise<void>;
41
41
  onAddReply?: (parentId: string, text: string) => void | Promise<void>;
42
42
  onToggleReaction?: (itemId: string, reaction: string) => void | Promise<void>;
43
+ /**
44
+ * Edit the text of a comment the viewer wrote (objectui#12268). The feed
45
+ * offers it on a `comment` row whose `authoredByViewer` the host set. A
46
+ * returned promise that REJECTS means the edit was not stored: the editor
47
+ * keeps the draft and the row keeps its old text, and reporting why is the
48
+ * host's job. Typed by the feed's own item id (`FeedItem['id']`), so the
49
+ * feed hands it on without a cast.
50
+ */
51
+ onEditComment?: (itemId: string | number, text: string) => void | Promise<void>;
52
+ /**
53
+ * Delete a comment the viewer wrote (objectui#12268), offered on the same
54
+ * rows. The host asks for confirmation before it deletes; a rejection means
55
+ * nothing was deleted.
56
+ */
57
+ onDeleteComment?: (itemId: string | number) => void | Promise<void>;
43
58
  /** Optional list of users (or any mentionable entities) the comment
44
59
  * input can suggest after the user types `@`. When omitted the input
45
60
  * falls back to free-text @-mentions without autocompletion. */
@@ -50,6 +65,18 @@ export interface DiscussionContextValue {
50
65
  onUploadAttachments?: (files: FileList) => Promise<DiscussionAttachment[]>;
51
66
  loading?: boolean;
52
67
  error?: Error | null;
68
+ /**
69
+ * The host's `sys_activity` read was REFUSED: 401 / 403, or a permission
70
+ * envelope (objectui#11195). The feed renderers then show a no-permission
71
+ * state rather than claiming the record has no activity. Produced by the
72
+ * host that owns the fetch (app-shell `RecordDetailView`), which judges the
73
+ * rejection with plugin-detail's `isRefusedFeedRead`. A read that answered
74
+ * with zero rows, a 404, or any other failure is not a refusal and leaves
75
+ * this unset.
76
+ */
77
+ activityDenied?: boolean;
78
+ /** The same, for the host's `sys_comment` read. */
79
+ commentsDenied?: boolean;
53
80
  }
54
81
  export interface DiscussionContextProviderProps extends DiscussionContextValue {
55
82
  children: React.ReactNode;
@@ -24,10 +24,14 @@ export const DiscussionContextProvider = ({ children, ...value }) => {
24
24
  value.onAddComment,
25
25
  value.onAddReply,
26
26
  value.onToggleReaction,
27
+ value.onEditComment,
28
+ value.onDeleteComment,
27
29
  value.mentionSuggestions,
28
30
  value.onUploadAttachments,
29
31
  value.loading,
30
32
  value.error,
33
+ value.activityDenied,
34
+ value.commentsDenied,
31
35
  ]);
32
36
  return (_jsx(DiscussionContext.Provider, { value: memo, children: children }));
33
37
  };
@@ -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
  }
@@ -50,6 +50,57 @@ export interface ApprovalProgress {
50
50
  satisfied: boolean;
51
51
  }>;
52
52
  }
53
+ /**
54
+ * The approval state a record surface shows (objectui#2902): whether an
55
+ * approval is running on the record (`pending`, which drives the status band
56
+ * and the recall lever) and whether it also forbids edits (`locked`).
57
+ */
58
+ export interface ApprovalLockState {
59
+ pending: boolean;
60
+ locked: boolean;
61
+ }
62
+ /** Input to {@link resolveApprovalLock}. */
63
+ export interface ApprovalLockInput {
64
+ /**
65
+ * Whether the approvals read ANSWERED (a 2xx) for this record. When it did,
66
+ * the open request is the whole answer and the record's `approval_status`
67
+ * column is not consulted. `false` covers a route that is absent (404 /
68
+ * 501), a read that has not landed yet, and a read that failed before it
69
+ * ever answered.
70
+ */
71
+ readAnswered: boolean;
72
+ /**
73
+ * The open approval on the record, reduced to its lock policy, or `null`
74
+ * when there is none. For a host this is the pending request it found
75
+ * (`locked` from the node's `lock_record`); for a renderer it is the verdict
76
+ * a host threaded through {@link InlineEditContextValue}.
77
+ */
78
+ openApproval: {
79
+ locked: boolean;
80
+ } | null;
81
+ /** The record's own `approval_status` value, read only as the fallback. */
82
+ recordStatus: unknown;
83
+ }
84
+ /**
85
+ * THE approval-lock rule of a record surface (objectui#12220). The record page
86
+ * host (`@object-ui/app-shell` `RecordDetailView`) and the detail body's
87
+ * approval band (`@object-ui/plugin-detail` `DetailView`) both call this, so
88
+ * the two cannot disagree about when a record is locked.
89
+ *
90
+ * 1. An open approval decides: it is pending, and it locks exactly when its
91
+ * node's `lockRecord` policy says so (objectui#2902).
92
+ * 2. With none, an approvals read that ANSWERED means no approval is running:
93
+ * not pending, not locked. The record's `approval_status` column is not a
94
+ * second opinion there. A composition without the approvals service answers
95
+ * the read `200 { data: [] }` and enforces no lock, so a `pending` left in
96
+ * the column by any writer would otherwise lock the record with nothing to
97
+ * recall and nothing the server refuses.
98
+ * 3. Only when no read answered does the column stand in: `pending` or
99
+ * `in_approval` reads as pending AND locked. It carries no node granularity,
100
+ * so it can only mean "locked", the conservative read for a backend that
101
+ * mirrors the status but exposes no approvals API (objectui#2618).
102
+ */
103
+ export declare function resolveApprovalLock({ readAnswered, openApproval, recordStatus, }: ApprovalLockInput): ApprovalLockState;
53
104
  export interface InlineEditContextValue {
54
105
  /** True while the record is in inline-edit mode. */
55
106
  editing: boolean;
@@ -86,6 +137,18 @@ export interface InlineEditContextValue {
86
137
  * still get a coherent band. Defaults to `false`.
87
138
  */
88
139
  approvalPending: boolean;
140
+ /**
141
+ * Whether the host's `locked` / `approvalPending` come from an approvals
142
+ * read that ANSWERED (a 2xx) for this record (objectui#12220). When `true`,
143
+ * the host's pair is the whole answer and a renderer must not fall back to
144
+ * the record's `approval_status` column: `false` / `false` then means "no
145
+ * approval is running", not "this host has no opinion". Feed it to
146
+ * {@link resolveApprovalLock} as `readAnswered`.
147
+ *
148
+ * Omitted ⇒ `false` ⇒ unchanged: a renderer keeps the column as its
149
+ * fallback, so a host that never learns of this prop renders as before.
150
+ */
151
+ approvalReadAnswered?: boolean;
89
152
  /**
90
153
  * Quorum / per-group tally of the pending approval node, when it aggregates
91
154
  * more than one decision (objectstack#4478). Threaded verbatim from the
@@ -95,6 +158,37 @@ export interface InlineEditContextValue {
95
158
  * doesn't resolve approvals at all.
96
159
  */
97
160
  approvalProgress?: ApprovalProgress;
161
+ /**
162
+ * Whether the viewer is the SUBMITTER of the pending approval request
163
+ * (`830ed5803`). Recall is the submitter's lever and the server enforces
164
+ * exactly that — a non-submitter's recall is refused — so a recall button
165
+ * lit for everyone who can read the record is a button whose click must
166
+ * fail. The host resolves the identity (server-resolved `viewer.is_submitter`
167
+ * on the pending request, with an id comparison as the pre-framework#3310
168
+ * fallback) for the same reason it resolves `locked`: the renderer stays
169
+ * DataSource-agnostic and never re-derives who submitted what.
170
+ *
171
+ * TRI-STATE, and the third state is the load-bearing one:
172
+ * `true` — the viewer submitted this approval; offer recall.
173
+ * `false` — the host consulted the pending request and this viewer is
174
+ * not its submitter; withdraw the recall affordance.
175
+ * `undefined` — the host does not resolve submitter identity at all
176
+ * (no approvals API, band driven by the record's
177
+ * `approval_status` mirror alone). Behaviour is UNCHANGED
178
+ * from before this signal existed: recall stays offered.
179
+ *
180
+ * The `undefined` case defaults to *showing* on purpose, mirroring how
181
+ * `approvalPending` falls back to `locked`: omission preserves a host's
182
+ * previous behaviour. Defaulting it to "hide" would silently take the
183
+ * submitter's only unlock lever away from every host that never learns of
184
+ * this prop — trading a cosmetic defect for a functional loss.
185
+ *
186
+ * ⚠️ FEEDBACK ONLY. This decides who is SHOWN the entry, never who MAY
187
+ * recall: the server authorizes the recall itself and rejects a
188
+ * non-submitter regardless of what any client renders. Nothing may treat
189
+ * this as an authorization verdict.
190
+ */
191
+ approvalIsSubmitter?: boolean;
98
192
  /**
99
193
  * Human-readable reason for the approval lock, surfaced as the band's
100
194
  * tooltip. Optional — consumers fall back to their own localized default
@@ -104,7 +198,15 @@ export interface InlineEditContextValue {
104
198
  /**
105
199
  * Draft of user-edited values. Holds ONLY the keys the user actually
106
200
  * changed, so the save path never writes computed / read-only / untouched
107
- * fields. Read a field's live value as `draft[name] ?? data[name]`.
201
+ * fields. Read a field's live value from the staged record
202
+ * `{ ...data, ...draft }`, where `data` is the saved record: an OWN draft
203
+ * key wins even when its value is empty (`null`, `undefined`, `''`), and
204
+ * `data[name]` is read only when the key is absent. An own key with an
205
+ * empty value is a field the user, or a cascade clear, emptied, not an
206
+ * untouched one. A read that falls back to the saved value there shows the
207
+ * value the user just removed, and hands an option widget that pruned it
208
+ * the same value to prune again on every render (objectui#7190,
209
+ * objectui#10466).
108
210
  */
109
211
  draft: Record<string, any>;
110
212
  /** Field to auto-focus when edit was entered from a specific field. */
@@ -113,6 +215,34 @@ export interface InlineEditContextValue {
113
215
  saving: boolean;
114
216
  /** Last save error message, or null (driven by the save bar). */
115
217
  error: string | null;
218
+ /**
219
+ * Per-field reasons from the last rejected save, keyed by field MACHINE NAME
220
+ * — or `null` when the refusal was not field-scoped (objectui#6868).
221
+ *
222
+ * The record-level companion to `error`, and driven by the same component:
223
+ * `<InlineEditSaveBar>` reads the server's `VALIDATION_FAILED` envelope
224
+ * through `extractFieldErrors` and publishes the result here, so the field
225
+ * renderers (`DetailSection`, `HeaderHighlight`) can draw the reason beside
226
+ * the input the server actually refused. Before this slot existed the save
227
+ * bar and the field rows had no channel between them — they are SIBLINGS
228
+ * under this provider in both persistence modes — so an attributed refusal
229
+ * could only be shown record-level, which is not the in-place hint the
230
+ * objectui#6868 ruling asked for.
231
+ *
232
+ * ⚠️ PRESENTATION ONLY. These are the SERVER's verdicts, transported. The
233
+ * ruling makes the server the validation authority on the inline-edit
234
+ * surface: nothing may populate this slot from a client-side rule check, and
235
+ * nothing may read it as an authorization or acceptance decision. It is text
236
+ * to render beside an input, nothing more.
237
+ *
238
+ * Keyed by machine name because that is what both ends already use — the
239
+ * draft key the field renderers set, and the `field` the server names — so
240
+ * no spelling translation sits between the refusal and the input.
241
+ *
242
+ * Cleared by `enter()` and by teardown, exactly like `error`, so an
243
+ * attribution can never outlive the session that produced it.
244
+ */
245
+ fieldErrors: Record<string, string> | null;
116
246
  /** Enter inline-edit mode, optionally focused on `field`. No-op when `!canEdit`. */
117
247
  enter: (field?: string) => void;
118
248
  /** Stage a single field edit into the draft. */
@@ -125,6 +255,11 @@ export interface InlineEditContextValue {
125
255
  setSaving: (saving: boolean) => void;
126
256
  /** Set or clear the save error message (used by the save bar). */
127
257
  setError: (error: string | null) => void;
258
+ /**
259
+ * Publish or clear the per-field reasons for a rejected save (used by the
260
+ * save bar). Pass `null` when the refusal was not field-scoped.
261
+ */
262
+ setFieldErrors: (errors: Record<string, string> | null) => void;
128
263
  }
129
264
  export interface InlineEditProviderProps {
130
265
  /**
@@ -146,12 +281,27 @@ export interface InlineEditProviderProps {
146
281
  * only know about the lock rendering exactly as before.
147
282
  */
148
283
  approvalPending?: boolean;
284
+ /**
285
+ * Whether `locked` / `approvalPending` come from an approvals read that
286
+ * answered (objectui#12220; see `InlineEditContextValue`). Omitted ⇒ `false`,
287
+ * which keeps a renderer's `approval_status` column fallback exactly as
288
+ * before.
289
+ */
290
+ approvalReadAnswered?: boolean;
149
291
  /**
150
292
  * The pending node's server-computed decision tally (objectstack#4478).
151
293
  * Surfaced verbatim so the band can show "2 of 3" / per-group ticks. Omitted
152
294
  * for `first_response` nodes and for hosts that don't read approvals.
153
295
  */
154
296
  approvalProgress?: ApprovalProgress;
297
+ /**
298
+ * Whether the viewer submitted the pending approval request
299
+ * (`830ed5803`). Surfaced verbatim so the recall affordance is offered to
300
+ * the submitter only. Omitted ⇒ `undefined` ⇒ recall is offered exactly as
301
+ * it was before this prop existed (see `InlineEditContextValue`), so a host
302
+ * that resolves no approval identity is unchanged.
303
+ */
304
+ approvalIsSubmitter?: boolean;
155
305
  /** Optional human-readable lock reason, surfaced as the band tooltip. */
156
306
  lockedReason?: string;
157
307
  children: React.ReactNode;
@@ -23,8 +23,35 @@ import { jsx as _jsx } from "react/jsx-runtime";
23
23
  * context for the draft/editing flags and drives `saving`/`error`/`reset`.
24
24
  */
25
25
  import React from 'react';
26
+ /**
27
+ * THE approval-lock rule of a record surface (objectui#12220). The record page
28
+ * host (`@object-ui/app-shell` `RecordDetailView`) and the detail body's
29
+ * approval band (`@object-ui/plugin-detail` `DetailView`) both call this, so
30
+ * the two cannot disagree about when a record is locked.
31
+ *
32
+ * 1. An open approval decides: it is pending, and it locks exactly when its
33
+ * node's `lockRecord` policy says so (objectui#2902).
34
+ * 2. With none, an approvals read that ANSWERED means no approval is running:
35
+ * not pending, not locked. The record's `approval_status` column is not a
36
+ * second opinion there. A composition without the approvals service answers
37
+ * the read `200 { data: [] }` and enforces no lock, so a `pending` left in
38
+ * the column by any writer would otherwise lock the record with nothing to
39
+ * recall and nothing the server refuses.
40
+ * 3. Only when no read answered does the column stand in: `pending` or
41
+ * `in_approval` reads as pending AND locked. It carries no node granularity,
42
+ * so it can only mean "locked", the conservative read for a backend that
43
+ * mirrors the status but exposes no approvals API (objectui#2618).
44
+ */
45
+ export function resolveApprovalLock({ readAnswered, openApproval, recordStatus, }) {
46
+ if (openApproval)
47
+ return { pending: true, locked: openApproval.locked };
48
+ if (readAnswered)
49
+ return { pending: false, locked: false };
50
+ const columnPending = recordStatus === 'pending' || recordStatus === 'in_approval';
51
+ return { pending: columnPending, locked: columnPending };
52
+ }
26
53
  const InlineEditContext = React.createContext(null);
27
- export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPending, approvalProgress, lockedReason, children, }) => {
54
+ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPending, approvalReadAnswered = false, approvalProgress, approvalIsSubmitter, lockedReason, children, }) => {
28
55
  // A locked record always has an approval running; the converse is what
29
56
  // #2902 added. Defaulting this way means an un-migrated host keeps its
30
57
  // previous behavior with no code change.
@@ -34,6 +61,7 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPen
34
61
  const [autoFocusField, setAutoFocusField] = React.useState(null);
35
62
  const [saving, setSaving] = React.useState(false);
36
63
  const [error, setError] = React.useState(null);
64
+ const [fieldErrors, setFieldErrors] = React.useState(null);
37
65
  const enter = React.useCallback((field) => {
38
66
  // Object-lifecycle / permission gate: never enter edit on a record the
39
67
  // host has marked non-editable, even if a stray affordance fires.
@@ -42,6 +70,7 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPen
42
70
  setAutoFocusField(field ?? null);
43
71
  setEditing(true);
44
72
  setError(null);
73
+ setFieldErrors(null);
45
74
  }, [canEdit]);
46
75
  const setField = React.useCallback((field, value) => {
47
76
  setDraft((prev) => ({ ...prev, [field]: value }));
@@ -56,25 +85,30 @@ export const InlineEditProvider = ({ canEdit = true, locked = false, approvalPen
56
85
  setAutoFocusField(null);
57
86
  setSaving(false);
58
87
  setError(null);
88
+ setFieldErrors(null);
59
89
  }, []);
60
90
  const value = React.useMemo(() => ({
61
91
  editing,
62
92
  canEdit,
63
93
  locked,
64
94
  approvalPending: pending,
95
+ approvalReadAnswered,
65
96
  approvalProgress,
97
+ approvalIsSubmitter,
66
98
  lockedReason,
67
99
  draft,
68
100
  autoFocusField,
69
101
  saving,
70
102
  error,
103
+ fieldErrors,
71
104
  enter,
72
105
  setField,
73
106
  cancel: teardown,
74
107
  reset: teardown,
75
108
  setSaving,
76
109
  setError,
77
- }), [editing, canEdit, locked, pending, approvalProgress, lockedReason, draft, autoFocusField, saving, error, enter, setField, teardown]);
110
+ setFieldErrors,
111
+ }), [editing, canEdit, locked, pending, approvalReadAnswered, approvalProgress, approvalIsSubmitter, lockedReason, draft, autoFocusField, saving, error, fieldErrors, enter, setField, teardown]);
78
112
  return _jsx(InlineEditContext.Provider, { value: value, children: children });
79
113
  };
80
114
  /**
@@ -0,0 +1,60 @@
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 type { PagePrint } from '@objectstack/spec/ui';
9
+ /**
10
+ * PrintPageContext — the `print` declaration of the page this subtree renders
11
+ * in, or `null` outside a print page (objectui#11958, card ② of the ruling of
12
+ * record on objectstack#8346).
13
+ *
14
+ * A page that declares `print` is a document: its blocks come from the spec's
15
+ * printable block subset (`PRINTABLE_PAGE_COMPONENT_TYPES` in
16
+ * `@objectstack/spec/ui`), and each of those blocks has to reach paper WHOLE.
17
+ * Most of them already do. The ones that draw a window of their data on screen
18
+ * read this context to stop doing so silently: `record:line_items` names its
19
+ * row cap when the cap cut lines off, and a collapsible `record:details`
20
+ * section draws open, because a closed section never reaches paper.
21
+ *
22
+ * Published by `PageRenderer` (`@object-ui/components`) for a page whose
23
+ * `print` is an object. The value is the declaration itself, so a block that
24
+ * ever needs one of its keys reads it from here rather than from a second
25
+ * channel. Defaults to `null`, so every page that does not declare `print` is
26
+ * untouched.
27
+ */
28
+ declare const PrintPageContext: import("react").Context<{
29
+ paperSize?: "A4" | "A5" | "Letter" | "Legal" | undefined;
30
+ orientation?: "portrait" | "landscape" | undefined;
31
+ margins?: {
32
+ top?: number | undefined;
33
+ right?: number | undefined;
34
+ bottom?: number | undefined;
35
+ left?: number | undefined;
36
+ } | undefined;
37
+ repeatHeader?: boolean | undefined;
38
+ repeatFooter?: boolean | undefined;
39
+ pageNumbers?: boolean | undefined;
40
+ repeatTableHeaders?: boolean | undefined;
41
+ avoidBreakInside?: boolean | undefined;
42
+ } | null>;
43
+ export declare const PrintPageProvider: import("react").Provider<{
44
+ paperSize?: "A4" | "A5" | "Letter" | "Legal" | undefined;
45
+ orientation?: "portrait" | "landscape" | undefined;
46
+ margins?: {
47
+ top?: number | undefined;
48
+ right?: number | undefined;
49
+ bottom?: number | undefined;
50
+ left?: number | undefined;
51
+ } | undefined;
52
+ repeatHeader?: boolean | undefined;
53
+ repeatFooter?: boolean | undefined;
54
+ pageNumbers?: boolean | undefined;
55
+ repeatTableHeaders?: boolean | undefined;
56
+ avoidBreakInside?: boolean | undefined;
57
+ } | null>;
58
+ /** The enclosing page's `print` declaration, or `null` outside a print page. Safe outside a provider. */
59
+ export declare const usePrintPage: () => PagePrint | null;
60
+ export { PrintPageContext };
@@ -0,0 +1,32 @@
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 { createContext, useContext } from 'react';
9
+ /**
10
+ * PrintPageContext — the `print` declaration of the page this subtree renders
11
+ * in, or `null` outside a print page (objectui#11958, card ② of the ruling of
12
+ * record on objectstack#8346).
13
+ *
14
+ * A page that declares `print` is a document: its blocks come from the spec's
15
+ * printable block subset (`PRINTABLE_PAGE_COMPONENT_TYPES` in
16
+ * `@objectstack/spec/ui`), and each of those blocks has to reach paper WHOLE.
17
+ * Most of them already do. The ones that draw a window of their data on screen
18
+ * read this context to stop doing so silently: `record:line_items` names its
19
+ * row cap when the cap cut lines off, and a collapsible `record:details`
20
+ * section draws open, because a closed section never reaches paper.
21
+ *
22
+ * Published by `PageRenderer` (`@object-ui/components`) for a page whose
23
+ * `print` is an object. The value is the declaration itself, so a block that
24
+ * ever needs one of its keys reads it from here rather than from a second
25
+ * channel. Defaults to `null`, so every page that does not declare `print` is
26
+ * untouched.
27
+ */
28
+ const PrintPageContext = createContext(null);
29
+ export const PrintPageProvider = PrintPageContext.Provider;
30
+ /** The enclosing page's `print` declaration, or `null` outside a print page. Safe outside a provider. */
31
+ export const usePrintPage = () => useContext(PrintPageContext);
32
+ export { PrintPageContext };
@@ -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, [