@blocksdiy/blocks-client-sdk 1.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/README.md +1 -0
- package/dist/Action.d.ts +142 -0
- package/dist/Action.d.ts.map +1 -0
- package/dist/Action.js +143 -0
- package/dist/Action.js.map +1 -0
- package/dist/AgentChat.d.ts +42 -0
- package/dist/AgentChat.d.ts.map +1 -0
- package/dist/AgentChat.js +34 -0
- package/dist/AgentChat.js.map +1 -0
- package/dist/ClientSdk.d.ts +309 -0
- package/dist/ClientSdk.d.ts.map +1 -0
- package/dist/ClientSdk.js +396 -0
- package/dist/ClientSdk.js.map +1 -0
- package/dist/Entity.d.ts +299 -0
- package/dist/Entity.d.ts.map +1 -0
- package/dist/Entity.js +329 -0
- package/dist/Entity.js.map +1 -0
- package/dist/Page.d.ts +56 -0
- package/dist/Page.d.ts.map +1 -0
- package/dist/Page.js +52 -0
- package/dist/Page.js.map +1 -0
- package/dist/ReactClientSdk.d.ts +886 -0
- package/dist/ReactClientSdk.d.ts.map +1 -0
- package/dist/ReactClientSdk.jsx +1238 -0
- package/dist/ReactClientSdk.jsx.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1238 @@
|
|
|
1
|
+
import { FatalAppError } from "@blockscom/blocks-client-api/errors";
|
|
2
|
+
import { useWebsockets, useWebsocketsAppSubscribe } from "@blockscom/blocks-client-api/websocketService";
|
|
3
|
+
import { websocketsService } from "@blockscom/blocks-client-api/websocketService";
|
|
4
|
+
import { useAiStreamChunks } from "@blockscom/react-common/useAiStreamChunks";
|
|
5
|
+
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
7
|
+
import { ClientSdk } from "./ClientSdk.js";
|
|
8
|
+
export class ReactClientSdk extends ClientSdk {
|
|
9
|
+
}
|
|
10
|
+
const ENTITIES_QUERY_KEY = "entities";
|
|
11
|
+
const CURRENT_USER_QUERY_KEY = "currentUser";
|
|
12
|
+
const ClientContext = createContext(null);
|
|
13
|
+
const ThemeModeContext = createContext({
|
|
14
|
+
themeMode: "system",
|
|
15
|
+
setThemeMode: () => { },
|
|
16
|
+
});
|
|
17
|
+
const EXECUTE_ACTION_WINDOWS = [{ duration: 20000, maxCount: 5, withData: true }];
|
|
18
|
+
const ENTITY_OPERATION_WINDOWS = [{ duration: 5000, maxCount: 5, withData: true }];
|
|
19
|
+
const ENTITY_ROW_OPERATION_WINDOWS = [
|
|
20
|
+
{ duration: 5000, maxCount: 5, withData: true },
|
|
21
|
+
{ duration: 10000, maxCount: 15, withData: false },
|
|
22
|
+
];
|
|
23
|
+
/* public decouple - blocks common */
|
|
24
|
+
const USERS_TABLE_BLOCK_ID = "68760b42d4ce152c91ce0e1c";
|
|
25
|
+
export var AppDataEventTypes;
|
|
26
|
+
(function (AppDataEventTypes) {
|
|
27
|
+
AppDataEventTypes["APP_DATA_INSERT"] = "APP_DATA_INSERT";
|
|
28
|
+
AppDataEventTypes["APP_DATA_UPDATE"] = "APP_DATA_UPDATE";
|
|
29
|
+
AppDataEventTypes["APP_DATA_DELETE"] = "APP_DATA_DELETE";
|
|
30
|
+
})(AppDataEventTypes || (AppDataEventTypes = {}));
|
|
31
|
+
/* ends of blocks common */
|
|
32
|
+
const useComponentName = (functionName) => {
|
|
33
|
+
const componentName = useMemo(() => {
|
|
34
|
+
try {
|
|
35
|
+
const stack = new Error().stack || "";
|
|
36
|
+
const lines = stack.split("\n");
|
|
37
|
+
// Find the line containing the function name and get the line above it
|
|
38
|
+
const functionNameIndex = lines.findIndex((line) => line.includes(functionName));
|
|
39
|
+
if (functionNameIndex > 0) {
|
|
40
|
+
const callerLine = lines[functionNameIndex + 1];
|
|
41
|
+
const fatherComponent = callerLine.split("at ")[1];
|
|
42
|
+
if (fatherComponent) {
|
|
43
|
+
return fatherComponent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return "Unknown";
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return "Unknown";
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
return componentName;
|
|
53
|
+
};
|
|
54
|
+
const useActionRateLimiting = (actionConfig, functionName) => {
|
|
55
|
+
const componentName = useComponentName(functionName);
|
|
56
|
+
const queryClient = useQueryClient();
|
|
57
|
+
const validateExceededMaxCount = (inputs) => {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
for (let windowIndex = 0; windowIndex < EXECUTE_ACTION_WINDOWS.length; windowIndex++) {
|
|
60
|
+
const executeActionWindow = EXECUTE_ACTION_WINDOWS[windowIndex];
|
|
61
|
+
// Determine the query key based on whether we're checking with or without inputs
|
|
62
|
+
const queryKey = executeActionWindow.withData
|
|
63
|
+
? [`action-result-${windowIndex}`, actionConfig.actionBlockId, inputs]
|
|
64
|
+
: [`action-result-${windowIndex}`, actionConfig.actionBlockId];
|
|
65
|
+
const executeActionHistory = queryClient.getQueryData(queryKey) || [];
|
|
66
|
+
// Filter history within the current time window
|
|
67
|
+
const executeActionHistoryInWindow = executeActionHistory.filter((history) => history.timestamp > now - executeActionWindow.duration);
|
|
68
|
+
// Check if rate limit is exceeded for this window
|
|
69
|
+
if (executeActionHistoryInWindow.length >= executeActionWindow.maxCount) {
|
|
70
|
+
const errorMessage = executeActionWindow.withData
|
|
71
|
+
? `Executed action with the same parameters (${JSON.stringify(inputs)}) too many times (${executeActionWindow.maxCount} times in ${executeActionWindow.duration / 1000}s) - actionBlockId: "${actionConfig.actionBlockId}", from component: ${componentName}`
|
|
72
|
+
: `Executed action too many times (${executeActionWindow.maxCount} times in ${executeActionWindow.duration / 1000}s) - actionBlockId: "${actionConfig.actionBlockId}", from component: ${componentName}`;
|
|
73
|
+
throw new FatalAppError(errorMessage);
|
|
74
|
+
}
|
|
75
|
+
// Update the history for this window
|
|
76
|
+
queryClient.setQueryData(queryKey, [...executeActionHistoryInWindow, { timestamp: now }]);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
return { validateExceededMaxCount };
|
|
80
|
+
};
|
|
81
|
+
const useEntityMutationRateLimiting = (entityConfig, functionName) => {
|
|
82
|
+
const componentName = useComponentName(functionName);
|
|
83
|
+
const queryClient = useQueryClient();
|
|
84
|
+
const validateExceededMaxCount = ({ data }) => {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
for (let windowIndex = 0; windowIndex < ENTITY_OPERATION_WINDOWS.length; windowIndex++) {
|
|
87
|
+
const entityOperationWindow = ENTITY_OPERATION_WINDOWS[windowIndex];
|
|
88
|
+
const queryKey = entityOperationWindow.withData
|
|
89
|
+
? [`entity-operation-${windowIndex}`, entityConfig.tableBlockId, data]
|
|
90
|
+
: [`entity-operation-${windowIndex}`, entityConfig.tableBlockId];
|
|
91
|
+
const entityOperationHistory = queryClient.getQueryData(queryKey) || [];
|
|
92
|
+
// Filter history within the current time window
|
|
93
|
+
const entityOperationHistoryInWindow = entityOperationHistory.filter((history) => history.timestamp > now - entityOperationWindow.duration);
|
|
94
|
+
// Check if rate limit is exceeded for this window
|
|
95
|
+
if (entityOperationHistoryInWindow.length >= entityOperationWindow.maxCount) {
|
|
96
|
+
const errorMessage = `Too many entity operations (${entityOperationWindow.maxCount} times in ${entityOperationWindow.duration / 1000}s) - tableBlockId: "${entityConfig.tableBlockId}"${data && entityOperationWindow.withData ? `, data: ${JSON.stringify(data)}` : ""}, from component: ${componentName}`;
|
|
97
|
+
throw new FatalAppError(errorMessage);
|
|
98
|
+
}
|
|
99
|
+
// Update the history for this window
|
|
100
|
+
queryClient.setQueryData(queryKey, [...entityOperationHistoryInWindow, { timestamp: now }]);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return { validateExceededMaxCount };
|
|
104
|
+
};
|
|
105
|
+
const useEntityRowMutationRateLimiting = (entityConfig, functionName) => {
|
|
106
|
+
const componentName = useComponentName(functionName);
|
|
107
|
+
const queryClient = useQueryClient();
|
|
108
|
+
const validateExceededMaxCount = ({ rowId, data }) => {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
for (let windowIndex = 0; windowIndex < ENTITY_ROW_OPERATION_WINDOWS.length; windowIndex++) {
|
|
111
|
+
const entityOperationWindow = ENTITY_ROW_OPERATION_WINDOWS[windowIndex];
|
|
112
|
+
const queryKey = entityOperationWindow.withData
|
|
113
|
+
? [`entity-row-operation-${windowIndex}`, entityConfig.tableBlockId, rowId, data]
|
|
114
|
+
: [`entity-row-operation-${windowIndex}`, entityConfig.tableBlockId, rowId];
|
|
115
|
+
const entityOperationHistory = queryClient.getQueryData(queryKey) || [];
|
|
116
|
+
// Filter history within the current time window
|
|
117
|
+
const entityOperationHistoryInWindow = entityOperationHistory.filter((history) => history.timestamp > now - entityOperationWindow.duration);
|
|
118
|
+
// Check if rate limit is exceeded for this window
|
|
119
|
+
if (entityOperationHistoryInWindow.length >= entityOperationWindow.maxCount) {
|
|
120
|
+
const errorMessage = `Too many entity row operations (${entityOperationWindow.maxCount} times in ${entityOperationWindow.duration / 1000}s) - tableBlockId: "${entityConfig.tableBlockId}", rowId: "${rowId}"${data && entityOperationWindow.withData ? `, data: ${JSON.stringify(data)}` : ""}, from component: ${componentName}`;
|
|
121
|
+
throw new FatalAppError(errorMessage);
|
|
122
|
+
}
|
|
123
|
+
// Update the history for this window
|
|
124
|
+
queryClient.setQueryData(queryKey, [...entityOperationHistoryInWindow, { timestamp: now }]);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
return { validateExceededMaxCount };
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Provides ClientSdk instance to React component tree
|
|
131
|
+
*
|
|
132
|
+
* This component sets up the necessary context providers for both the ClientSdk
|
|
133
|
+
* and React Query, enabling all the hooks in this module to work properly.
|
|
134
|
+
*
|
|
135
|
+
* @param {Object} props - Component props
|
|
136
|
+
* @param {ClientSdk} props.client - ClientSdk instance to provide
|
|
137
|
+
* @param {React.ReactNode} props.children - Child components
|
|
138
|
+
* @param {ThemeMode} [props.themeMode] - (Optional) Theme mode
|
|
139
|
+
* @param {Function} [props.setThemeMode] - (Optional) Function to set the theme mode
|
|
140
|
+
* @returns {JSX.Element} Provider component
|
|
141
|
+
* @example
|
|
142
|
+
* ```tsx
|
|
143
|
+
* const client = new ClientSdk({ appId: 'my-app-id', user: {
|
|
144
|
+
* email: 'test@test.com',
|
|
145
|
+
* firstName: 'Test',
|
|
146
|
+
* lastName: 'User',
|
|
147
|
+
* isAuthenticated: true,
|
|
148
|
+
* } });
|
|
149
|
+
*
|
|
150
|
+
* function App() {
|
|
151
|
+
* return (
|
|
152
|
+
* <ClientProvider client={client}>
|
|
153
|
+
* <YourAppComponents />
|
|
154
|
+
* </ClientProvider>
|
|
155
|
+
* );
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export const ClientProvider = ({ client, children, themeMode, setThemeMode, }) => {
|
|
160
|
+
const [queryClient] = useState(() => new QueryClient());
|
|
161
|
+
const user = client.getUser();
|
|
162
|
+
const userIdInNumber = useMemo(() => {
|
|
163
|
+
if (user?.id) {
|
|
164
|
+
const num = Number(user.id);
|
|
165
|
+
if (isNaN(num)) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
return num;
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}, [user?.id]);
|
|
172
|
+
useWebsockets({ userId: userIdInNumber, token: client.token });
|
|
173
|
+
useWebsocketsAppSubscribe({ appId: client.appId });
|
|
174
|
+
const dataChangeCallback = useCallback(async (data) => {
|
|
175
|
+
queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, data.table] });
|
|
176
|
+
data.affectRows?.forEach((row) => {
|
|
177
|
+
queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, data.table, "one", { id: row.id }] });
|
|
178
|
+
});
|
|
179
|
+
}, [queryClient]);
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
websocketsService.listen(AppDataEventTypes.APP_DATA_INSERT, dataChangeCallback);
|
|
182
|
+
websocketsService.listen(AppDataEventTypes.APP_DATA_UPDATE, dataChangeCallback);
|
|
183
|
+
websocketsService.listen(AppDataEventTypes.APP_DATA_DELETE, dataChangeCallback);
|
|
184
|
+
return () => {
|
|
185
|
+
websocketsService.unlisten(AppDataEventTypes.APP_DATA_INSERT, dataChangeCallback);
|
|
186
|
+
websocketsService.unlisten(AppDataEventTypes.APP_DATA_UPDATE, dataChangeCallback);
|
|
187
|
+
websocketsService.unlisten(AppDataEventTypes.APP_DATA_DELETE, dataChangeCallback);
|
|
188
|
+
};
|
|
189
|
+
}, [dataChangeCallback]);
|
|
190
|
+
return (<ClientContext value={client}>
|
|
191
|
+
<ThemeModeContext value={{ themeMode, setThemeMode }}>
|
|
192
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
193
|
+
</ThemeModeContext>
|
|
194
|
+
</ClientContext>);
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Hook to access the ClientSdk instance from context
|
|
198
|
+
*
|
|
199
|
+
* This hook provides direct access to the ClientSdk, which is the core client
|
|
200
|
+
* for interacting with entities, actions, and pages.
|
|
201
|
+
*
|
|
202
|
+
* @returns {ClientSdk} The ClientSdk instance
|
|
203
|
+
* @throws {Error} If used outside of ClientProvider
|
|
204
|
+
* @example
|
|
205
|
+
* ```tsx
|
|
206
|
+
* function MyComponent() {
|
|
207
|
+
* const client = useClient();
|
|
208
|
+
* // Use client directly for advanced use cases
|
|
209
|
+
* const userEntity = client.entity(userConfig);
|
|
210
|
+
* }
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export const useClient = () => {
|
|
214
|
+
const client = useContext(ClientContext);
|
|
215
|
+
if (!client) {
|
|
216
|
+
throw new Error("Client not found");
|
|
217
|
+
}
|
|
218
|
+
return client;
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Hook to fetch all entities of a specific type
|
|
222
|
+
*
|
|
223
|
+
* Entities represent data objects stored in tables with standard CRUD operations.
|
|
224
|
+
* This hook provides a React Query-powered way to fetch multiple entities.
|
|
225
|
+
*
|
|
226
|
+
* EntityConfig properties:
|
|
227
|
+
* - tableBlockId: Unique identifier for the table
|
|
228
|
+
* - instanceType: TypeScript type defining the entity's structure
|
|
229
|
+
*
|
|
230
|
+
* @template E - Entity configuration type
|
|
231
|
+
* @param {E} entityConfig - Configuration for the entity type
|
|
232
|
+
* @param {any} [filters] - Optional filters to apply to the query
|
|
233
|
+
* @param {Object} [queryOptions] - Optional React Query configuration options
|
|
234
|
+
* @param {boolean} [queryOptions.enabled] - Whether the query should automatically execute
|
|
235
|
+
* @param {EntityType<E>[]} [queryOptions.initialData] - Initial data to use for the query
|
|
236
|
+
* @param {EntityType<E>[]} [queryOptions.placeholderData] - Placeholder data to use while loading
|
|
237
|
+
* @returns {Object} Query result with data, loading state, and error
|
|
238
|
+
* @returns {EntityType<E>[] | undefined} returns.data - The fetched entities
|
|
239
|
+
* @returns {boolean} returns.isLoading - Whether the query is loading
|
|
240
|
+
* @returns {Error | null} returns.error - Any error that occurred
|
|
241
|
+
* @returns {Function} returns.refetch - Function to manually refetch the data
|
|
242
|
+
* @returns {boolean} returns.isError - Whether the query resulted in an error
|
|
243
|
+
* @returns {boolean} returns.isFetched - Whether the query has been fetched
|
|
244
|
+
* @returns {boolean} returns.isFetching - Whether the query is currently fetching
|
|
245
|
+
* @returns {boolean} returns.isSuccess - Whether the query was successful
|
|
246
|
+
* @returns {string} returns.status - Current status of the query: 'idle', 'loading', 'success', 'error', or 'pending'
|
|
247
|
+
* @example
|
|
248
|
+
* ```tsx
|
|
249
|
+
* // Define a Item entity configuration
|
|
250
|
+
* const itemConfig = {
|
|
251
|
+
* tableBlockId: 'items-table',
|
|
252
|
+
* instanceType: {} as {
|
|
253
|
+
* name: string;
|
|
254
|
+
* price: number;
|
|
255
|
+
* }
|
|
256
|
+
* };
|
|
257
|
+
*
|
|
258
|
+
* function ItemsList() {
|
|
259
|
+
* const { data: items, isLoading } = useEntityGetAll(itemConfig);
|
|
260
|
+
*
|
|
261
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
262
|
+
* return (
|
|
263
|
+
* <ul>
|
|
264
|
+
* {items?.map(item => <li key={item.id}>{item.name}</li>)}
|
|
265
|
+
* </ul>
|
|
266
|
+
* );
|
|
267
|
+
* }
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
export const useEntityGetAll = (entityConfig, filters, queryOptions = {}) => {
|
|
271
|
+
const { enabled, initialData, placeholderData } = queryOptions;
|
|
272
|
+
const client = useClient();
|
|
273
|
+
const { data, isLoading, error, isError, isFetched, isFetching, isSuccess, status } = useQuery({
|
|
274
|
+
queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, filters],
|
|
275
|
+
queryFn: () => client.entity(entityConfig).findMany(filters),
|
|
276
|
+
enabled,
|
|
277
|
+
initialData,
|
|
278
|
+
placeholderData,
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
data,
|
|
282
|
+
isLoading,
|
|
283
|
+
error,
|
|
284
|
+
refetch: () => ({ data, isLoading, error, isError, isFetched, isFetching, isSuccess, status }),
|
|
285
|
+
isError,
|
|
286
|
+
isFetched,
|
|
287
|
+
isFetching,
|
|
288
|
+
isSuccess,
|
|
289
|
+
status,
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* Hook to fetch a single entity by filters
|
|
294
|
+
*
|
|
295
|
+
* Retrieves a specific entity instance using filter criteria.
|
|
296
|
+
* EntityConfig defines the structure and mapping of the entity data.
|
|
297
|
+
*
|
|
298
|
+
* @template EC - Entity configuration type
|
|
299
|
+
* @param {EC} entityConfig - Configuration for the entity type
|
|
300
|
+
* @param {Record<string, any>} filters - Filters to identify the entity (e.g., { id: "123" } or { email: "user@example.com" })
|
|
301
|
+
* @param {Object} [queryOptions] - Optional React Query configuration options
|
|
302
|
+
* @param {boolean} [queryOptions.enabled] - Whether the query should automatically execute
|
|
303
|
+
* @param {EntityType<EC> | null} [queryOptions.initialData] - Initial data to use for the query
|
|
304
|
+
* @returns {Object} Query result with data, loading state, and error
|
|
305
|
+
* @returns {EntityType<EC> | undefined | null} returns.data - The fetched entity
|
|
306
|
+
* @returns {boolean} returns.isLoading - Whether the query is loading
|
|
307
|
+
* @returns {Error | null} returns.error - Any error that occurred
|
|
308
|
+
* @returns {Function} returns.refetch - Function to manually refetch the data
|
|
309
|
+
* @returns {boolean} returns.isError - Whether the query resulted in an error
|
|
310
|
+
* @returns {boolean} returns.isFetched - Whether the query has been fetched
|
|
311
|
+
* @returns {boolean} returns.isFetching - Whether the query is currently fetching
|
|
312
|
+
* @returns {boolean} returns.isSuccess - Whether the query was successful
|
|
313
|
+
* @returns {string} returns.status - Current status of the query: 'idle', 'loading', 'success', 'error', or 'pending'
|
|
314
|
+
* @example
|
|
315
|
+
* ```tsx
|
|
316
|
+
* // Define the entity configuration
|
|
317
|
+
* const userConfig = {
|
|
318
|
+
* tableBlockId: 'users-table',
|
|
319
|
+
* instanceType: {} as {
|
|
320
|
+
* name: string;
|
|
321
|
+
* email: string;
|
|
322
|
+
* }
|
|
323
|
+
* };
|
|
324
|
+
*
|
|
325
|
+
* function UserProfile({ userEmail }) {
|
|
326
|
+
* const { data: user, isLoading } = useEntityGetOne(UserEntity, { email: userEmail });
|
|
327
|
+
*
|
|
328
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
329
|
+
* if (!user) return <div>User not found</div>;
|
|
330
|
+
*
|
|
331
|
+
* return <div>Name: {user.name}</div>;
|
|
332
|
+
* }
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
export const useEntityGetOne = (entityConfig, filters, queryOptions = {}) => {
|
|
336
|
+
const { enabled, initialData } = queryOptions;
|
|
337
|
+
const client = useClient();
|
|
338
|
+
let finalFilters = filters;
|
|
339
|
+
if (typeof filters === "string") {
|
|
340
|
+
finalFilters = { id: parseInt(filters) };
|
|
341
|
+
}
|
|
342
|
+
else if (typeof filters === "number") {
|
|
343
|
+
finalFilters = { id: filters };
|
|
344
|
+
}
|
|
345
|
+
const { data, isLoading, error, isError, isFetched, isFetching, isSuccess, status } = useQuery({
|
|
346
|
+
queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, "one", finalFilters],
|
|
347
|
+
queryFn: () => client.entity(entityConfig).findOne(finalFilters),
|
|
348
|
+
enabled,
|
|
349
|
+
initialData,
|
|
350
|
+
});
|
|
351
|
+
return {
|
|
352
|
+
data,
|
|
353
|
+
isLoading,
|
|
354
|
+
error,
|
|
355
|
+
refetch: () => ({ data, isLoading, error, isError, isFetched, isFetching, isSuccess, status }),
|
|
356
|
+
isError,
|
|
357
|
+
isFetched,
|
|
358
|
+
isFetching,
|
|
359
|
+
isSuccess,
|
|
360
|
+
status,
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* Hook to create a new entity
|
|
365
|
+
*
|
|
366
|
+
* Creates a new entity instance in the data store.
|
|
367
|
+
* EntityTypeOnlyMutable<EC> represents the writable properties of the entity.
|
|
368
|
+
*
|
|
369
|
+
* @template EC - Entity configuration type
|
|
370
|
+
* @param {EC} entityConfig - Configuration for the entity type
|
|
371
|
+
* @returns {Object} Mutation object with create function, loading state, and error
|
|
372
|
+
* @example
|
|
373
|
+
* ```tsx
|
|
374
|
+
* // Define the entity configuration
|
|
375
|
+
* const userConfig = {
|
|
376
|
+
* tableBlockId: 'users-table',
|
|
377
|
+
* instanceType: {} as {
|
|
378
|
+
* name: string;
|
|
379
|
+
* email: string;
|
|
380
|
+
* }
|
|
381
|
+
* };
|
|
382
|
+
*
|
|
383
|
+
* function CreateUserForm() {
|
|
384
|
+
* const { createFunction, isLoading } = useEntityCreate(userConfig);
|
|
385
|
+
* const [name, setName] = useState('');
|
|
386
|
+
* const [email, setEmail] = useState('');
|
|
387
|
+
*
|
|
388
|
+
* const handleSubmit = async (e) => {
|
|
389
|
+
* e.preventDefault();
|
|
390
|
+
* await createFunction({
|
|
391
|
+
* data: {
|
|
392
|
+
* name,
|
|
393
|
+
* email
|
|
394
|
+
* }
|
|
395
|
+
* });
|
|
396
|
+
* setName('');
|
|
397
|
+
* setEmail('');
|
|
398
|
+
* };
|
|
399
|
+
*
|
|
400
|
+
* return (
|
|
401
|
+
* <form onSubmit={handleSubmit}>
|
|
402
|
+
* <input value={name} onChange={e => setName(e.target.value)} placeholder="Name" />
|
|
403
|
+
* <input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" />
|
|
404
|
+
* <button type="submit" disabled={isLoading}>Create</button>
|
|
405
|
+
* </form>
|
|
406
|
+
* );
|
|
407
|
+
* }
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
export const useEntityCreate = (entityConfig) => {
|
|
411
|
+
const client = useClient();
|
|
412
|
+
const queryClient = useQueryClient();
|
|
413
|
+
const { validateExceededMaxCount } = useEntityMutationRateLimiting(entityConfig, "useEntityCreate");
|
|
414
|
+
const { mutateAsync: createFunction, isPending: isLoading, error, } = useMutation({
|
|
415
|
+
mutationFn: ({ data }) => {
|
|
416
|
+
validateExceededMaxCount({ data });
|
|
417
|
+
return client.entity(entityConfig).create(data);
|
|
418
|
+
},
|
|
419
|
+
onSuccess: () => {
|
|
420
|
+
// Invalidate the list so we refetch it with the newly created item
|
|
421
|
+
queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
if (error instanceof FatalAppError) {
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
return { createFunction, isLoading, error };
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* Hook to create multiple entities in a single operation
|
|
431
|
+
*
|
|
432
|
+
* Creates multiple entity instances in the data store in one batch operation.
|
|
433
|
+
* This is more efficient than calling useEntityCreate multiple times when you need
|
|
434
|
+
* to create several entities at once. EntityTypeOnlyMutable<EC> represents the
|
|
435
|
+
* writable properties of the entity.
|
|
436
|
+
*
|
|
437
|
+
* @template EC - Entity configuration type
|
|
438
|
+
* @param {EC} entityConfig - Configuration for the entity type
|
|
439
|
+
* @returns {Object} Mutation object with createMany function, loading state, and error
|
|
440
|
+
* @example
|
|
441
|
+
* ```tsx
|
|
442
|
+
* // Define the entity configuration
|
|
443
|
+
* const taskConfig = {
|
|
444
|
+
* tableBlockId: 'tasks-table',
|
|
445
|
+
* instanceType: {} as {
|
|
446
|
+
* title: string;
|
|
447
|
+
* status: 'todo' | 'in_progress' | 'done';
|
|
448
|
+
* assigneeId: string;
|
|
449
|
+
* }
|
|
450
|
+
* };
|
|
451
|
+
*
|
|
452
|
+
* function ImportTasksButton() {
|
|
453
|
+
* const { createManyFunction, isLoading } = useEntityCreateMany(taskConfig);
|
|
454
|
+
*
|
|
455
|
+
* const handleImport = async () => {
|
|
456
|
+
* const tasksToCreate = [
|
|
457
|
+
* { title: 'Review code', status: 'todo', assigneeId: 'user-1' },
|
|
458
|
+
* { title: 'Write tests', status: 'todo', assigneeId: 'user-2' },
|
|
459
|
+
* { title: 'Update docs', status: 'in_progress', assigneeId: 'user-1' }
|
|
460
|
+
* ];
|
|
461
|
+
*
|
|
462
|
+
* const createdTasks = await createManyFunction({ data: tasksToCreate });
|
|
463
|
+
* console.log(`Created ${createdTasks.length} tasks`);
|
|
464
|
+
* };
|
|
465
|
+
*
|
|
466
|
+
* return (
|
|
467
|
+
* <button onClick={handleImport} disabled={isLoading}>
|
|
468
|
+
* {isLoading ? 'Importing...' : 'Import Tasks'}
|
|
469
|
+
* </button>
|
|
470
|
+
* );
|
|
471
|
+
* }
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
474
|
+
export const useEntityCreateMany = (entityConfig) => {
|
|
475
|
+
const client = useClient();
|
|
476
|
+
const queryClient = useQueryClient();
|
|
477
|
+
const { validateExceededMaxCount } = useEntityMutationRateLimiting(entityConfig, "useEntityCreateMany");
|
|
478
|
+
const { mutateAsync: createManyFunction, isPending: isLoading, error, } = useMutation({
|
|
479
|
+
mutationFn: ({ data }) => {
|
|
480
|
+
validateExceededMaxCount({ data });
|
|
481
|
+
return client.entity(entityConfig).createMany(data);
|
|
482
|
+
},
|
|
483
|
+
onSuccess: () => {
|
|
484
|
+
// Invalidate the list so we refetch it with the newly created item
|
|
485
|
+
queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
if (error instanceof FatalAppError) {
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
return { createManyFunction, isLoading, error };
|
|
492
|
+
};
|
|
493
|
+
/**
|
|
494
|
+
* Hook to update an existing entity
|
|
495
|
+
*
|
|
496
|
+
* Updates an existing entity with the provided partial data.
|
|
497
|
+
* Only the properties included in the data object will be updated.
|
|
498
|
+
*
|
|
499
|
+
* @template EC - Entity configuration type
|
|
500
|
+
* @param {EC} entityConfig - Configuration for the entity type
|
|
501
|
+
* @returns {Object} Mutation object with update function, loading state, and error
|
|
502
|
+
* @example
|
|
503
|
+
* ```tsx
|
|
504
|
+
* // Define the entity configuration
|
|
505
|
+
* const userConfig = {
|
|
506
|
+
* tableBlockId: 'users-table',
|
|
507
|
+
* instanceType: {} as {
|
|
508
|
+
* name: string;
|
|
509
|
+
* email: string;
|
|
510
|
+
* }
|
|
511
|
+
* };
|
|
512
|
+
*
|
|
513
|
+
* function EditUserForm({ user }) {
|
|
514
|
+
* const { updateFunction, isLoading } = useEntityUpdate(userConfig);
|
|
515
|
+
* const [name, setName] = useState(user.name);
|
|
516
|
+
*
|
|
517
|
+
* const handleSubmit = async (e) => {
|
|
518
|
+
* e.preventDefault();
|
|
519
|
+
* await updateFunction({
|
|
520
|
+
* id: user.id,
|
|
521
|
+
* data: { name }
|
|
522
|
+
* });
|
|
523
|
+
* };
|
|
524
|
+
*
|
|
525
|
+
* return (
|
|
526
|
+
* <form onSubmit={handleSubmit}>
|
|
527
|
+
* <input value={name} onChange={e => setName(e.target.value)} />
|
|
528
|
+
* <button type="submit" disabled={isLoading}>Update</button>
|
|
529
|
+
* </form>
|
|
530
|
+
* );
|
|
531
|
+
* }
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
export const useEntityUpdate = (entityConfig) => {
|
|
535
|
+
const client = useClient();
|
|
536
|
+
const queryClient = useQueryClient();
|
|
537
|
+
const { validateExceededMaxCount } = useEntityRowMutationRateLimiting(entityConfig, "useEntityUpdate");
|
|
538
|
+
const { mutateAsync: updateFunction, isPending: isLoading, error, } = useMutation({
|
|
539
|
+
mutationFn: ({ id, data }) => {
|
|
540
|
+
validateExceededMaxCount({ rowId: id, data });
|
|
541
|
+
return client.entity(entityConfig).updateOne(id, data);
|
|
542
|
+
},
|
|
543
|
+
onSuccess: (_result, { id }) => {
|
|
544
|
+
// Invalidate both the list and the specific item's query
|
|
545
|
+
queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
|
|
546
|
+
queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, "one", { id }] });
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
if (error instanceof FatalAppError) {
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
return { updateFunction, isLoading, error };
|
|
553
|
+
};
|
|
554
|
+
/**
|
|
555
|
+
* Hook to delete an entity
|
|
556
|
+
*
|
|
557
|
+
* Permanently removes an entity from the data store by its ID.
|
|
558
|
+
* Automatically invalidates relevant queries after deletion.
|
|
559
|
+
*
|
|
560
|
+
* @template EC - Entity configuration type
|
|
561
|
+
* @param {EC} entityConfig - Configuration for the entity type
|
|
562
|
+
* @returns {Object} Mutation object with delete function, loading state, and error
|
|
563
|
+
* @example
|
|
564
|
+
* ```tsx
|
|
565
|
+
* // Define the entity configuration
|
|
566
|
+
* const userConfig = {
|
|
567
|
+
* tableBlockId: 'users-table',
|
|
568
|
+
* instanceType: {} as {
|
|
569
|
+
* name: string;
|
|
570
|
+
* email: string;
|
|
571
|
+
* }
|
|
572
|
+
* };
|
|
573
|
+
*
|
|
574
|
+
* function DeleteUserButton({ userId }) {
|
|
575
|
+
* const { deleteFunction, isLoading } = useEntityDelete(userConfig);
|
|
576
|
+
*
|
|
577
|
+
* const handleDelete = async () => {
|
|
578
|
+
* if (confirm('Are you sure?')) {
|
|
579
|
+
* await deleteFunction({ id: userId });
|
|
580
|
+
* }
|
|
581
|
+
* };
|
|
582
|
+
*
|
|
583
|
+
* return (
|
|
584
|
+
* <button onClick={handleDelete} disabled={isLoading}>
|
|
585
|
+
* Delete User
|
|
586
|
+
* </button>
|
|
587
|
+
* );
|
|
588
|
+
* }
|
|
589
|
+
* ```
|
|
590
|
+
*/
|
|
591
|
+
export const useEntityDelete = (entityConfig) => {
|
|
592
|
+
const client = useClient();
|
|
593
|
+
const queryClient = useQueryClient();
|
|
594
|
+
const { validateExceededMaxCount } = useEntityRowMutationRateLimiting(entityConfig, "useEntityDelete");
|
|
595
|
+
const { mutateAsync: deleteFunction, isPending: isLoading, error, } = useMutation({
|
|
596
|
+
mutationFn: ({ id }) => {
|
|
597
|
+
validateExceededMaxCount({ rowId: id });
|
|
598
|
+
return client.entity(entityConfig).deleteOne(id);
|
|
599
|
+
},
|
|
600
|
+
onSuccess: (_result, { id }) => {
|
|
601
|
+
// Invalidate both the list and the specific item's query
|
|
602
|
+
queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
|
|
603
|
+
queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, "one", { id }] });
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
if (error instanceof FatalAppError) {
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
return { deleteFunction, isLoading, error };
|
|
610
|
+
};
|
|
611
|
+
/**
|
|
612
|
+
* Hook to delete multiple entities at once
|
|
613
|
+
*
|
|
614
|
+
* Permanently removes multiple entities from the data store by their IDs.
|
|
615
|
+
* Automatically invalidates relevant queries after deletion.
|
|
616
|
+
*
|
|
617
|
+
* @template EC - Entity configuration type
|
|
618
|
+
* @param {EC} entityConfig - Configuration for the entity type
|
|
619
|
+
* @returns {Object} Mutation object with delete many function, loading state, and error
|
|
620
|
+
* @example
|
|
621
|
+
* ```tsx
|
|
622
|
+
* // Define the entity configuration
|
|
623
|
+
* const userConfig = {
|
|
624
|
+
* tableBlockId: 'users-table',
|
|
625
|
+
* instanceType: {} as {
|
|
626
|
+
* name: string;
|
|
627
|
+
* email: string;
|
|
628
|
+
* }
|
|
629
|
+
* };
|
|
630
|
+
*
|
|
631
|
+
* function DeleteSelectedUsersButton({ selectedUserIds }) {
|
|
632
|
+
* const { deleteManyFunction, isLoading } = useEntityDeleteMany(userConfig);
|
|
633
|
+
*
|
|
634
|
+
* const handleDeleteSelected = async () => {
|
|
635
|
+
* if (confirm(`Delete ${selectedUserIds.length} users?`)) {
|
|
636
|
+
* await deleteManyFunction({ ids: selectedUserIds });
|
|
637
|
+
* }
|
|
638
|
+
* };
|
|
639
|
+
*
|
|
640
|
+
* return (
|
|
641
|
+
* <button onClick={handleDeleteSelected} disabled={isLoading}>
|
|
642
|
+
* Delete Selected ({selectedUserIds.length})
|
|
643
|
+
* </button>
|
|
644
|
+
* );
|
|
645
|
+
* }
|
|
646
|
+
* ```
|
|
647
|
+
*/
|
|
648
|
+
export const useEntityDeleteMany = (entityConfig) => {
|
|
649
|
+
const client = useClient();
|
|
650
|
+
const queryClient = useQueryClient();
|
|
651
|
+
const { validateExceededMaxCount } = useEntityMutationRateLimiting(entityConfig, "useEntityDeleteMany");
|
|
652
|
+
const { mutateAsync: deleteManyFunction, isPending: isLoading, error, } = useMutation({
|
|
653
|
+
mutationFn: ({ ids }) => {
|
|
654
|
+
validateExceededMaxCount({ data: ids });
|
|
655
|
+
return client.entity(entityConfig).deleteMany(ids);
|
|
656
|
+
},
|
|
657
|
+
onSuccess: (_result, { ids }) => {
|
|
658
|
+
queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
|
|
659
|
+
ids.forEach((id) => {
|
|
660
|
+
queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, "one", { id }] });
|
|
661
|
+
});
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
if (error instanceof FatalAppError) {
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
return { deleteManyFunction, isLoading, error };
|
|
668
|
+
};
|
|
669
|
+
/**
|
|
670
|
+
* Hook to execute an action, with support for streaming results
|
|
671
|
+
*
|
|
672
|
+
* Actions represent executable workflows that perform any type of server-side operation,
|
|
673
|
+
* including data processing, business logic, integrations with external
|
|
674
|
+
* services, file operations, AI functionalities, and more. This hook provides a React-friendly
|
|
675
|
+
* way to execute these actions with proper state management.
|
|
676
|
+
*
|
|
677
|
+
* AI capabilities are a particularly interesting use case for actions - you can leverage
|
|
678
|
+
* various AI models and services while the SDK handles the complexity of streaming responses,
|
|
679
|
+
* state management, and UI integration.
|
|
680
|
+
*
|
|
681
|
+
* The hook provides two different result states:
|
|
682
|
+
* - `result`: Contains the final, complete output once the action execution is done
|
|
683
|
+
* - `streamResult`: Contains incrementally updating results during streaming operations,
|
|
684
|
+
* updated in real-time as chunks arrive from the server before the action is complete
|
|
685
|
+
*
|
|
686
|
+
* ActionConfig properties:
|
|
687
|
+
* - actionBlockId: Unique identifier for the action workflow
|
|
688
|
+
* - inputInstanceType: TypeScript type defining the action's input parameters
|
|
689
|
+
* - outputInstanceType: TypeScript type defining the action's output structure
|
|
690
|
+
*
|
|
691
|
+
* @template AC - Action configuration type
|
|
692
|
+
* @param {AC} actionConfig - Configuration for the action
|
|
693
|
+
* @returns {Object} Action execution object with functions, state, and results
|
|
694
|
+
* @returns {Function} returns.executeFunction - Function to execute the action with inputs
|
|
695
|
+
* @returns {AC["outputInstanceType"]} returns.result - Final result after action completes
|
|
696
|
+
* @returns {AC["outputInstanceType"]} returns.streamResult - Incrementally updated result during streaming
|
|
697
|
+
* @returns {boolean} returns.isLoading - Whether the action is currently executing
|
|
698
|
+
* @returns {boolean} returns.isDone - Whether the action has completed
|
|
699
|
+
* @returns {Error | null} returns.error - Any error that occurred during execution
|
|
700
|
+
* @returns {Function} returns.clear - Function to clear result and streamResult values
|
|
701
|
+
* @example
|
|
702
|
+
* ```tsx
|
|
703
|
+
* // Example 1: Process payment action
|
|
704
|
+
* const processPaymentConfig = {
|
|
705
|
+
* actionBlockId: 'process-payment',
|
|
706
|
+
* inputInstanceType: {} as {
|
|
707
|
+
* amount: number;
|
|
708
|
+
* paymentMethod: string;
|
|
709
|
+
* currency: string;
|
|
710
|
+
* },
|
|
711
|
+
* outputInstanceType: {} as {
|
|
712
|
+
* success: boolean;
|
|
713
|
+
* transactionId: string;
|
|
714
|
+
* receiptUrl?: string;
|
|
715
|
+
* error?: string;
|
|
716
|
+
* }
|
|
717
|
+
* };
|
|
718
|
+
*
|
|
719
|
+
* function PaymentForm() {
|
|
720
|
+
* const { executeFunction, result, isLoading, error } = useExecuteAction(processPaymentConfig);
|
|
721
|
+
* const [amount, setAmount] = useState('');
|
|
722
|
+
* const [paymentMethod, setPaymentMethod] = useState('credit_card');
|
|
723
|
+
*
|
|
724
|
+
* const handleSubmit = async (e) => {
|
|
725
|
+
* e.preventDefault();
|
|
726
|
+
* await executeFunction({
|
|
727
|
+
* amount: parseFloat(amount),
|
|
728
|
+
* paymentMethod,
|
|
729
|
+
* currency: 'USD'
|
|
730
|
+
* });
|
|
731
|
+
* };
|
|
732
|
+
*
|
|
733
|
+
* return (
|
|
734
|
+
* <div>
|
|
735
|
+
* <form onSubmit={handleSubmit}>
|
|
736
|
+
* <input
|
|
737
|
+
* type="number"
|
|
738
|
+
* value={amount}
|
|
739
|
+
* onChange={e => setAmount(e.target.value)}
|
|
740
|
+
* placeholder="Amount"
|
|
741
|
+
* />
|
|
742
|
+
* <select value={paymentMethod} onChange={e => setPaymentMethod(e.target.value)}>
|
|
743
|
+
* <option value="credit_card">Credit Card</option>
|
|
744
|
+
* <option value="paypal">PayPal</option>
|
|
745
|
+
* </select>
|
|
746
|
+
* <button type="submit" disabled={isLoading}>Process Payment</button>
|
|
747
|
+
* </form>
|
|
748
|
+
*
|
|
749
|
+
* {isLoading && <div>Processing payment...</div>}
|
|
750
|
+
* {error && <div className="error">Error: {error.message}</div>}
|
|
751
|
+
* {result?.success && <div className="success">
|
|
752
|
+
* Payment successful! Transaction ID: {result.transactionId}
|
|
753
|
+
* </div>}
|
|
754
|
+
* </div>
|
|
755
|
+
* );
|
|
756
|
+
* }
|
|
757
|
+
*
|
|
758
|
+
* // Example 2: Data export action (with streaming)
|
|
759
|
+
* const exportDataConfig = {
|
|
760
|
+
* actionBlockId: 'export-data',
|
|
761
|
+
* inputInstanceType: {} as {
|
|
762
|
+
* format: 'csv' | 'json';
|
|
763
|
+
* filters: Record<string, any>;
|
|
764
|
+
* },
|
|
765
|
+
* outputInstanceType: {} as {
|
|
766
|
+
* progress: number;
|
|
767
|
+
* downloadUrl?: string;
|
|
768
|
+
* status: string;
|
|
769
|
+
* }
|
|
770
|
+
* };
|
|
771
|
+
*
|
|
772
|
+
* function DataExportTool() {
|
|
773
|
+
* const { executeFunction, result, isLoading, isDone } = useExecuteAction(exportDataConfig);
|
|
774
|
+
* const [format, setFormat] = useState<'csv' | 'json'>('csv');
|
|
775
|
+
*
|
|
776
|
+
* const handleExport = async () => {
|
|
777
|
+
* await executeFunction({
|
|
778
|
+
* format,
|
|
779
|
+
* filters: { startDate: '2023-01-01' }
|
|
780
|
+
* });
|
|
781
|
+
* };
|
|
782
|
+
*
|
|
783
|
+
* return (
|
|
784
|
+
* <div>
|
|
785
|
+
* <div>
|
|
786
|
+
* <select value={format} onChange={e => setFormat(e.target.value as 'csv' | 'json')}>
|
|
787
|
+
* <option value="csv">CSV</option>
|
|
788
|
+
* <option value="json">JSON</option>
|
|
789
|
+
* </select>
|
|
790
|
+
* <button onClick={handleExport} disabled={isLoading}>Export Data</button>
|
|
791
|
+
* </div>
|
|
792
|
+
*
|
|
793
|
+
* {isLoading && !isDone && (
|
|
794
|
+
* <div>
|
|
795
|
+
* {result?.progress ? `Export in progress: ${result.progress}%` : 'Starting export...'}
|
|
796
|
+
* </div>
|
|
797
|
+
* )}
|
|
798
|
+
* {isDone && result?.downloadUrl && (
|
|
799
|
+
* <a href={result.downloadUrl} download>Download your data</a>
|
|
800
|
+
* )}
|
|
801
|
+
* </div>
|
|
802
|
+
* );
|
|
803
|
+
* }
|
|
804
|
+
* ```
|
|
805
|
+
*/
|
|
806
|
+
export const useExecuteAction = (actionConfig) => {
|
|
807
|
+
const client = useClient();
|
|
808
|
+
const [isDone, setIsDone] = useState(false);
|
|
809
|
+
const { validateExceededMaxCount } = useActionRateLimiting(actionConfig, "useExecuteAction");
|
|
810
|
+
const [streamResult, setStreamResult] = useState();
|
|
811
|
+
const { handleChunk, onCompleteChunks } = useAiStreamChunks({
|
|
812
|
+
onChunk: (chunkData) => {
|
|
813
|
+
// TODO: This is a very hard-coded solution designed for the generate text action, who uses "response" as the key
|
|
814
|
+
// We should find a better way to handle this
|
|
815
|
+
setStreamResult((prevResult) => {
|
|
816
|
+
const prevResponse = prevResult?.response || "";
|
|
817
|
+
return {
|
|
818
|
+
...(prevResult || {}),
|
|
819
|
+
response: prevResponse + chunkData,
|
|
820
|
+
};
|
|
821
|
+
});
|
|
822
|
+
},
|
|
823
|
+
});
|
|
824
|
+
const { data: result, mutateAsync: executeFunction, isPending: isLoading, error, reset, } = useMutation({
|
|
825
|
+
mutationFn: async (inputs) => {
|
|
826
|
+
let streamResultHandled = false;
|
|
827
|
+
validateExceededMaxCount(inputs);
|
|
828
|
+
const result = await client.action(actionConfig).execute(inputs, {
|
|
829
|
+
onChunk: (chunkData) => {
|
|
830
|
+
streamResultHandled = true;
|
|
831
|
+
handleChunk(chunkData.contentIndex, chunkData.content);
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
if (!streamResultHandled) {
|
|
835
|
+
setStreamResult(result);
|
|
836
|
+
}
|
|
837
|
+
return result;
|
|
838
|
+
},
|
|
839
|
+
onMutate: () => {
|
|
840
|
+
setStreamResult(undefined);
|
|
841
|
+
setIsDone(false);
|
|
842
|
+
},
|
|
843
|
+
onSettled: () => {
|
|
844
|
+
onCompleteChunks();
|
|
845
|
+
setIsDone(true);
|
|
846
|
+
},
|
|
847
|
+
});
|
|
848
|
+
if (error instanceof FatalAppError) {
|
|
849
|
+
throw error;
|
|
850
|
+
}
|
|
851
|
+
const clear = useCallback(() => {
|
|
852
|
+
reset();
|
|
853
|
+
setStreamResult(undefined);
|
|
854
|
+
}, []);
|
|
855
|
+
return { executeFunction, result, streamResult, isLoading, isDone, error, clear };
|
|
856
|
+
};
|
|
857
|
+
/**
|
|
858
|
+
* Hook to get an AgentChat instance
|
|
859
|
+
*
|
|
860
|
+
* This hook provides access to an AgentChat instance for the specified configuration.
|
|
861
|
+
*
|
|
862
|
+
* @template ACC - AgentChat configuration type
|
|
863
|
+
* @param {ACC} agentChatConfig - Configuration for the agent chat
|
|
864
|
+
* @returns {AgentChat<ACC>} An AgentChat instance for the given configuration
|
|
865
|
+
* @example
|
|
866
|
+
* ```tsx
|
|
867
|
+
* // Define the agent chat configuration
|
|
868
|
+
* const agentChatConfig = {
|
|
869
|
+
* agentChatId: 'agent-chat-id'
|
|
870
|
+
* };
|
|
871
|
+
*
|
|
872
|
+
* function AgentChatComponent() {
|
|
873
|
+
* const agentChat = useAgentChat(agentChatConfig);
|
|
874
|
+
* return <AgentChat agentChat={agentChat} />;
|
|
875
|
+
* }
|
|
876
|
+
* ```
|
|
877
|
+
*/
|
|
878
|
+
export const useAgentChat = (agentChatConfig) => {
|
|
879
|
+
const client = useClient();
|
|
880
|
+
return client.agentChat(agentChatConfig);
|
|
881
|
+
};
|
|
882
|
+
/**
|
|
883
|
+
* Hook to handle file uploads
|
|
884
|
+
*
|
|
885
|
+
* Provides a function to upload files with progress tracking
|
|
886
|
+
*
|
|
887
|
+
* @returns {Object} Upload object with upload functionality and status
|
|
888
|
+
* @returns {Function} returns.uploadFunction - Function that takes a File and returns a Promise with the URL
|
|
889
|
+
* @returns {boolean} returns.isLoading - Whether an upload is in progress
|
|
890
|
+
* @returns {number} returns.uploadPercentage - Current upload progress (0-100)
|
|
891
|
+
* @example
|
|
892
|
+
* ```tsx
|
|
893
|
+
* function FileUploader() {
|
|
894
|
+
* const { uploadFunction, isLoading, uploadPercentage } = useFileUpload();
|
|
895
|
+
* const [fileUrl, setFileUrl] = useState('');
|
|
896
|
+
*
|
|
897
|
+
* const handleFileChange = async (event) => {
|
|
898
|
+
* const file = event.target.files[0];
|
|
899
|
+
* if (file) {
|
|
900
|
+
* try {
|
|
901
|
+
* const url = await uploadFunction(file);
|
|
902
|
+
* setFileUrl(url);
|
|
903
|
+
* } catch (error) {
|
|
904
|
+
* console.error('Upload failed:', error);
|
|
905
|
+
* }
|
|
906
|
+
* }
|
|
907
|
+
* };
|
|
908
|
+
*
|
|
909
|
+
* return (
|
|
910
|
+
* <div>
|
|
911
|
+
* <input type="file" onChange={handleFileChange} disabled={isLoading} />
|
|
912
|
+
* {isLoading && <Progress value={uploadPercentage} />}
|
|
913
|
+
* {fileUrl && <img src={fileUrl} alt="Uploaded file" />}
|
|
914
|
+
* </div>
|
|
915
|
+
* );
|
|
916
|
+
* }
|
|
917
|
+
* ```
|
|
918
|
+
*/
|
|
919
|
+
export const useFileUpload = () => {
|
|
920
|
+
const client = useClient();
|
|
921
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
922
|
+
const [uploadPercentage, setUploadPercentage] = useState(0);
|
|
923
|
+
const uploadFunction = useCallback(async (file) => {
|
|
924
|
+
setIsLoading(true);
|
|
925
|
+
setUploadPercentage(0);
|
|
926
|
+
try {
|
|
927
|
+
const protectedUrl = await client.upload(file, {
|
|
928
|
+
onProgress: (percentage) => {
|
|
929
|
+
setUploadPercentage(percentage);
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
return protectedUrl;
|
|
933
|
+
}
|
|
934
|
+
finally {
|
|
935
|
+
setIsLoading(false);
|
|
936
|
+
}
|
|
937
|
+
}, [client]);
|
|
938
|
+
return { uploadFunction, isLoading, uploadPercentage };
|
|
939
|
+
};
|
|
940
|
+
/**
|
|
941
|
+
* Represents a user's data
|
|
942
|
+
* @interface User
|
|
943
|
+
* @property {string} [id] - The user's ID (optional, must exist if user is authenticated)
|
|
944
|
+
* @property {string} [email] - The user's email (optional, must exist if user is authenticated)
|
|
945
|
+
* @property {string} [name] - The user's name (optional, must exist if user is authenticated)
|
|
946
|
+
* @property {string} [profileImageUrl] - The user's profile image URL (optional)
|
|
947
|
+
* @property {string} [firstName] - The user's first name (optional)
|
|
948
|
+
* @property {string} [lastName] - The user's last name (optional)
|
|
949
|
+
* @property {string} [role] - The user's role name (optional)
|
|
950
|
+
* @property {string} [permission] - The user's permission level: 'build' (can modify app) or 'use' (read-only) (optional)
|
|
951
|
+
*/
|
|
952
|
+
/**
|
|
953
|
+
* Hook to get the current user information
|
|
954
|
+
*
|
|
955
|
+
* This hook provides access to the user's data
|
|
956
|
+
* that was provided during SDK initialization, including role information for multi-role apps.
|
|
957
|
+
*
|
|
958
|
+
* @returns {User} The current user object with optional role property
|
|
959
|
+
* @example
|
|
960
|
+
* ```tsx
|
|
961
|
+
* const user = useUser();
|
|
962
|
+
* console.log(user.isAuthenticated ? `Logged in as: ${user.firstName} ${user.lastName}` : "Not logged in");
|
|
963
|
+
*
|
|
964
|
+
* // For multi-role apps, conditionally render based on role
|
|
965
|
+
* if (user.role === "Manager") {
|
|
966
|
+
* return <ManagerDashboard />;
|
|
967
|
+
* } else if (user.role === "Employee") {
|
|
968
|
+
* return <EmployeeDashboard />;
|
|
969
|
+
* }
|
|
970
|
+
* ```
|
|
971
|
+
*/
|
|
972
|
+
export const useUser = () => {
|
|
973
|
+
const client = useClient();
|
|
974
|
+
const { data: user } = useQuery({
|
|
975
|
+
queryKey: [CURRENT_USER_QUERY_KEY],
|
|
976
|
+
queryFn: () => client.getUser(),
|
|
977
|
+
initialData: client.getUser(),
|
|
978
|
+
});
|
|
979
|
+
return user;
|
|
980
|
+
};
|
|
981
|
+
/**
|
|
982
|
+
* Hook to manage the application theme mode
|
|
983
|
+
*
|
|
984
|
+
* This hook provides access to the current theme mode and a function to change it.
|
|
985
|
+
* It supports three theme modes: dark, light, and system (which follows the user's OS preference).
|
|
986
|
+
*
|
|
987
|
+
* @returns {Object} Object containing current theme mode and setter function
|
|
988
|
+
* @returns {("dark" | "light" | "system")} themeMode - The current theme mode
|
|
989
|
+
* @returns {Function} setThemeMode - Function to update the theme mode
|
|
990
|
+
* @example
|
|
991
|
+
* ```tsx
|
|
992
|
+
* function ThemeToggle() {
|
|
993
|
+
* const { themeMode, setThemeMode } = useThemeMode();
|
|
994
|
+
*
|
|
995
|
+
* return (
|
|
996
|
+
* <div>
|
|
997
|
+
* <p>Current theme: {themeMode}</p>
|
|
998
|
+
* <button onClick={() => setThemeMode("dark")}>Dark</button>
|
|
999
|
+
* <button onClick={() => setThemeMode("light")}>Light</button>
|
|
1000
|
+
* <button onClick={() => setThemeMode("system")}>System</button>
|
|
1001
|
+
* </div>
|
|
1002
|
+
* );
|
|
1003
|
+
* }
|
|
1004
|
+
*
|
|
1005
|
+
* // Example with a select dropdown
|
|
1006
|
+
* function ThemeSelector() {
|
|
1007
|
+
* const { themeMode, setThemeMode } = useThemeMode();
|
|
1008
|
+
*
|
|
1009
|
+
* return (
|
|
1010
|
+
* <select
|
|
1011
|
+
* value={themeMode}
|
|
1012
|
+
* onChange={(e) => setThemeMode(e.target.value as "dark" | "light" | "system")}
|
|
1013
|
+
* >
|
|
1014
|
+
* <option value="system">System</option>
|
|
1015
|
+
* <option value="light">Light</option>
|
|
1016
|
+
* <option value="dark">Dark</option>
|
|
1017
|
+
* </select>
|
|
1018
|
+
* );
|
|
1019
|
+
* }
|
|
1020
|
+
* ```
|
|
1021
|
+
*/
|
|
1022
|
+
export const useThemeMode = () => {
|
|
1023
|
+
const { themeMode, setThemeMode } = useContext(ThemeModeContext);
|
|
1024
|
+
return { themeMode, setThemeMode };
|
|
1025
|
+
};
|
|
1026
|
+
/**
|
|
1027
|
+
* Hook to change a user's role in multi-role applications
|
|
1028
|
+
*
|
|
1029
|
+
* This hook provides functionality to change a user's role, but only if the current user
|
|
1030
|
+
* has "build" permission. The hook automatically invalidates relevant queries after a successful
|
|
1031
|
+
* role change, including the current user's data if they changed their own role.
|
|
1032
|
+
*
|
|
1033
|
+
* @param {Object} [params] - Hook parameters
|
|
1034
|
+
* @param {UpdateUserOptions} [params.options] - Optional configuration for the user update
|
|
1035
|
+
* @returns {Object} Mutation object with role change function, loading state, error, and authorization state
|
|
1036
|
+
* @returns {Function} returns.changeUserRoleFunction - Function to change a user's role
|
|
1037
|
+
* @returns {boolean} returns.isLoading - Whether the role change is in progress
|
|
1038
|
+
* @returns {Error | null} returns.error - Any error that occurred during the role change
|
|
1039
|
+
* @returns {boolean} returns.isEnabled - Whether the current user is authorized to change roles (has "build" permission)
|
|
1040
|
+
* @example
|
|
1041
|
+
* ```tsx
|
|
1042
|
+
* function UserRoleManager({ userId, currentRole }) {
|
|
1043
|
+
* const { changeUserRoleFunction, isLoading, error, isEnabled } = useChangeUserRole();
|
|
1044
|
+
* const [selectedRole, setSelectedRole] = useState(currentRole);
|
|
1045
|
+
*
|
|
1046
|
+
* const handleRoleChange = async () => {
|
|
1047
|
+
* try {
|
|
1048
|
+
* await changeUserRoleFunction({
|
|
1049
|
+
* userId,
|
|
1050
|
+
* role: selectedRole
|
|
1051
|
+
* });
|
|
1052
|
+
* alert('Role changed successfully!');
|
|
1053
|
+
* } catch (err) {
|
|
1054
|
+
* console.error('Failed to change role:', err);
|
|
1055
|
+
* }
|
|
1056
|
+
* };
|
|
1057
|
+
*
|
|
1058
|
+
* if (!isEnabled) {
|
|
1059
|
+
* return <div>You don't have permission to change user roles</div>;
|
|
1060
|
+
* }
|
|
1061
|
+
*
|
|
1062
|
+
* return (
|
|
1063
|
+
* <div>
|
|
1064
|
+
* <select
|
|
1065
|
+
* value={selectedRole}
|
|
1066
|
+
* onChange={(e) => setSelectedRole(e.target.value)}
|
|
1067
|
+
* disabled={isLoading}
|
|
1068
|
+
* >
|
|
1069
|
+
* <option value="Admin">Admin</option>
|
|
1070
|
+
* <option value="Manager">Manager</option>
|
|
1071
|
+
* <option value="Employee">Employee</option>
|
|
1072
|
+
* </select>
|
|
1073
|
+
* <button onClick={handleRoleChange} disabled={isLoading}>
|
|
1074
|
+
* {isLoading ? 'Changing...' : 'Change Role'}
|
|
1075
|
+
* </button>
|
|
1076
|
+
* {error && <div className="error">Error: {error.message}</div>}
|
|
1077
|
+
* </div>
|
|
1078
|
+
* );
|
|
1079
|
+
* }
|
|
1080
|
+
* ```
|
|
1081
|
+
*/
|
|
1082
|
+
export const useChangeUserRole = ({ options } = {}) => {
|
|
1083
|
+
const client = useClient();
|
|
1084
|
+
const queryClient = useQueryClient();
|
|
1085
|
+
const currentUser = useUser();
|
|
1086
|
+
const isEnabled = currentUser?.permission === "build";
|
|
1087
|
+
const { mutateAsync: changeUserRoleFunction, isPending: isLoading, error, } = useMutation({
|
|
1088
|
+
mutationFn: ({ userId, role }) => {
|
|
1089
|
+
if (!isEnabled) {
|
|
1090
|
+
throw new Error("You are not authorized to change user roles");
|
|
1091
|
+
}
|
|
1092
|
+
return client.changeUserRole(userId, role, options);
|
|
1093
|
+
},
|
|
1094
|
+
onSuccess: (_result, { userId }) => {
|
|
1095
|
+
if (currentUser.id && userId === currentUser.id) {
|
|
1096
|
+
queryClient.invalidateQueries({ queryKey: [CURRENT_USER_QUERY_KEY] });
|
|
1097
|
+
}
|
|
1098
|
+
queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, USERS_TABLE_BLOCK_ID] });
|
|
1099
|
+
queryClient.invalidateQueries({
|
|
1100
|
+
queryKey: [ENTITIES_QUERY_KEY, USERS_TABLE_BLOCK_ID, "one", { id: userId }],
|
|
1101
|
+
});
|
|
1102
|
+
},
|
|
1103
|
+
});
|
|
1104
|
+
if (error instanceof FatalAppError) {
|
|
1105
|
+
throw error;
|
|
1106
|
+
}
|
|
1107
|
+
return { changeUserRoleFunction, isLoading, error, isEnabled };
|
|
1108
|
+
};
|
|
1109
|
+
/**
|
|
1110
|
+
* Hook to send a passwordless login link (magic link) to a user's email
|
|
1111
|
+
*
|
|
1112
|
+
* This hook provides functionality to send an email containing a one-time login link
|
|
1113
|
+
* that allows users to authenticate without entering a password. When the user clicks
|
|
1114
|
+
* the link in their email, they will be automatically logged into the application.
|
|
1115
|
+
* The hook manages loading state, success state, and error handling automatically.
|
|
1116
|
+
*
|
|
1117
|
+
* @returns {Object} Object with send function, loading state, success state, and error
|
|
1118
|
+
* @returns {Function} returns.sendLoginLink - Function to send the login link to an email address
|
|
1119
|
+
* @returns {boolean} returns.isLoading - Whether the request is currently in progress
|
|
1120
|
+
* @returns {boolean} returns.isSuccess - Whether the login link was sent successfully
|
|
1121
|
+
* @returns {Error | null} returns.error - Any error that occurred during the request
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```tsx
|
|
1124
|
+
* function PasswordlessLoginForm() {
|
|
1125
|
+
* const { sendLoginLink, isLoading, isSuccess, error } = useSendLoginLink();
|
|
1126
|
+
* const [email, setEmail] = useState('');
|
|
1127
|
+
*
|
|
1128
|
+
* const handleSubmit = async (e: React.FormEvent) => {
|
|
1129
|
+
* e.preventDefault();
|
|
1130
|
+
* await sendLoginLink({ email });
|
|
1131
|
+
* };
|
|
1132
|
+
*
|
|
1133
|
+
* if (isSuccess) {
|
|
1134
|
+
* return <div>Check your email for a login link!</div>;
|
|
1135
|
+
* }
|
|
1136
|
+
*
|
|
1137
|
+
* return (
|
|
1138
|
+
* <form onSubmit={handleSubmit}>
|
|
1139
|
+
* <input
|
|
1140
|
+
* type="email"
|
|
1141
|
+
* value={email}
|
|
1142
|
+
* onChange={(e) => setEmail(e.target.value)}
|
|
1143
|
+
* placeholder="Enter your email"
|
|
1144
|
+
* disabled={isLoading}
|
|
1145
|
+
* />
|
|
1146
|
+
* <button type="submit" disabled={isLoading}>
|
|
1147
|
+
* {isLoading ? 'Sending...' : 'Send Login Link'}
|
|
1148
|
+
* </button>
|
|
1149
|
+
* {error && <div className="error">{error.message}</div>}
|
|
1150
|
+
* </form>
|
|
1151
|
+
* );
|
|
1152
|
+
* }
|
|
1153
|
+
* ```
|
|
1154
|
+
*/
|
|
1155
|
+
export const useSendLoginLink = () => {
|
|
1156
|
+
const { sendLoginLink: clientSendLoginLink } = useClient();
|
|
1157
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
1158
|
+
const [isSuccess, setIsSuccess] = useState(false);
|
|
1159
|
+
const [error, setError] = useState(null);
|
|
1160
|
+
const sendLoginLink = useCallback(async ({ email }) => {
|
|
1161
|
+
setIsLoading(true);
|
|
1162
|
+
setIsSuccess(false);
|
|
1163
|
+
setError(null);
|
|
1164
|
+
try {
|
|
1165
|
+
await clientSendLoginLink({ email });
|
|
1166
|
+
setIsSuccess(true);
|
|
1167
|
+
setError(null);
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
setIsSuccess(false);
|
|
1171
|
+
setError(error);
|
|
1172
|
+
}
|
|
1173
|
+
finally {
|
|
1174
|
+
setIsLoading(false);
|
|
1175
|
+
}
|
|
1176
|
+
}, [clientSendLoginLink]);
|
|
1177
|
+
return { sendLoginLink, isLoading, isSuccess, error };
|
|
1178
|
+
};
|
|
1179
|
+
/**
|
|
1180
|
+
* Hook to get the Google OAuth login URL
|
|
1181
|
+
*
|
|
1182
|
+
* This hook returns the URL for initiating the Google OAuth login flow.
|
|
1183
|
+
* When users navigate to this URL, they will be redirected to Google's authentication page.
|
|
1184
|
+
* After successful authentication with Google, they will be redirected back to the application
|
|
1185
|
+
* and automatically logged in.
|
|
1186
|
+
*
|
|
1187
|
+
* @returns {string} The Google OAuth login URL
|
|
1188
|
+
* @example
|
|
1189
|
+
* ```tsx
|
|
1190
|
+
* // Example 1: Use in a Link component
|
|
1191
|
+
* function GoogleLoginLink() {
|
|
1192
|
+
* const googleLoginUrl = useGoogleLogin();
|
|
1193
|
+
*
|
|
1194
|
+
* return (
|
|
1195
|
+
* <Link to={googleLoginUrl}>
|
|
1196
|
+
* <Button>Sign in with Google</Button>
|
|
1197
|
+
* </Link>
|
|
1198
|
+
* );
|
|
1199
|
+
* }
|
|
1200
|
+
*
|
|
1201
|
+
* // Example 2: Redirect with a button
|
|
1202
|
+
* function GoogleLoginButton() {
|
|
1203
|
+
* const googleLoginUrl = useGoogleLogin();
|
|
1204
|
+
*
|
|
1205
|
+
* const handleGoogleLogin = () => {
|
|
1206
|
+
* window.location.href = googleLoginUrl;
|
|
1207
|
+
* };
|
|
1208
|
+
*
|
|
1209
|
+
* return <Button onClick={handleGoogleLogin}>Sign in with Google</Button>;
|
|
1210
|
+
* }
|
|
1211
|
+
* ```
|
|
1212
|
+
*/
|
|
1213
|
+
export const useGoogleLogin = () => {
|
|
1214
|
+
const { getGoogleLoginUrl: clientGetGoogleLoginUrl } = useClient();
|
|
1215
|
+
return clientGetGoogleLoginUrl();
|
|
1216
|
+
};
|
|
1217
|
+
/**
|
|
1218
|
+
* @deprecated
|
|
1219
|
+
* This hook is deprecated and should not be used.
|
|
1220
|
+
*/
|
|
1221
|
+
export const usePageParams = (pageConfig) => {
|
|
1222
|
+
const client = useClient();
|
|
1223
|
+
const pageParams = useMemo(() => {
|
|
1224
|
+
const page = client.page(pageConfig);
|
|
1225
|
+
return page.getParams();
|
|
1226
|
+
}, [client, pageConfig.pageBlockId]);
|
|
1227
|
+
return pageParams;
|
|
1228
|
+
};
|
|
1229
|
+
/**
|
|
1230
|
+
* @deprecated
|
|
1231
|
+
* This hook is deprecated and should not be used.
|
|
1232
|
+
*/
|
|
1233
|
+
export const usePageUrl = (pageConfig) => {
|
|
1234
|
+
const client = useClient();
|
|
1235
|
+
const pageParams = usePageParams(pageConfig);
|
|
1236
|
+
return client.page(pageConfig).getUrl(pageParams);
|
|
1237
|
+
};
|
|
1238
|
+
//# sourceMappingURL=ReactClientSdk.jsx.map
|