@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.
- package/CHANGELOG.md +4263 -0
- package/README.md +383 -42
- package/dist/SchemaRenderer.d.ts +64 -7
- package/dist/SchemaRenderer.js +1326 -106
- package/dist/context/ActionContext.js +40 -1
- package/dist/context/AppShellContext.d.ts +27 -0
- package/dist/context/DiscussionContext.d.ts +27 -0
- package/dist/context/DiscussionContext.js +4 -0
- package/dist/context/DrillNavigationContext.d.ts +13 -0
- package/dist/context/InlineEditContext.d.ts +151 -1
- package/dist/context/InlineEditContext.js +36 -2
- package/dist/context/PrintPageContext.d.ts +60 -0
- package/dist/context/PrintPageContext.js +32 -0
- package/dist/context/RecordContext.d.ts +48 -5
- package/dist/context/RecordContext.js +10 -1
- package/dist/context/RelatedRecordActionsContext.d.ts +72 -2
- package/dist/context/RelatedRecordActionsContext.js +4 -0
- package/dist/context/SchemaRendererContext.d.ts +61 -6
- package/dist/context/SchemaRendererContext.js +39 -6
- package/dist/context/ThemeContext.js +1 -1
- package/dist/context/actionRunnerTranslator.d.ts +27 -0
- package/dist/context/actionRunnerTranslator.js +37 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +1 -0
- package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
- package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
- package/dist/element-data-source/ElementDataSourceGate.d.ts +118 -3
- package/dist/element-data-source/ElementDataSourceGate.js +401 -19
- package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
- package/dist/element-data-source/useElementApiDataSource.js +198 -0
- package/dist/hooks/index.d.ts +3 -1
- package/dist/hooks/index.js +10 -1
- package/dist/hooks/useActionEngine.js +46 -3
- package/dist/hooks/useActionRunner.js +4 -0
- package/dist/hooks/useActionTextLocalizer.d.ts +35 -9
- package/dist/hooks/useActionTextLocalizer.js +70 -14
- package/dist/hooks/useConfigBagEvaluator.d.ts +31 -0
- package/dist/hooks/useConfigBagEvaluator.js +40 -0
- package/dist/hooks/useDatasetDimensionLabels.js +25 -10
- package/dist/hooks/useDensityMode.d.ts +8 -2
- package/dist/hooks/useDensityMode.js +8 -2
- package/dist/hooks/useDiscovery.d.ts +1 -10
- package/dist/hooks/useETagCache.js +25 -4
- package/dist/hooks/useElementDataSource.d.ts +15 -2
- package/dist/hooks/useElementDataSource.js +24 -7
- package/dist/hooks/useExpression.d.ts +52 -59
- package/dist/hooks/useExpression.js +58 -90
- package/dist/hooks/useFilterScope.d.ts +22 -2
- package/dist/hooks/useFilterScope.js +29 -4
- package/dist/hooks/useGlobalUndo.d.ts +10 -0
- package/dist/hooks/useGlobalUndo.js +112 -46
- package/dist/hooks/useNavigationOverlay.d.ts +76 -11
- package/dist/hooks/useNavigationOverlay.js +119 -9
- package/dist/hooks/useOffline.js +65 -11
- package/dist/hooks/usePageAssignment.d.ts +33 -7
- package/dist/hooks/usePageAssignment.js +85 -21
- package/dist/hooks/useRecordSearch.d.ts +34 -1
- package/dist/hooks/useRecordSearch.js +42 -7
- package/dist/hooks/useResolvedFilter.d.ts +46 -0
- package/dist/hooks/useResolvedFilter.js +130 -0
- package/dist/hooks/useSchemaPersistence.d.ts +7 -0
- package/dist/hooks/useSchemaPersistence.js +144 -5
- package/dist/hooks/useSettledSchema.d.ts +123 -0
- package/dist/hooks/useSettledSchema.js +186 -0
- package/dist/hooks/useViewData.d.ts +10 -3
- package/dist/hooks/useViewData.js +5 -14
- package/dist/index.d.ts +9 -1
- package/dist/index.js +48 -1
- package/dist/schema-input.d.ts +38 -8
- package/dist/schema-input.js +39 -7
- package/dist/utils/configBag.d.ts +68 -0
- package/dist/utils/configBag.js +70 -0
- package/dist/utils/error-message.d.ts +1 -1
- package/dist/utils/error-message.js +1 -1
- package/dist/utils/inlineAria.d.ts +61 -0
- package/dist/utils/inlineAria.js +70 -0
- package/dist/utils/localeMap.d.ts +27 -0
- package/dist/utils/localeMap.js +106 -0
- package/dist/utils/nonGridRowCeiling.d.ts +44 -0
- package/dist/utils/nonGridRowCeiling.js +78 -0
- package/dist/utils/paramsBag.d.ts +28 -0
- package/dist/utils/paramsBag.js +146 -0
- package/dist/utils/propsBagDiagnostic.d.ts +196 -0
- package/dist/utils/propsBagDiagnostic.js +422 -0
- package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
- package/dist/utils/refusedDataPropDiagnostic.js +94 -0
- package/dist/utils/unevaluatedExpression.d.ts +44 -1
- package/dist/utils/unevaluatedExpression.js +92 -11
- package/dist/utils/visibilityDiagnostic.d.ts +282 -21
- package/dist/utils/visibilityDiagnostic.js +321 -46
- package/package.json +10 -10
- package/dist/hooks/useClientNotifications.d.ts +0 -47
- package/dist/hooks/useClientNotifications.js +0 -162
- package/dist/spec-bridge/SpecBridge.d.ts +0 -28
- package/dist/spec-bridge/SpecBridge.js +0 -56
- package/dist/spec-bridge/bridges/form-view.d.ts +0 -158
- package/dist/spec-bridge/bridges/form-view.js +0 -170
- package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
- package/dist/spec-bridge/bridges/list-view.js +0 -268
- package/dist/spec-bridge/index.d.ts +0 -11
- package/dist/spec-bridge/index.js +0 -10
- package/dist/spec-bridge/types.d.ts +0 -55
- 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
|
|
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
|
-
|
|
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
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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, ...
|
|
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, [
|