@object-ui/react 16.0.0 → 17.1.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 +786 -0
- package/README.md +65 -0
- package/dist/SchemaRenderer.js +12 -2
- package/dist/context/InlineEditContext.d.ts +21 -0
- package/dist/context/InlineEditContext.js +7 -2
- package/dist/context/NotificationContext.d.ts +245 -17
- package/dist/context/NotificationContext.js +222 -20
- package/dist/context/SchemaRendererContext.d.ts +10 -1
- package/dist/context/SchemaRendererContext.js +7 -2
- package/dist/context/ThemeContext.js +4 -8
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.js +3 -1
- package/dist/hooks/useAnimation.d.ts +40 -4
- package/dist/hooks/useAnimation.js +41 -10
- package/dist/hooks/useCapabilityGate.d.ts +16 -0
- package/dist/hooks/useCapabilityGate.js +60 -0
- package/dist/hooks/useClientNotifications.d.ts +0 -6
- package/dist/hooks/useClientNotifications.js +7 -53
- package/dist/hooks/useETagCache.js +4 -0
- package/dist/hooks/useExpression.d.ts +8 -2
- package/dist/hooks/useExpression.js +9 -1
- package/dist/hooks/useFilterScope.d.ts +30 -0
- package/dist/hooks/useFilterScope.js +55 -0
- package/dist/hooks/useNavigationOverlay.d.ts +11 -0
- package/dist/hooks/useNavigationOverlay.js +35 -1
- package/dist/hooks/usePageVariables.js +23 -11
- package/dist/hooks/useRecordSearch.js +100 -5
- package/dist/hooks/useViewData.js +10 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/spec-bridge/SpecBridge.d.ts +0 -4
- package/dist/spec-bridge/SpecBridge.js +0 -12
- package/dist/spec-bridge/bridges/list-view.js +5 -11
- package/dist/spec-bridge/index.d.ts +0 -2
- package/dist/spec-bridge/index.js +0 -2
- package/dist/utils/error-message.d.ts +52 -0
- package/dist/utils/error-message.js +138 -0
- package/package.json +7 -7
- package/dist/hooks/useColumnSummary.d.ts +0 -38
- package/dist/hooks/useColumnSummary.js +0 -54
- package/dist/spec-bridge/bridges/dashboard.d.ts +0 -81
- package/dist/spec-bridge/bridges/dashboard.js +0 -121
- package/dist/spec-bridge/bridges/page.d.ts +0 -58
- package/dist/spec-bridge/bridges/page.js +0 -101
|
@@ -17,8 +17,13 @@
|
|
|
17
17
|
* `sys_inbox_message` materialization and the `sys_notification_receipt`
|
|
18
18
|
* read-state spine (the re-modeled `sys_notification` L2 event carries no
|
|
19
19
|
* recipient/read columns). This hook only needs to call the SDK with its
|
|
20
|
-
* current signatures: `list(options)`, `markRead(ids[])`, `markAllRead()
|
|
21
|
-
*
|
|
20
|
+
* current signatures: `list(options)`, `markRead(ids[])`, `markAllRead()`.
|
|
21
|
+
*
|
|
22
|
+
* The former registerDevice/getPreferences/updatePreferences delegates were
|
|
23
|
+
* removed (objectstack#3612): the SDK deleted those methods because the
|
|
24
|
+
* /notifications/devices and /notifications/preferences server routes were
|
|
25
|
+
* never built — every call was a guaranteed error. They return when the
|
|
26
|
+
* server side exists.
|
|
22
27
|
*/
|
|
23
28
|
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
24
29
|
import { SchemaRendererContext } from '../context/SchemaRendererContext';
|
|
@@ -113,54 +118,6 @@ export function useClientNotifications(options = {}) {
|
|
|
113
118
|
setLoading(false);
|
|
114
119
|
}
|
|
115
120
|
}, [client, notify]);
|
|
116
|
-
/* ---- registerDevice ---- */
|
|
117
|
-
const registerDevice = useCallback(async (token, platform) => {
|
|
118
|
-
if (!client?.notifications)
|
|
119
|
-
return;
|
|
120
|
-
setError(null);
|
|
121
|
-
try {
|
|
122
|
-
// ADR-0030 / @objectstack/client v7+: registerDevice takes a single
|
|
123
|
-
// RegisterDeviceRequest object (`{ token, platform }`), not positional
|
|
124
|
-
// args. `platform` is the device platform enum.
|
|
125
|
-
await client.notifications.registerDevice({
|
|
126
|
-
token,
|
|
127
|
-
platform: platform,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
catch (err) {
|
|
131
|
-
const wrapped = err instanceof Error ? err : new Error('Failed to register device');
|
|
132
|
-
setError(wrapped);
|
|
133
|
-
throw wrapped;
|
|
134
|
-
}
|
|
135
|
-
}, [client]);
|
|
136
|
-
/* ---- getPreferences ---- */
|
|
137
|
-
const getPreferences = useCallback(async () => {
|
|
138
|
-
if (!client?.notifications)
|
|
139
|
-
return null;
|
|
140
|
-
setError(null);
|
|
141
|
-
try {
|
|
142
|
-
return await client.notifications.getPreferences();
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
const wrapped = err instanceof Error ? err : new Error('Failed to get preferences');
|
|
146
|
-
setError(wrapped);
|
|
147
|
-
throw wrapped;
|
|
148
|
-
}
|
|
149
|
-
}, [client]);
|
|
150
|
-
/* ---- updatePreferences ---- */
|
|
151
|
-
const updatePreferences = useCallback(async (prefs) => {
|
|
152
|
-
if (!client?.notifications)
|
|
153
|
-
return;
|
|
154
|
-
setError(null);
|
|
155
|
-
try {
|
|
156
|
-
await client.notifications.updatePreferences(prefs);
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
const wrapped = err instanceof Error ? err : new Error('Failed to update preferences');
|
|
160
|
-
setError(wrapped);
|
|
161
|
-
throw wrapped;
|
|
162
|
-
}
|
|
163
|
-
}, [client]);
|
|
164
121
|
/* ---- markAsRead (server + local) ---- */
|
|
165
122
|
const markAsRead = useCallback(async (id) => {
|
|
166
123
|
// Optimistically mark locally first
|
|
@@ -198,9 +155,6 @@ export function useClientNotifications(options = {}) {
|
|
|
198
155
|
}, [pollInterval, client, fetchNotifications]);
|
|
199
156
|
return {
|
|
200
157
|
fetchNotifications,
|
|
201
|
-
registerDevice,
|
|
202
|
-
getPreferences,
|
|
203
|
-
updatePreferences,
|
|
204
158
|
markAsRead,
|
|
205
159
|
loading,
|
|
206
160
|
error,
|
|
@@ -222,6 +222,10 @@ export function useETagCache(userConfig = {}) {
|
|
|
222
222
|
catch (e) {
|
|
223
223
|
if (e instanceof Error && e.message.startsWith('Unsupported'))
|
|
224
224
|
throw e;
|
|
225
|
+
// Any other parse failure collapses to a generic message; we can't
|
|
226
|
+
// attach `e` as the `Error` `cause` because this package targets
|
|
227
|
+
// ES2020, whose lib types the 1-arg `Error` constructor only.
|
|
228
|
+
// eslint-disable-next-line preserve-caught-error
|
|
225
229
|
throw new Error('Invalid URL');
|
|
226
230
|
}
|
|
227
231
|
}
|
|
@@ -40,7 +40,10 @@ export declare function usePredicateScope(): Record<string, any>;
|
|
|
40
40
|
* This is the canonical helper to use in renderers so we never end up with
|
|
41
41
|
* `${[object Object]}` after JS template-literal interpolation.
|
|
42
42
|
*/
|
|
43
|
-
export declare function toPredicateInput(value: unknown): string | boolean |
|
|
43
|
+
export declare function toPredicateInput(value: unknown): string | boolean | {
|
|
44
|
+
dialect: 'cel';
|
|
45
|
+
source: string;
|
|
46
|
+
} | undefined;
|
|
44
47
|
/**
|
|
45
48
|
* Hook for evaluating expressions with dynamic context
|
|
46
49
|
*
|
|
@@ -51,7 +54,10 @@ export declare function toPredicateInput(value: unknown): string | boolean | und
|
|
|
51
54
|
* ```
|
|
52
55
|
*/
|
|
53
56
|
export declare function useExpression(expression: string | boolean | number | null | undefined, context?: Record<string, any>): any;
|
|
54
|
-
export declare function useCondition(condition: string | boolean | undefined
|
|
57
|
+
export declare function useCondition(condition: string | boolean | undefined | {
|
|
58
|
+
dialect?: string;
|
|
59
|
+
source?: string;
|
|
60
|
+
}, context?: Record<string, any>, options?: {
|
|
55
61
|
throwOnError?: boolean;
|
|
56
62
|
label?: string;
|
|
57
63
|
}): boolean;
|
|
@@ -63,7 +63,15 @@ export function toPredicateInput(value) {
|
|
|
63
63
|
return `\${${value}}`;
|
|
64
64
|
if (typeof value === 'object' && typeof value.source === 'string') {
|
|
65
65
|
const src = value.source;
|
|
66
|
-
|
|
66
|
+
if (!src)
|
|
67
|
+
return undefined;
|
|
68
|
+
// #2661 — preserve a CEL-dialect envelope so `useCondition` routes it to the
|
|
69
|
+
// canonical `@objectstack/formula` engine (identical verdict to the server),
|
|
70
|
+
// instead of collapsing it to a `${source}` string on the legacy JS path.
|
|
71
|
+
// Every other dialect (template / unset) keeps the legacy `${…}` behavior.
|
|
72
|
+
if (value.dialect === 'cel')
|
|
73
|
+
return { dialect: 'cel', source: src };
|
|
74
|
+
return `\${${src}}`;
|
|
67
75
|
}
|
|
68
76
|
return undefined;
|
|
69
77
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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 ReactNode } from 'react';
|
|
9
|
+
import type { FilterTokenScope } from '@object-ui/core';
|
|
10
|
+
/**
|
|
11
|
+
* Provide the session scope used to resolve filter placeholders.
|
|
12
|
+
*
|
|
13
|
+
* Mount once, above any surface that renders filtered data. The console shell
|
|
14
|
+
* mounts it next to `ExpressionProvider`, where both the signed-in user and
|
|
15
|
+
* the active organization are already resolved.
|
|
16
|
+
*/
|
|
17
|
+
export declare function FilterScopeProvider({ currentUserId, currentOrgId, children, }: {
|
|
18
|
+
currentUserId?: string | null;
|
|
19
|
+
currentOrgId?: string | null;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}): import("react").FunctionComponentElement<import("react").ProviderProps<FilterTokenScope>>;
|
|
22
|
+
/**
|
|
23
|
+
* Read the session scope for filter placeholder resolution.
|
|
24
|
+
*
|
|
25
|
+
* Pass the result straight to `resolveFilterPlaceholders(filter, scope)` from
|
|
26
|
+
* `@object-ui/core` — that helper expands every placeholder vocabulary in one
|
|
27
|
+
* call, which is the point: resolving only some of them is the defect behind
|
|
28
|
+
* #3574.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useFilterScope(): FilterTokenScope;
|
|
@@ -0,0 +1,55 @@
|
|
|
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, createElement, useContext, useMemo } from 'react';
|
|
9
|
+
/**
|
|
10
|
+
* The session values filter placeholders resolve against — `{current_user_id}`
|
|
11
|
+
* and `{current_org_id}` (framework #3574).
|
|
12
|
+
*
|
|
13
|
+
* ## Why a dedicated context
|
|
14
|
+
*
|
|
15
|
+
* The renderer packages (`plugin-dashboard`, `plugin-charts`, `plugin-list`)
|
|
16
|
+
* deliberately do not depend on `@object-ui/auth`; they receive everything
|
|
17
|
+
* they need as schema or context. But a widget cannot resolve
|
|
18
|
+
* `{current_user_id}` without knowing who is signed in, and the surfaces that
|
|
19
|
+
* DO know (the app-shell) sit above them.
|
|
20
|
+
*
|
|
21
|
+
* `PredicateScopeContext` already carries `current_user` and would have
|
|
22
|
+
* covered the user id, but it is the *expression* evaluation scope — it
|
|
23
|
+
* carries no organization, and widening it for an unrelated consumer would
|
|
24
|
+
* couple filter resolution to predicate semantics. A separate, two-field
|
|
25
|
+
* context keeps the contract explicit and lets a non-console host provide
|
|
26
|
+
* exactly these values without adopting the expression stack.
|
|
27
|
+
*
|
|
28
|
+
* When no provider is mounted this returns an empty scope. That is a safe
|
|
29
|
+
* default: unresolved tokens are left intact by the resolver, so the filter
|
|
30
|
+
* matches nothing rather than silently widening, and the resolver emits one
|
|
31
|
+
* warning naming the token.
|
|
32
|
+
*/
|
|
33
|
+
const FilterScopeContext = createContext({});
|
|
34
|
+
/**
|
|
35
|
+
* Provide the session scope used to resolve filter placeholders.
|
|
36
|
+
*
|
|
37
|
+
* Mount once, above any surface that renders filtered data. The console shell
|
|
38
|
+
* mounts it next to `ExpressionProvider`, where both the signed-in user and
|
|
39
|
+
* the active organization are already resolved.
|
|
40
|
+
*/
|
|
41
|
+
export function FilterScopeProvider({ currentUserId, currentOrgId, children, }) {
|
|
42
|
+
const value = useMemo(() => ({ currentUserId, currentOrgId }), [currentUserId, currentOrgId]);
|
|
43
|
+
return createElement(FilterScopeContext.Provider, { value }, children);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the session scope for filter placeholder resolution.
|
|
47
|
+
*
|
|
48
|
+
* Pass the result straight to `resolveFilterPlaceholders(filter, scope)` from
|
|
49
|
+
* `@object-ui/core` — that helper expands every placeholder vocabulary in one
|
|
50
|
+
* call, which is the point: resolving only some of them is the defect behind
|
|
51
|
+
* #3574.
|
|
52
|
+
*/
|
|
53
|
+
export function useFilterScope() {
|
|
54
|
+
return useContext(FilterScopeContext);
|
|
55
|
+
}
|
|
@@ -15,8 +15,19 @@ export interface NavigationConfig {
|
|
|
15
15
|
view?: string;
|
|
16
16
|
preventNavigation?: boolean;
|
|
17
17
|
openNewTab?: boolean;
|
|
18
|
+
/** Spec `NavigationConfig.size` (#2578) — coarse overlay bucket. */
|
|
19
|
+
size?: 'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
20
|
+
/** @deprecated [#2578 → `size`] explicit pixel/percent width. */
|
|
18
21
|
width?: string | number;
|
|
19
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the overlay width from a NavigationConfig: an explicit `width` wins
|
|
25
|
+
* (app-shell pre-resolves `size` into it); otherwise a declared bucket maps
|
|
26
|
+
* through {@link OVERLAY_SIZE_WIDTHS}. `'auto'`/absent stays `undefined` — the
|
|
27
|
+
* host's default width — because deriving `auto` needs the object's field
|
|
28
|
+
* count, which only schema-aware hosts have. Exported for the parity test.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveOverlayWidth(navigation: NavigationConfig | undefined): string | number | undefined;
|
|
20
31
|
export type NavigationMode = NavigationConfig['mode'];
|
|
21
32
|
export interface UseNavigationOverlayOptions {
|
|
22
33
|
/** The navigation configuration from the schema */
|
|
@@ -16,6 +16,40 @@
|
|
|
16
16
|
* NavigationConfig support.
|
|
17
17
|
*/
|
|
18
18
|
import { useState, useCallback, useMemo } from 'react';
|
|
19
|
+
/**
|
|
20
|
+
* Pixel cap per overlay `size` bucket, clamped to the viewport at render —
|
|
21
|
+
* mirrors `plugin-view/src/recordSurface.ts` (`OVERLAY_SIZE_PX`), which owns
|
|
22
|
+
* the `size: 'auto'` field-count derivation this layer cannot perform (no
|
|
23
|
+
* object schema here). Kept in lockstep by the spec-parity test.
|
|
24
|
+
*
|
|
25
|
+
* Before #2942 this hook only read the deprecated `width`, so an authored
|
|
26
|
+
* `size` bucket was silently ignored by every host except app-shell (which
|
|
27
|
+
* pre-resolves it before calling in).
|
|
28
|
+
*/
|
|
29
|
+
const OVERLAY_SIZE_WIDTHS = {
|
|
30
|
+
sm: 'min(92vw, 480px)',
|
|
31
|
+
md: 'min(92vw, 720px)',
|
|
32
|
+
lg: 'min(92vw, 960px)',
|
|
33
|
+
xl: 'min(92vw, 1200px)',
|
|
34
|
+
full: 'min(92vw, 1600px)',
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the overlay width from a NavigationConfig: an explicit `width` wins
|
|
38
|
+
* (app-shell pre-resolves `size` into it); otherwise a declared bucket maps
|
|
39
|
+
* through {@link OVERLAY_SIZE_WIDTHS}. `'auto'`/absent stays `undefined` — the
|
|
40
|
+
* host's default width — because deriving `auto` needs the object's field
|
|
41
|
+
* count, which only schema-aware hosts have. Exported for the parity test.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveOverlayWidth(navigation) {
|
|
44
|
+
if (!navigation)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (navigation.width !== undefined)
|
|
47
|
+
return navigation.width;
|
|
48
|
+
const size = navigation.size;
|
|
49
|
+
if (size && size !== 'auto')
|
|
50
|
+
return OVERLAY_SIZE_WIDTHS[size];
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
19
53
|
/**
|
|
20
54
|
* Hook for NavigationConfig-driven navigation overlay.
|
|
21
55
|
*
|
|
@@ -48,7 +82,7 @@ export function useNavigationOverlay(options) {
|
|
|
48
82
|
const [isOpen, setIsOpen] = useState(false);
|
|
49
83
|
const [selectedRecord, setSelectedRecord] = useState(null);
|
|
50
84
|
const mode = navigation?.mode ?? 'page';
|
|
51
|
-
const width = navigation
|
|
85
|
+
const width = resolveOverlayWidth(navigation);
|
|
52
86
|
const view = navigation?.view;
|
|
53
87
|
const isOverlay = mode === 'drawer' || mode === 'modal' || mode === 'split' || mode === 'popover';
|
|
54
88
|
const close = useCallback(() => {
|
|
@@ -117,18 +117,30 @@ PageVariablesProvider.displayName = 'PageVariablesProvider';
|
|
|
117
117
|
*/
|
|
118
118
|
export function usePageVariables() {
|
|
119
119
|
const ctx = useContext(PageVariablesContext);
|
|
120
|
-
|
|
121
|
-
// Graceful fallback — allows components to work outside a Page
|
|
122
|
-
return {
|
|
123
|
-
variables: {},
|
|
124
|
-
definitions: [],
|
|
125
|
-
setVariable: () => { },
|
|
126
|
-
setVariables: () => { },
|
|
127
|
-
resetVariables: () => { },
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
return ctx;
|
|
120
|
+
return ctx ?? OUTSIDE_PAGE;
|
|
131
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Graceful fallback — allows components to work outside a Page.
|
|
124
|
+
*
|
|
125
|
+
* MUST be a module constant, not a fresh literal per call. `SchemaRenderer`
|
|
126
|
+
* feeds `variables` into the `evaluatedSchema` memo's dependency list, so a new
|
|
127
|
+
* object each render defeats that memo for every component rendered outside a
|
|
128
|
+
* PageVariablesProvider — and hands children a new schema identity every time.
|
|
129
|
+
* A `kind:'react'` page memoises its compiled source on that identity, so the
|
|
130
|
+
* page was being remounted and its `useState` reset (objectui#2954).
|
|
131
|
+
*
|
|
132
|
+
* Frozen through, because sharing one instance means a stray write would no
|
|
133
|
+
* longer be scoped to one render — it would leak to every consumer outside a
|
|
134
|
+
* provider. There is no writer today (the setters are the API, and here they
|
|
135
|
+
* are no-ops); the freeze keeps it that way.
|
|
136
|
+
*/
|
|
137
|
+
const OUTSIDE_PAGE = Object.freeze({
|
|
138
|
+
variables: Object.freeze({}),
|
|
139
|
+
definitions: Object.freeze([]),
|
|
140
|
+
setVariable: () => { },
|
|
141
|
+
setVariables: () => { },
|
|
142
|
+
resetVariables: () => { },
|
|
143
|
+
});
|
|
132
144
|
/**
|
|
133
145
|
* Hook to check if a PageVariablesProvider is available.
|
|
134
146
|
*/
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
9
9
|
import { getRecordDisplayName as defaultDisplayName } from '@object-ui/core';
|
|
10
|
+
import { errorCodeIs } from '@object-ui/types';
|
|
10
11
|
// ADR-0079: the global-search default display-name resolver is now the ONE
|
|
11
12
|
// shared `getRecordDisplayName` from `@object-ui/core` (honors titleFormat →
|
|
12
13
|
// `displayNameField` → type-aware derivation → `Record #<id>`). Callers can
|
|
@@ -80,10 +81,20 @@ export function useRecordSearch(opts) {
|
|
|
80
81
|
// Stable signature derived from the object metadata fields we actually
|
|
81
82
|
// touch. Prevents pointless re-runs when the parent re-renders with a
|
|
82
83
|
// new array reference but identical content.
|
|
84
|
+
//
|
|
85
|
+
// Deliberately UNCAPPED: `maxObjectsQueried` bounds the number of parallel
|
|
86
|
+
// per-object requests on the fanout path (applied there), not the search
|
|
87
|
+
// scope itself. Capping here also truncated the `objects` whitelist sent to
|
|
88
|
+
// the single-request `/api/v1/search` call, so the palette silently searched
|
|
89
|
+
// only the first N nav objects — records in every later object were
|
|
90
|
+
// unfindable no matter what the user typed. Measured symptom: ⌘K in an app
|
|
91
|
+
// with 40+ searchable objects only ever queried the first 8 nav entries,
|
|
92
|
+
// so which sidebar group happened to come first decided what was findable.
|
|
83
93
|
const candidates = useMemo(() => {
|
|
84
94
|
if (!Array.isArray(objects) || objects.length === 0)
|
|
85
95
|
return [];
|
|
86
|
-
|
|
96
|
+
// Assigned in both branches below before it's read; no dead initializer.
|
|
97
|
+
let pool;
|
|
87
98
|
if (Array.isArray(objectNames) && objectNames.length > 0) {
|
|
88
99
|
const byName = new Map();
|
|
89
100
|
for (const obj of objects) {
|
|
@@ -98,8 +109,8 @@ export function useRecordSearch(opts) {
|
|
|
98
109
|
else {
|
|
99
110
|
pool = objects.filter((o) => o?.searchable !== false);
|
|
100
111
|
}
|
|
101
|
-
return pool
|
|
102
|
-
}, [objects, objectNames
|
|
112
|
+
return pool;
|
|
113
|
+
}, [objects, objectNames]);
|
|
103
114
|
const candidateSignature = useMemo(() => {
|
|
104
115
|
return candidates.map((o) => `${o?.name}:${o?.titleField ?? ''}`).join('|');
|
|
105
116
|
}, [candidates]);
|
|
@@ -126,7 +137,90 @@ export function useRecordSearch(opts) {
|
|
|
126
137
|
// Skip if a newer run started during the debounce window.
|
|
127
138
|
if (runIdRef.current !== myRunId)
|
|
128
139
|
return;
|
|
129
|
-
|
|
140
|
+
// Prefer the platform's unified global-search endpoint when the data
|
|
141
|
+
// source exposes it (ObjectStackAdapter → GET /api/v1/search?q=). The
|
|
142
|
+
// per-object `find({ $search })` fanout below only runs each object's
|
|
143
|
+
// metadata-driven search (ADR-0061) and can miss records the global
|
|
144
|
+
// search index knows about — the command-palette symptom in framework
|
|
145
|
+
// #3371, where `/api/v1/search` returned an account/opportunity the
|
|
146
|
+
// palette never showed. When `searchAll` is absent (mock/test adapters,
|
|
147
|
+
// non-ObjectStack backends) we fall back to the fanout.
|
|
148
|
+
const searchAll = dataSource.searchAll;
|
|
149
|
+
if (typeof searchAll === 'function') {
|
|
150
|
+
// Scope to the candidate objects (the app's searchable nav objects) so
|
|
151
|
+
// the palette doesn't surface records from objects the current app
|
|
152
|
+
// can't route to. We both ask the server to scope (`objects`) and
|
|
153
|
+
// re-filter locally, guarding servers that ignore the param.
|
|
154
|
+
const byName = new Map();
|
|
155
|
+
for (const obj of candidates) {
|
|
156
|
+
if (typeof obj?.name === 'string')
|
|
157
|
+
byName.set(obj.name, obj);
|
|
158
|
+
}
|
|
159
|
+
Promise.resolve()
|
|
160
|
+
.then(() => searchAll.call(dataSource, trimmed, {
|
|
161
|
+
limit: maxObjectsQueried * topPerObject,
|
|
162
|
+
objects: Array.from(byName.keys()),
|
|
163
|
+
}))
|
|
164
|
+
.then((res) => {
|
|
165
|
+
// Discard if a newer run started while we were waiting.
|
|
166
|
+
if (runIdRef.current !== myRunId)
|
|
167
|
+
return;
|
|
168
|
+
const rawHits = Array.isArray(res?.hits)
|
|
169
|
+
? res.hits
|
|
170
|
+
: Array.isArray(res)
|
|
171
|
+
? res
|
|
172
|
+
: [];
|
|
173
|
+
const hits = [];
|
|
174
|
+
for (const h of rawHits) {
|
|
175
|
+
const objectName = h?.object ?? h?.objectName;
|
|
176
|
+
if (typeof objectName !== 'string')
|
|
177
|
+
continue;
|
|
178
|
+
// Keep only whitelisted objects (no-op when the server already
|
|
179
|
+
// honored `objects`).
|
|
180
|
+
if (byName.size > 0 && !byName.has(objectName))
|
|
181
|
+
continue;
|
|
182
|
+
const recordId = h?.id ?? h?.record?.id ?? h?.record?._id;
|
|
183
|
+
if (recordId == null)
|
|
184
|
+
continue;
|
|
185
|
+
const objDef = byName.get(objectName) ?? { name: objectName };
|
|
186
|
+
const title = typeof h?.title === 'string' && h.title.trim() !== '' ? h.title.trim() : '';
|
|
187
|
+
const display = title || getDisplayName(objDef, h?.record ?? {}) || `Record #${recordId}`;
|
|
188
|
+
const snippet = typeof h?.snippet === 'string' ? h.snippet.trim() : '';
|
|
189
|
+
hits.push({
|
|
190
|
+
objectName,
|
|
191
|
+
objectLabel: typeof objDef.label === 'string' && objDef.label.length > 0
|
|
192
|
+
? objDef.label
|
|
193
|
+
: objectName,
|
|
194
|
+
recordId: String(recordId),
|
|
195
|
+
display,
|
|
196
|
+
// Only surface a snippet when it adds information beyond the title.
|
|
197
|
+
subtitle: snippet && snippet !== display ? snippet : undefined,
|
|
198
|
+
icon: objDef?.icon,
|
|
199
|
+
score: scoreHit(trimmed, display, String(recordId)),
|
|
200
|
+
raw: h?.record ?? h,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// The server already ranks hits across objects; preserve that order.
|
|
204
|
+
// Only float an exact-id paste to the very top (parity with the
|
|
205
|
+
// fanout path). Array.prototype.sort is stable, so every other hit
|
|
206
|
+
// keeps the server ordering.
|
|
207
|
+
hits.sort((a, b) => (b.score === 110 ? 1 : 0) - (a.score === 110 ? 1 : 0));
|
|
208
|
+
setResults(hits);
|
|
209
|
+
setIsSearching(false);
|
|
210
|
+
setError(undefined);
|
|
211
|
+
})
|
|
212
|
+
.catch((err) => {
|
|
213
|
+
if (runIdRef.current !== myRunId)
|
|
214
|
+
return;
|
|
215
|
+
setResults([]);
|
|
216
|
+
setIsSearching(false);
|
|
217
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Fanout fallback only: one request per object, so THIS is where the
|
|
222
|
+
// parallel-request cap applies.
|
|
223
|
+
const requests = candidates.slice(0, maxObjectsQueried).map((obj) => Promise.resolve()
|
|
130
224
|
.then(() => dataSource.find(obj.name, {
|
|
131
225
|
$search: trimmed,
|
|
132
226
|
$top: topPerObject,
|
|
@@ -148,7 +242,7 @@ export function useRecordSearch(opts) {
|
|
|
148
242
|
// last real error so the UI can show a non-blocking hint.
|
|
149
243
|
const status = err?.httpStatus ?? err?.status;
|
|
150
244
|
const code = err?.code;
|
|
151
|
-
if (status === 404 ||
|
|
245
|
+
if (status === 404 || errorCodeIs(err, 'OBJECT_NOT_FOUND'))
|
|
152
246
|
continue;
|
|
153
247
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
154
248
|
continue;
|
|
@@ -193,6 +287,7 @@ export function useRecordSearch(opts) {
|
|
|
193
287
|
dataSource,
|
|
194
288
|
candidateSignature,
|
|
195
289
|
topPerObject,
|
|
290
|
+
maxObjectsQueried,
|
|
196
291
|
minLength,
|
|
197
292
|
debounceMs,
|
|
198
293
|
getDisplayName,
|
|
@@ -44,18 +44,26 @@ import { SchemaRendererContext } from '../context/SchemaRendererContext';
|
|
|
44
44
|
*/
|
|
45
45
|
export function useViewData(options) {
|
|
46
46
|
const { viewData, resource = '', params, dataSource: explicitDataSource, adapterOptions, skip = false, } = options;
|
|
47
|
-
// Get context DataSource
|
|
47
|
+
// Get context DataSource + host-authenticated fetch
|
|
48
48
|
const context = useContext(SchemaRendererContext);
|
|
49
49
|
const contextDataSource = context?.dataSource ?? null;
|
|
50
|
+
const contextApiFetch = context?.apiFetch;
|
|
50
51
|
// Resolve the DataSource — memoize to prevent re-creation on every render.
|
|
51
52
|
// We key on the viewData provider + config identity.
|
|
52
53
|
const resolvedDataSource = useMemo(() => {
|
|
53
54
|
if (explicitDataSource)
|
|
54
55
|
return explicitDataSource;
|
|
55
|
-
|
|
56
|
+
// Default the 'api' provider's fetch to the host's authenticated fetch
|
|
57
|
+
// (SchemaRendererContext.apiFetch) so custom endpoints carry the same
|
|
58
|
+
// credentials as native requests; explicit adapterOptions still win.
|
|
59
|
+
return resolveDataSource(viewData, contextDataSource, {
|
|
60
|
+
fetch: contextApiFetch,
|
|
61
|
+
...adapterOptions,
|
|
62
|
+
});
|
|
56
63
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
64
|
}, [
|
|
58
65
|
explicitDataSource,
|
|
66
|
+
contextApiFetch,
|
|
59
67
|
viewData?.provider,
|
|
60
68
|
// For 'api' provider, key on read/write URLs
|
|
61
69
|
viewData?.provider === 'api' ? viewData.read?.url : undefined,
|
package/dist/index.d.ts
CHANGED
|
@@ -12,4 +12,6 @@ export * from './LazyPluginLoader';
|
|
|
12
12
|
export * from './spec-bridge';
|
|
13
13
|
export * from './data-invalidation';
|
|
14
14
|
export { resolveI18nLabel } from './utils/i18n';
|
|
15
|
-
export {
|
|
15
|
+
export { extractWriteErrorMessage, isPermissionError, extractFieldErrors } from './utils/error-message';
|
|
16
|
+
export type { WriteFieldError } from './utils/error-message';
|
|
17
|
+
export { I18nProvider, useObjectTranslation, useSafeTranslate, useObjectLabel, useSafeFieldLabel, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, type I18nConfig, type I18nProviderProps, type TranslationKeys, type DateFormatOptions, type CurrencyFormatOptions, type NumberFormatOptions, } from '@object-ui/i18n';
|
package/dist/index.js
CHANGED
|
@@ -13,5 +13,8 @@ export * from './spec-bridge';
|
|
|
13
13
|
export * from './data-invalidation';
|
|
14
14
|
// i18n utilities
|
|
15
15
|
export { resolveI18nLabel } from './utils/i18n';
|
|
16
|
+
// Write-error surfacing utilities (shared by drag-write plugins so a failed
|
|
17
|
+
// PATCH — e.g. an RLS 403 — is never silently swallowed).
|
|
18
|
+
export { extractWriteErrorMessage, isPermissionError, extractFieldErrors } from './utils/error-message';
|
|
16
19
|
// Built-in i18n support
|
|
17
|
-
export { I18nProvider, useObjectTranslation, useObjectLabel, useSafeFieldLabel, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from '@object-ui/i18n';
|
|
20
|
+
export { I18nProvider, useObjectTranslation, useSafeTranslate, useObjectLabel, useSafeFieldLabel, useI18nContext, createI18n, getDirection, getAvailableLanguages, formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from '@object-ui/i18n';
|
|
@@ -18,9 +18,5 @@ export declare class SpecBridge {
|
|
|
18
18
|
transformListView(spec: any): SchemaNode;
|
|
19
19
|
/** Transform a FormView spec */
|
|
20
20
|
transformFormView(spec: any): SchemaNode;
|
|
21
|
-
/** Transform a Page spec */
|
|
22
|
-
transformPage(spec: any): SchemaNode;
|
|
23
|
-
/** Transform a Dashboard spec */
|
|
24
|
-
transformDashboard(spec: any): SchemaNode;
|
|
25
21
|
updateContext(ctx: Partial<BridgeContext>): void;
|
|
26
22
|
}
|
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { bridgeListView } from './bridges/list-view';
|
|
9
9
|
import { bridgeFormView } from './bridges/form-view';
|
|
10
|
-
import { bridgePage } from './bridges/page';
|
|
11
|
-
import { bridgeDashboard } from './bridges/dashboard';
|
|
12
10
|
export class SpecBridge {
|
|
13
11
|
constructor(context = {}) {
|
|
14
12
|
Object.defineProperty(this, "bridges", {
|
|
@@ -26,8 +24,6 @@ export class SpecBridge {
|
|
|
26
24
|
this.context = context;
|
|
27
25
|
this.register('list', bridgeListView);
|
|
28
26
|
this.register('form', bridgeFormView);
|
|
29
|
-
this.register('page', bridgePage);
|
|
30
|
-
this.register('dashboard', bridgeDashboard);
|
|
31
27
|
}
|
|
32
28
|
register(specType, bridge) {
|
|
33
29
|
this.bridges.set(specType, bridge);
|
|
@@ -48,14 +44,6 @@ export class SpecBridge {
|
|
|
48
44
|
transformFormView(spec) {
|
|
49
45
|
return this.transform('form', spec);
|
|
50
46
|
}
|
|
51
|
-
/** Transform a Page spec */
|
|
52
|
-
transformPage(spec) {
|
|
53
|
-
return this.transform('page', spec);
|
|
54
|
-
}
|
|
55
|
-
/** Transform a Dashboard spec */
|
|
56
|
-
transformDashboard(spec) {
|
|
57
|
-
return this.transform('dashboard', spec);
|
|
58
|
-
}
|
|
59
47
|
updateContext(ctx) {
|
|
60
48
|
this.context = { ...this.context, ...ctx };
|
|
61
49
|
}
|
|
@@ -134,17 +134,11 @@ export const bridgeListView = (spec, _context) => {
|
|
|
134
134
|
// P1.6 — i18n & ARIA
|
|
135
135
|
if (spec.aria)
|
|
136
136
|
node.aria = spec.aria;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
if (sharing.type && sharing.enabled == null) {
|
|
144
|
-
sharing.enabled = true;
|
|
145
|
-
}
|
|
146
|
-
node.sharing = sharing;
|
|
147
|
-
}
|
|
137
|
+
// `sharing` is already the spec's `ViewSharing` shape on both sides (#2890) —
|
|
138
|
+
// this used to DOWNGRADE it here, inventing a legacy `visibility` audience and
|
|
139
|
+
// an `enabled` flag that the renderer then had to fold back.
|
|
140
|
+
if (spec.sharing)
|
|
141
|
+
node.sharing = spec.sharing;
|
|
148
142
|
if (spec.hiddenFields)
|
|
149
143
|
node.hiddenFields = spec.hiddenFields;
|
|
150
144
|
if (spec.fieldOrder)
|
|
@@ -9,5 +9,3 @@ export { SpecBridge } from './SpecBridge';
|
|
|
9
9
|
export type { BridgeContext, BridgeFn, ObjectDefLite } from './types';
|
|
10
10
|
export { bridgeListView } from './bridges/list-view';
|
|
11
11
|
export { bridgeFormView } from './bridges/form-view';
|
|
12
|
-
export { bridgePage } from './bridges/page';
|
|
13
|
-
export { bridgeDashboard } from './bridges/dashboard';
|