@object-ui/react 16.1.0 → 17.2.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 +790 -1
- package/README.md +41 -0
- package/dist/SchemaRenderer.js +12 -2
- package/dist/context/AppShellContext.js +51 -23
- package/dist/context/InlineEditContext.d.ts +64 -0
- package/dist/context/InlineEditContext.js +8 -2
- package/dist/context/NotificationContext.d.ts +245 -17
- package/dist/context/NotificationContext.js +222 -20
- 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/useFilterScope.d.ts +30 -0
- package/dist/hooks/useFilterScope.js +55 -0
- package/dist/hooks/useNavigationOverlay.d.ts +34 -10
- package/dist/hooks/useNavigationOverlay.js +35 -1
- package/dist/hooks/useOffline.d.ts +51 -33
- package/dist/hooks/useOffline.js +3 -3
- package/dist/hooks/usePageVariables.js +23 -11
- package/dist/hooks/usePerformance.d.ts +3 -3
- package/dist/hooks/usePerformance.js +2 -2
- package/dist/hooks/useRecordSearch.js +98 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -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/spec-input.d.ts +37 -0
- package/dist/spec-input.js +8 -0
- package/dist/utils/error-message.d.ts +31 -0
- package/dist/utils/error-message.js +49 -0
- package/package.json +8 -8
- 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,
|
|
@@ -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
|
+
}
|
|
@@ -5,18 +5,42 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
import type { NavigationConfigSchema } from '@objectstack/spec/ui';
|
|
9
|
+
import type { SpecAuthoredInput } from '../spec-input';
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* The spec's `NavigationConfigSchema`, authoring side, with `mode` required.
|
|
12
|
+
*
|
|
13
|
+
* This was a hand copy carrying the note "inline … to avoid importing from
|
|
14
|
+
* @object-ui/types (which may not be a direct dependency of @object-ui/react)".
|
|
15
|
+
* That reasoning is about the wrong package: the vocabulary belongs to
|
|
16
|
+
* `@objectstack/spec`, which IS a direct dependency of this one — the same
|
|
17
|
+
* expired "kept local to avoid a dependency" comment objectui#3169 found in
|
|
18
|
+
* `@object-ui/app-shell`, where the dependency had likewise been there all
|
|
19
|
+
* along. Check `package.json` before believing such a note (objectstack#4115).
|
|
20
|
+
*
|
|
21
|
+
* The ONE divergence: the spec defaults `mode`, so its authoring side makes it
|
|
22
|
+
* optional; this hook dispatches on `mode` and its callers always supply one,
|
|
23
|
+
* so it is required here. Every other key is the spec's, by reference. Pinned
|
|
24
|
+
* by `__tests__/offline-nav-performance-spec-parity.test.ts`.
|
|
25
|
+
*
|
|
26
|
+
* Two per-key notes the hand copy carried, kept here because a derived alias
|
|
27
|
+
* has no members to hang them on:
|
|
28
|
+
* - `size` is the coarse overlay bucket added by #2578; `resolveOverlayWidth`
|
|
29
|
+
* below maps it through {@link OVERLAY_SIZE_WIDTHS}.
|
|
30
|
+
* - `width` is DEPRECATED by #2578 in favour of `size`. It still wins when
|
|
31
|
+
* present, because app-shell pre-resolves `size` into it.
|
|
12
32
|
*/
|
|
13
|
-
export
|
|
14
|
-
mode:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
33
|
+
export type NavigationConfig = Omit<SpecAuthoredInput<typeof NavigationConfigSchema>, 'mode'> & {
|
|
34
|
+
mode: NonNullable<SpecAuthoredInput<typeof NavigationConfigSchema>['mode']>;
|
|
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 declare function resolveOverlayWidth(navigation: NavigationConfig | undefined): string | number | undefined;
|
|
20
44
|
export type NavigationMode = NavigationConfig['mode'];
|
|
21
45
|
export interface UseNavigationOverlayOptions {
|
|
22
46
|
/** 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(() => {
|
|
@@ -5,14 +5,25 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
import type { OfflineStrategy, ConflictResolution, PersistStorage, EvictionPolicy, OfflineCacheConfigSchema, OfflineConfigSchema, SyncConfigSchema } from '@objectstack/spec/ui';
|
|
9
|
+
import type { SpecAuthoredInput } from '../spec-input';
|
|
8
10
|
/** Offline strategy determines how data is fetched when connectivity is limited. */
|
|
9
|
-
export type OfflineStrategy
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
export type { OfflineStrategy };
|
|
12
|
+
/**
|
|
13
|
+
* Conflict resolution strategy for sync operations.
|
|
14
|
+
*
|
|
15
|
+
* Renamed from `ConflictResolutionStrategy` — that name belongs to a DIFFERENT
|
|
16
|
+
* spec export (`@objectstack/spec/api`: `error | priority | first-wins |
|
|
17
|
+
* last-wins`, the metadata-merge policy). The spec's name for the union this
|
|
18
|
+
* hook actually uses is `ConflictResolution`, so the fix was to take the spec's
|
|
19
|
+
* own name and its binding at once, the same move `FieldGroup` →
|
|
20
|
+
* `ObjectFieldGroup` made in objectui#3169.
|
|
21
|
+
*/
|
|
22
|
+
export type { ConflictResolution };
|
|
12
23
|
/** Persist storage backend. */
|
|
13
|
-
export type PersistStorageType =
|
|
24
|
+
export type PersistStorageType = PersistStorage;
|
|
14
25
|
/** Eviction policy for cache management. */
|
|
15
|
-
export type EvictionPolicyType =
|
|
26
|
+
export type EvictionPolicyType = EvictionPolicy;
|
|
16
27
|
/** Sync state of the offline system. */
|
|
17
28
|
export type SyncState = 'idle' | 'syncing' | 'error' | 'offline';
|
|
18
29
|
/** A queued mutation waiting to be synced. */
|
|
@@ -23,31 +34,38 @@ export interface QueuedMutation {
|
|
|
23
34
|
resource: string;
|
|
24
35
|
data?: Record<string, unknown>;
|
|
25
36
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Cache configuration — the AUTHORING side of the spec's
|
|
39
|
+
* `OfflineCacheConfigSchema`.
|
|
40
|
+
*
|
|
41
|
+
* `persistStorage` and `evictionPolicy` carry `.default()`s, so they are
|
|
42
|
+
* optional to write and present after a parse. This hook is handed what an app
|
|
43
|
+
* author wrote, hence the input side; see {@link SpecAuthoredInput}.
|
|
44
|
+
*/
|
|
45
|
+
export type OfflineCacheConfig = SpecAuthoredInput<typeof OfflineCacheConfigSchema>;
|
|
46
|
+
/**
|
|
47
|
+
* Sync configuration — the AUTHORING side of the spec's `SyncConfigSchema`.
|
|
48
|
+
*
|
|
49
|
+
* `strategy` and `conflictResolution` carry `.default()`s, so the output type
|
|
50
|
+
* makes them required; this hook is handed what an app author wrote.
|
|
51
|
+
*/
|
|
52
|
+
export type OfflineSyncConfig = SpecAuthoredInput<typeof SyncConfigSchema>;
|
|
53
|
+
/**
|
|
54
|
+
* Top-level offline configuration — the AUTHORING side of the spec's
|
|
55
|
+
* `OfflineConfigSchema`.
|
|
56
|
+
*
|
|
57
|
+
* This name stays with `@object-ui/react` (objectui#3156 / objectui#3159): the
|
|
58
|
+
* `OfflineConfig` that used to sit in `@object-ui/types` was a service-worker
|
|
59
|
+
* ROUTE cache and has been renamed `PWAOfflineConfig`, so there is no
|
|
60
|
+
* cross-package clash left — this hook's config is the spec's concept, key for
|
|
61
|
+
* key, and takes the spec's binding.
|
|
62
|
+
*
|
|
63
|
+
* Input side, not `z.infer`, and here the reason is visible in the signature:
|
|
64
|
+
* `useOffline(config: OfflineConfig = {})` defaults to the empty object, which
|
|
65
|
+
* the output type — where `enabled`, `strategy` and `offlineIndicator` are all
|
|
66
|
+
* required once their `.default()`s have run — would reject outright.
|
|
67
|
+
*/
|
|
68
|
+
export type OfflineConfig = SpecAuthoredInput<typeof OfflineConfigSchema>;
|
|
51
69
|
/** Result returned by the useOffline hook. */
|
|
52
70
|
export interface OfflineResult {
|
|
53
71
|
/** Whether the browser is currently online. */
|
|
@@ -72,9 +90,9 @@ export interface OfflineResult {
|
|
|
72
90
|
offlineMessage: string;
|
|
73
91
|
}
|
|
74
92
|
/**
|
|
75
|
-
* Hook for offline mode detection and sync queue management.
|
|
76
|
-
*
|
|
77
|
-
*
|
|
93
|
+
* Hook for offline mode detection and sync queue management. Its config types
|
|
94
|
+
* ARE the spec's `OfflineConfigSchema` / `SyncConfigSchema` / `ConflictResolution`
|
|
95
|
+
* (authoring side) — see {@link OfflineConfig}.
|
|
78
96
|
*
|
|
79
97
|
* @example
|
|
80
98
|
* ```tsx
|
package/dist/hooks/useOffline.js
CHANGED
|
@@ -66,9 +66,9 @@ const DEFAULTS = {
|
|
|
66
66
|
queueMaxSize: 100,
|
|
67
67
|
};
|
|
68
68
|
/**
|
|
69
|
-
* Hook for offline mode detection and sync queue management.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
* Hook for offline mode detection and sync queue management. Its config types
|
|
70
|
+
* ARE the spec's `OfflineConfigSchema` / `SyncConfigSchema` / `ConflictResolution`
|
|
71
|
+
* (authoring side) — see {@link OfflineConfig}.
|
|
72
72
|
*
|
|
73
73
|
* @example
|
|
74
74
|
* ```tsx
|
|
@@ -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
|
*/
|
|
@@ -13,7 +13,7 @@ export interface VirtualScrollConfig {
|
|
|
13
13
|
itemHeight?: number;
|
|
14
14
|
overscan?: number;
|
|
15
15
|
}
|
|
16
|
-
/** Performance configuration
|
|
16
|
+
/** Performance configuration. */
|
|
17
17
|
export interface PerformanceConfig {
|
|
18
18
|
/** Whether to lazy-load components/data. */
|
|
19
19
|
lazyLoad?: boolean;
|
|
@@ -55,8 +55,8 @@ export interface PerformanceResult {
|
|
|
55
55
|
debounce: <T extends (...args: unknown[]) => void>(fn: T) => T;
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* Hook for performance monitoring and configuration
|
|
59
|
-
* PerformanceConfigSchema
|
|
58
|
+
* Hook for performance monitoring and configuration. Its config type IS the
|
|
59
|
+
* spec's `PerformanceConfigSchema` (authoring side) — see {@link PerformanceConfig}.
|
|
60
60
|
*
|
|
61
61
|
* Provides resolved config values, Web Vitals metrics, and utility functions
|
|
62
62
|
* (debounce, render marking) for performance-aware components.
|
|
@@ -52,8 +52,8 @@ function getLCP() {
|
|
|
52
52
|
// usePerformance hook
|
|
53
53
|
// ---------------------------------------------------------------------------
|
|
54
54
|
/**
|
|
55
|
-
* Hook for performance monitoring and configuration
|
|
56
|
-
* PerformanceConfigSchema
|
|
55
|
+
* Hook for performance monitoring and configuration. Its config type IS the
|
|
56
|
+
* spec's `PerformanceConfigSchema` (authoring side) — see {@link PerformanceConfig}.
|
|
57
57
|
*
|
|
58
58
|
* Provides resolved config values, Web Vitals metrics, and utility functions
|
|
59
59
|
* (debounce, render marking) for performance-aware components.
|
|
@@ -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,6 +81,15 @@ 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 [];
|
|
@@ -99,8 +109,8 @@ export function useRecordSearch(opts) {
|
|
|
99
109
|
else {
|
|
100
110
|
pool = objects.filter((o) => o?.searchable !== false);
|
|
101
111
|
}
|
|
102
|
-
return pool
|
|
103
|
-
}, [objects, objectNames
|
|
112
|
+
return pool;
|
|
113
|
+
}, [objects, objectNames]);
|
|
104
114
|
const candidateSignature = useMemo(() => {
|
|
105
115
|
return candidates.map((o) => `${o?.name}:${o?.titleField ?? ''}`).join('|');
|
|
106
116
|
}, [candidates]);
|
|
@@ -127,7 +137,90 @@ export function useRecordSearch(opts) {
|
|
|
127
137
|
// Skip if a newer run started during the debounce window.
|
|
128
138
|
if (runIdRef.current !== myRunId)
|
|
129
139
|
return;
|
|
130
|
-
|
|
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()
|
|
131
224
|
.then(() => dataSource.find(obj.name, {
|
|
132
225
|
$search: trimmed,
|
|
133
226
|
$top: topPerObject,
|
|
@@ -149,7 +242,7 @@ export function useRecordSearch(opts) {
|
|
|
149
242
|
// last real error so the UI can show a non-blocking hint.
|
|
150
243
|
const status = err?.httpStatus ?? err?.status;
|
|
151
244
|
const code = err?.code;
|
|
152
|
-
if (status === 404 ||
|
|
245
|
+
if (status === 404 || errorCodeIs(err, 'OBJECT_NOT_FOUND'))
|
|
153
246
|
continue;
|
|
154
247
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
155
248
|
continue;
|
|
@@ -194,6 +287,7 @@ export function useRecordSearch(opts) {
|
|
|
194
287
|
dataSource,
|
|
195
288
|
candidateSignature,
|
|
196
289
|
topPerObject,
|
|
290
|
+
maxObjectsQueried,
|
|
197
291
|
minLength,
|
|
198
292
|
debounceMs,
|
|
199
293
|
getDisplayName,
|
package/dist/index.d.ts
CHANGED
|
@@ -12,5 +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 { extractWriteErrorMessage, isPermissionError } from './utils/error-message';
|
|
15
|
+
export { extractWriteErrorMessage, isPermissionError, extractFieldErrors } from './utils/error-message';
|
|
16
|
+
export type { WriteFieldError } from './utils/error-message';
|
|
16
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
|
@@ -15,6 +15,6 @@ export * from './data-invalidation';
|
|
|
15
15
|
export { resolveI18nLabel } from './utils/i18n';
|
|
16
16
|
// Write-error surfacing utilities (shared by drag-write plugins so a failed
|
|
17
17
|
// PATCH — e.g. an RLS 403 — is never silently swallowed).
|
|
18
|
-
export { extractWriteErrorMessage, isPermissionError } from './utils/error-message';
|
|
18
|
+
export { extractWriteErrorMessage, isPermissionError, extractFieldErrors } from './utils/error-message';
|
|
19
19
|
// Built-in i18n support
|
|
20
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
|
}
|