@svadmin/core 0.34.2 → 0.36.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/package.json +1 -1
- package/src/audit.ts +11 -3
- package/src/auth-hooks.svelte.ts +83 -13
- package/src/context.svelte.ts +202 -29
- package/src/form-hooks.svelte.ts +70 -20
- package/src/hook-utils.svelte.ts +33 -21
- package/src/hooks.svelte.ts +180 -39
- package/src/index.ts +47 -4
- package/src/live.svelte.ts +7 -3
- package/src/mutation-hooks.svelte.ts +161 -52
- package/src/notification.svelte.ts +14 -4
- package/src/provider-bundle.ts +183 -0
- package/src/query-hooks.svelte.ts +55 -9
- package/src/query-keys.ts +124 -18
- package/src/task-hooks.svelte.ts +68 -19
- package/src/types.ts +25 -0
- package/src/useCan.ts +19 -4
- package/src/utility-hooks.svelte.ts +34 -6
package/src/index.ts
CHANGED
|
@@ -8,9 +8,45 @@ export {
|
|
|
8
8
|
setLiveProvider, getLiveProvider,
|
|
9
9
|
setTaskProvider, getTaskProvider,
|
|
10
10
|
createAdminContext, provideAdminContext, getAdminContext, captureAdminContext,
|
|
11
|
+
provideTenantContext, getTenantContext, getProviderBundle,
|
|
11
12
|
resetContext,
|
|
12
13
|
} from './context.svelte';
|
|
13
14
|
export type { DataProviderInput, AdminContextSource, AdminContextValue, AdminContextAccessor } from './context.svelte';
|
|
15
|
+
export {
|
|
16
|
+
createProviderBundle,
|
|
17
|
+
createTenantCacheKey,
|
|
18
|
+
defaultTenantAdapter,
|
|
19
|
+
isTenantCacheKey,
|
|
20
|
+
resolveTenantProviderMeta,
|
|
21
|
+
withProviderMeta,
|
|
22
|
+
withTenantDataProvider,
|
|
23
|
+
} from './provider-bundle';
|
|
24
|
+
export {
|
|
25
|
+
dataQueryMatches,
|
|
26
|
+
isQueryKey,
|
|
27
|
+
keys,
|
|
28
|
+
parseQueryKey,
|
|
29
|
+
queryKeyMatches,
|
|
30
|
+
queryKeys,
|
|
31
|
+
} from './query-keys';
|
|
32
|
+
export type {
|
|
33
|
+
DataQueryMatcher,
|
|
34
|
+
QueryDescriptor,
|
|
35
|
+
QueryKey,
|
|
36
|
+
QueryKeysBuilder,
|
|
37
|
+
QueryKeysContext,
|
|
38
|
+
QueryMatcher,
|
|
39
|
+
} from './query-keys';
|
|
40
|
+
export type {
|
|
41
|
+
ProviderBundle,
|
|
42
|
+
ProviderMetaInput,
|
|
43
|
+
ProviderMetaResolver,
|
|
44
|
+
TenantAdapter,
|
|
45
|
+
TenantCacheIdentity,
|
|
46
|
+
TenantCacheKey,
|
|
47
|
+
TenantContext,
|
|
48
|
+
TenantId,
|
|
49
|
+
} from './provider-bundle';
|
|
14
50
|
export {
|
|
15
51
|
useList, useInfiniteList,
|
|
16
52
|
useOne, useShow,
|
|
@@ -36,10 +72,17 @@ export { readURLState, writeURLState } from './url-sync';
|
|
|
36
72
|
export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, canAccessAsync, createFeatureGate } from './permissions.svelte';
|
|
37
73
|
export { useLive, useSubscription, usePublish } from './live.svelte';
|
|
38
74
|
export { toast, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue, resetToast } from './toast.svelte';
|
|
39
|
-
export {
|
|
75
|
+
export {
|
|
76
|
+
notify,
|
|
77
|
+
notifyWithProvider,
|
|
78
|
+
closeNotification,
|
|
79
|
+
setNotificationProvider,
|
|
80
|
+
getNotificationProvider,
|
|
81
|
+
} from './notification.svelte';
|
|
82
|
+
export type { NotificationParams } from './notification.svelte';
|
|
40
83
|
export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation, setI18nProvider, getI18nProvider, createI18nScope, provideI18nScope, getI18nScope, resetI18n } from './i18n.svelte';
|
|
41
84
|
export type { I18nProvider, I18nScope, I18nScopeOptions } from './i18n.svelte';
|
|
42
|
-
export { audit, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
|
|
85
|
+
export { audit, auditWithProvider, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
|
|
43
86
|
export type { AuditLogProvider } from './audit';
|
|
44
87
|
export { setChatProvider, getChatProvider, setChatContext, getChatContext, setAgentProvider, getAgentProvider, registerApproval, resolveApproval, hasPendingApprovals, resetChatProvider } from './chatProvider.svelte';
|
|
45
88
|
export type { ChatProvider, ChatMessage, ChatContext, ChatAction, AgentProvider, AgentEvent, AgentOptions, AdminTool, AdminToolParameter, ToolResult } from './chatProvider.svelte';
|
|
@@ -51,7 +94,6 @@ export type { AdminOptions, TextTransformers, OvertimeConfig } from './options.s
|
|
|
51
94
|
export { checkError } from './hook-utils.svelte';
|
|
52
95
|
export type { NotificationConfig, OvertimeOptions, OvertimeResult, LiveSubscriptionParams } from './hook-utils.svelte';
|
|
53
96
|
|
|
54
|
-
export * from './query-keys';
|
|
55
97
|
export { HttpError, UndoError } from './types';
|
|
56
98
|
export type {
|
|
57
99
|
DataProvider, AuthProvider, NotificationProvider, MutationMode,
|
|
@@ -67,7 +109,8 @@ export type {
|
|
|
67
109
|
DeleteParams, DeleteResult,
|
|
68
110
|
DeleteManyParams, DeleteManyResult,
|
|
69
111
|
Pagination, Sort, Filter, Identity,
|
|
70
|
-
ResourceDefinition,
|
|
112
|
+
ResourceDefinition, ResourceProviderConfig, ResourceTransportConfig, ResourceAdapterConfig,
|
|
113
|
+
FieldDefinition, MenuItem,
|
|
71
114
|
AuthActionResult, CheckResult,
|
|
72
115
|
ResourceTypeMap, KnownResources, InferData,
|
|
73
116
|
BaseRecord, Role, AuditLog
|
package/src/live.svelte.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// LiveProvider — Real-time subscription interface + hooks
|
|
2
2
|
|
|
3
3
|
import { useQueryClient } from '@tanstack/svelte-query';
|
|
4
|
+
import { captureAdminContext } from './context.svelte';
|
|
5
|
+
import { dataQueryMatches } from './query-keys';
|
|
4
6
|
|
|
5
7
|
// ─── Types ──────────────────────────────────────────────────────
|
|
6
8
|
|
|
@@ -34,6 +36,7 @@ export function useLive(
|
|
|
34
36
|
options?: { liveMode?: LiveMode | (() => LiveMode); onLiveEvent?: (event: LiveEvent) => void; liveParams?: Record<string, unknown> | (() => Record<string, unknown>); dataProviderName?: string }
|
|
35
37
|
): void {
|
|
36
38
|
const queryClient = useQueryClient();
|
|
39
|
+
const adminContext = captureAdminContext();
|
|
37
40
|
|
|
38
41
|
$effect(() => {
|
|
39
42
|
const lp = typeof liveProvider === 'function' ? liveProvider() : liveProvider;
|
|
@@ -50,9 +53,10 @@ export function useLive(
|
|
|
50
53
|
callback: (event) => {
|
|
51
54
|
options?.onLiveEvent?.(event);
|
|
52
55
|
if (liveMode === 'auto') {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
const matcher = adminContext.queryKeyMatcher(res, options?.dataProviderName);
|
|
57
|
+
queryClient.invalidateQueries({
|
|
58
|
+
predicate: (q) => dataQueryMatches(q.queryKey, { ...matcher, resource: res }),
|
|
59
|
+
});
|
|
56
60
|
}
|
|
57
61
|
},
|
|
58
62
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
1
|
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
|
3
2
|
import { getAdminOptions } from './options.svelte';
|
|
4
3
|
import { captureAdminContext } from './context.svelte';
|
|
4
|
+
import { dataQueryMatches } from './query-keys';
|
|
5
|
+
import type { QueryMatcher } from './query-keys';
|
|
5
6
|
import type { AdminContextAccessor } from './context.svelte';
|
|
6
7
|
import { useParsed } from './useParsed.svelte';
|
|
7
|
-
import {
|
|
8
|
+
import { auditWithProvider } from './audit';
|
|
8
9
|
import { UndoError } from './types';
|
|
9
10
|
import type { BaseRecord, HttpError, KnownResources } from './types';
|
|
10
11
|
import { checkError, createOvertimeTracker, fireSuccessNotification, fireErrorNotification } from './hook-utils.svelte';
|
|
@@ -38,38 +39,45 @@ export function publishLiveEvent(
|
|
|
38
39
|
* - update: ['list', 'many', 'detail']
|
|
39
40
|
* - delete: ['list', 'many']
|
|
40
41
|
*/
|
|
41
|
-
export
|
|
42
|
-
queryClient: ReturnType<typeof useQueryClient
|
|
43
|
-
resource: string
|
|
44
|
-
scopes
|
|
45
|
-
defaults: string[]
|
|
46
|
-
id?: string | number
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
export interface InvalidateByScopesRequest {
|
|
43
|
+
queryClient: ReturnType<typeof useQueryClient>;
|
|
44
|
+
resource: string;
|
|
45
|
+
scopes?: string[] | false;
|
|
46
|
+
defaults: string[];
|
|
47
|
+
id?: string | number;
|
|
48
|
+
matcher: Pick<QueryMatcher, 'provider' | 'tenant'>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function invalidateByScopes(request: InvalidateByScopesRequest): void {
|
|
52
|
+
const { queryClient, resource, scopes, defaults, id, matcher } = request;
|
|
49
53
|
if (scopes === false) return;
|
|
50
54
|
const effectiveScopes = (scopes && scopes.length > 0) ? scopes : defaults;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
const dataMatch = (queryKey: readonly unknown[], fields: QueryMatcher = {}) => dataQueryMatches(queryKey, {
|
|
56
|
+
...matcher,
|
|
57
|
+
resource,
|
|
58
|
+
...fields,
|
|
59
|
+
});
|
|
54
60
|
for (const scope of effectiveScopes) {
|
|
55
|
-
if (scope === 'list') queryClient.invalidateQueries({ predicate: (q) =>
|
|
56
|
-
else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) =>
|
|
57
|
-
else if ((scope === 'detail' || scope === 'one') && id != null) queryClient.invalidateQueries({ predicate: (q) =>
|
|
58
|
-
else if (scope === 'detail' || scope === 'one') queryClient.invalidateQueries({ predicate: (q) =>
|
|
59
|
-
else if (scope === 'resourceAll') queryClient.invalidateQueries({ predicate: (q) =>
|
|
61
|
+
if (scope === 'list') queryClient.invalidateQueries({ predicate: (q) => ['list', 'infiniteList', 'select', 'selectDefaults'].some((action) => dataMatch(q.queryKey, { action })) });
|
|
62
|
+
else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey, { action: 'many' }) });
|
|
63
|
+
else if ((scope === 'detail' || scope === 'one') && id != null) queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey, { action: 'one', id }) });
|
|
64
|
+
else if (scope === 'detail' || scope === 'one') queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey, { action: 'one' }) });
|
|
65
|
+
else if (scope === 'resourceAll') queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey) });
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
export function deepMerge(target:
|
|
69
|
+
export function deepMerge(target: unknown, source: unknown): unknown {
|
|
64
70
|
if (!target || typeof target !== 'object') return source;
|
|
65
71
|
if (!source || typeof source !== 'object') return target;
|
|
66
72
|
if (Array.isArray(source) || source instanceof Date) return source;
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
const targetRecord = target as Record<string, unknown>;
|
|
74
|
+
const sourceRecord = source as Record<string, unknown>;
|
|
75
|
+
const result: Record<string, unknown> = { ...targetRecord };
|
|
76
|
+
for (const [key, value] of Object.entries(sourceRecord)) {
|
|
77
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
|
|
78
|
+
result[key] = deepMerge(targetRecord[key] ?? {}, value);
|
|
71
79
|
} else {
|
|
72
|
-
result[key] =
|
|
80
|
+
result[key] = value;
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
return result;
|
|
@@ -133,21 +141,48 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
133
141
|
onMutate: options.mutationOptions?.onMutate,
|
|
134
142
|
onSuccess: (data, params, context) => {
|
|
135
143
|
const resName = params.resource ?? defaultResource;
|
|
136
|
-
fireSuccessNotification(
|
|
144
|
+
fireSuccessNotification({
|
|
145
|
+
config: params.successNotification,
|
|
146
|
+
defaultMessage: 'Created successfully',
|
|
147
|
+
data: data.data,
|
|
148
|
+
values: params.variables,
|
|
149
|
+
resource: resName,
|
|
150
|
+
provider: adminContext.notificationProvider,
|
|
151
|
+
});
|
|
137
152
|
const res = adminContext.getResource(resName);
|
|
138
153
|
const pk = res.primaryKey ?? 'id';
|
|
139
154
|
const newId = (data.data as Record<string, unknown>)[pk];
|
|
140
|
-
|
|
155
|
+
auditWithProvider(
|
|
156
|
+
{
|
|
157
|
+
action: 'create',
|
|
158
|
+
resource: resName,
|
|
159
|
+
recordId: String(newId),
|
|
160
|
+
meta: adminContext.getProviderMeta(resName),
|
|
161
|
+
},
|
|
162
|
+
adminContext.auditLogProvider,
|
|
163
|
+
);
|
|
141
164
|
publishLiveEvent(resName, 'created', newId != null ? [newId as string | number] : undefined, adminContext);
|
|
142
165
|
// Invalidation in onSuccess for create (refine pattern — no optimistic data to reconcile on error)
|
|
143
|
-
invalidateByScopes(
|
|
166
|
+
invalidateByScopes({
|
|
167
|
+
queryClient,
|
|
168
|
+
resource: resName,
|
|
169
|
+
scopes: params.invalidates,
|
|
170
|
+
defaults: ['list', 'many'],
|
|
171
|
+
matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
|
|
172
|
+
});
|
|
144
173
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
145
174
|
(options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, context);
|
|
146
175
|
}
|
|
147
176
|
},
|
|
148
177
|
onError: (error, params, context) => {
|
|
149
178
|
checkError(error, adminContext);
|
|
150
|
-
fireErrorNotification(
|
|
179
|
+
fireErrorNotification({
|
|
180
|
+
config: params.errorNotification,
|
|
181
|
+
defaultMessage: 'Create failed',
|
|
182
|
+
error,
|
|
183
|
+
resource: params.resource ?? defaultResource,
|
|
184
|
+
provider: adminContext.notificationProvider,
|
|
185
|
+
});
|
|
151
186
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
152
187
|
(options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, context);
|
|
153
188
|
}
|
|
@@ -232,12 +267,16 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
232
267
|
if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
|
|
233
268
|
const resName = params.resource ?? defaultResource;
|
|
234
269
|
const targetId = params.id ?? defaultId;
|
|
235
|
-
const
|
|
236
|
-
const
|
|
270
|
+
const matcher = adminContext.queryKeyMatcher(resName, params.dataProviderName);
|
|
271
|
+
const dataMatch = (queryKey: readonly unknown[], fields: QueryMatcher = {}) => dataQueryMatches(queryKey, {
|
|
272
|
+
...matcher,
|
|
273
|
+
resource: resName,
|
|
274
|
+
...fields,
|
|
275
|
+
});
|
|
237
276
|
|
|
238
|
-
await queryClient.cancelQueries({ predicate: (q) =>
|
|
277
|
+
await queryClient.cancelQueries({ predicate: (q) => dataMatch(q.queryKey) });
|
|
239
278
|
|
|
240
|
-
const previousQueries = queryClient.getQueriesData({ predicate: (q) =>
|
|
279
|
+
const previousQueries = queryClient.getQueriesData({ predicate: (q) => dataMatch(q.queryKey) });
|
|
241
280
|
|
|
242
281
|
const pk = adminContext.getResource(resName).primaryKey ?? 'id';
|
|
243
282
|
const updater = params.optimisticUpdateMap ?? { list: true, many: true, detail: true };
|
|
@@ -245,14 +284,17 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
245
284
|
if (updater.detail !== false) {
|
|
246
285
|
const detailFn = updater.detail;
|
|
247
286
|
if (typeof detailFn === 'function') {
|
|
248
|
-
queryClient.setQueriesData(
|
|
287
|
+
queryClient.setQueriesData(
|
|
288
|
+
{ predicate: (q) => dataMatch(q.queryKey, { action: 'one', id: targetId }) },
|
|
289
|
+
(old: unknown) => detailFn(old, params.variables, targetId),
|
|
290
|
+
);
|
|
249
291
|
} else {
|
|
250
|
-
queryClient.setQueriesData({ predicate: (q) =>
|
|
292
|
+
queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'one', id: targetId }) }, (old: Record<string, unknown> | undefined) => old ? { ...old, data: deepMerge((old as Record<string, unknown>).data || {}, params.variables) } : old);
|
|
251
293
|
}
|
|
252
294
|
}
|
|
253
295
|
|
|
254
296
|
if (updater.list !== false) {
|
|
255
|
-
queryClient.setQueriesData({ predicate: (q) =>
|
|
297
|
+
queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'list' }) }, (old: unknown) => {
|
|
256
298
|
if (typeof updater.list === 'function') return updater.list(old, params.variables, targetId);
|
|
257
299
|
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
258
300
|
const o = old as { data: Record<string, unknown>[] };
|
|
@@ -261,7 +303,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
261
303
|
}
|
|
262
304
|
|
|
263
305
|
if (updater.many !== false) {
|
|
264
|
-
queryClient.setQueriesData({ predicate: (q) =>
|
|
306
|
+
queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'many' }) }, (old: unknown) => {
|
|
265
307
|
if (typeof updater.many === 'function') return updater.many(old, params.variables, targetId);
|
|
266
308
|
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
267
309
|
const o = old as { data: Record<string, unknown>[] };
|
|
@@ -275,8 +317,23 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
275
317
|
const extractedCtx = (context as MutationContext)?._svadmin_ctx ? (context as MutationContext).userContext : context;
|
|
276
318
|
const resName = params.resource ?? defaultResource;
|
|
277
319
|
const targetId = params.id ?? defaultId;
|
|
278
|
-
fireSuccessNotification(
|
|
279
|
-
|
|
320
|
+
fireSuccessNotification({
|
|
321
|
+
config: params.successNotification,
|
|
322
|
+
defaultMessage: 'Updated successfully',
|
|
323
|
+
data: data.data,
|
|
324
|
+
values: params.variables,
|
|
325
|
+
resource: resName,
|
|
326
|
+
provider: adminContext.notificationProvider,
|
|
327
|
+
});
|
|
328
|
+
auditWithProvider(
|
|
329
|
+
{
|
|
330
|
+
action: 'update',
|
|
331
|
+
resource: resName,
|
|
332
|
+
recordId: String(targetId),
|
|
333
|
+
meta: adminContext.getProviderMeta(resName),
|
|
334
|
+
},
|
|
335
|
+
adminContext.auditLogProvider,
|
|
336
|
+
);
|
|
280
337
|
publishLiveEvent(resName, 'updated', targetId != null ? [targetId] : undefined, adminContext);
|
|
281
338
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
282
339
|
(options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
|
|
@@ -286,7 +343,14 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
286
343
|
if (error instanceof UndoError) return;
|
|
287
344
|
const resName = params.resource ?? defaultResource;
|
|
288
345
|
const targetId = params.id ?? defaultId;
|
|
289
|
-
invalidateByScopes(
|
|
346
|
+
invalidateByScopes({
|
|
347
|
+
queryClient,
|
|
348
|
+
resource: resName,
|
|
349
|
+
scopes: params.invalidates,
|
|
350
|
+
defaults: ['list', 'many', 'detail'],
|
|
351
|
+
id: targetId != null ? targetId : undefined,
|
|
352
|
+
matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
|
|
353
|
+
});
|
|
290
354
|
if (typeof options.mutationOptions?.onSettled === 'function') {
|
|
291
355
|
(options.mutationOptions.onSettled as (...a: unknown[]) => unknown)(_data, error, params, undefined);
|
|
292
356
|
}
|
|
@@ -301,7 +365,13 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
301
365
|
if (error instanceof UndoError) return;
|
|
302
366
|
checkError(error, adminContext);
|
|
303
367
|
const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
|
|
304
|
-
fireErrorNotification(
|
|
368
|
+
fireErrorNotification({
|
|
369
|
+
config: params.errorNotification,
|
|
370
|
+
defaultMessage: 'Update failed',
|
|
371
|
+
error,
|
|
372
|
+
resource: params.resource ?? defaultResource,
|
|
373
|
+
provider: adminContext.notificationProvider,
|
|
374
|
+
});
|
|
305
375
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
306
376
|
(options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, extractedCtx);
|
|
307
377
|
}
|
|
@@ -381,25 +451,29 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
381
451
|
if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
|
|
382
452
|
const resName = params.resource ?? defaultResource;
|
|
383
453
|
const targetId = params.id ?? defaultId;
|
|
384
|
-
const
|
|
385
|
-
const
|
|
454
|
+
const matcher = adminContext.queryKeyMatcher(resName, params.dataProviderName);
|
|
455
|
+
const dataMatch = (queryKey: readonly unknown[], fields: QueryMatcher = {}) => dataQueryMatches(queryKey, {
|
|
456
|
+
...matcher,
|
|
457
|
+
resource: resName,
|
|
458
|
+
...fields,
|
|
459
|
+
});
|
|
386
460
|
|
|
387
|
-
await queryClient.cancelQueries({ predicate: (q) =>
|
|
461
|
+
await queryClient.cancelQueries({ predicate: (q) => dataMatch(q.queryKey) });
|
|
388
462
|
|
|
389
|
-
const previousQueries = queryClient.getQueriesData({ predicate: (q) =>
|
|
463
|
+
const previousQueries = queryClient.getQueriesData({ predicate: (q) => dataMatch(q.queryKey) });
|
|
390
464
|
|
|
391
465
|
const pk = adminContext.getResource(resName).primaryKey ?? 'id';
|
|
392
466
|
|
|
393
|
-
if (targetId != null) queryClient.removeQueries({ predicate: (q) =>
|
|
467
|
+
if (targetId != null) queryClient.removeQueries({ predicate: (q) => dataMatch(q.queryKey, { action: 'one', id: targetId }) });
|
|
394
468
|
|
|
395
|
-
queryClient.setQueriesData({ predicate: (q) =>
|
|
469
|
+
queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'list' }) }, (old: unknown) => {
|
|
396
470
|
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
397
471
|
const o = old as { data: Record<string, unknown>[]; total?: number };
|
|
398
472
|
const filtered = o.data.filter((item) => String(item[pk]) !== String(targetId));
|
|
399
473
|
return { ...o, data: filtered, total: (o.total ?? o.data.length) - (o.data.length - filtered.length) };
|
|
400
474
|
});
|
|
401
475
|
|
|
402
|
-
queryClient.setQueriesData({ predicate: (q) =>
|
|
476
|
+
queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'many' }) }, (old: unknown) => {
|
|
403
477
|
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
404
478
|
const o = old as { data: Record<string, unknown>[] };
|
|
405
479
|
return { ...o, data: o.data.filter((item) => String(item[pk]) !== String(targetId)) };
|
|
@@ -414,11 +488,33 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
414
488
|
|
|
415
489
|
// Remove detail cache entry (refine pattern — no stale show page)
|
|
416
490
|
if (targetId != null) {
|
|
417
|
-
queryClient.removeQueries({
|
|
491
|
+
queryClient.removeQueries({
|
|
492
|
+
predicate: (q) => dataQueryMatches(q.queryKey, {
|
|
493
|
+
...adminContext.queryKeyMatcher(resName, params.dataProviderName),
|
|
494
|
+
resource: resName,
|
|
495
|
+
action: 'one',
|
|
496
|
+
id: targetId,
|
|
497
|
+
}),
|
|
498
|
+
});
|
|
418
499
|
}
|
|
419
500
|
|
|
420
|
-
fireSuccessNotification(
|
|
421
|
-
|
|
501
|
+
fireSuccessNotification({
|
|
502
|
+
config: params.successNotification,
|
|
503
|
+
defaultMessage: 'Deleted successfully',
|
|
504
|
+
data: data.data,
|
|
505
|
+
values: params.variables,
|
|
506
|
+
resource: resName,
|
|
507
|
+
provider: adminContext.notificationProvider,
|
|
508
|
+
});
|
|
509
|
+
auditWithProvider(
|
|
510
|
+
{
|
|
511
|
+
action: 'delete',
|
|
512
|
+
resource: resName,
|
|
513
|
+
recordId: String(targetId),
|
|
514
|
+
meta: adminContext.getProviderMeta(resName),
|
|
515
|
+
},
|
|
516
|
+
adminContext.auditLogProvider,
|
|
517
|
+
);
|
|
422
518
|
publishLiveEvent(resName, 'deleted', targetId != null ? [targetId] : undefined, adminContext);
|
|
423
519
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
424
520
|
(options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
|
|
@@ -428,7 +524,14 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
428
524
|
if (error instanceof UndoError) return;
|
|
429
525
|
const resName = params.resource ?? defaultResource;
|
|
430
526
|
const targetId = params.id ?? defaultId;
|
|
431
|
-
invalidateByScopes(
|
|
527
|
+
invalidateByScopes({
|
|
528
|
+
queryClient,
|
|
529
|
+
resource: resName,
|
|
530
|
+
scopes: params.invalidates,
|
|
531
|
+
defaults: ['list', 'many'],
|
|
532
|
+
id: targetId != null ? targetId : undefined,
|
|
533
|
+
matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
|
|
534
|
+
});
|
|
432
535
|
if (typeof options.mutationOptions?.onSettled === 'function') {
|
|
433
536
|
(options.mutationOptions.onSettled as (...a: unknown[]) => unknown)(_data, error, params, undefined);
|
|
434
537
|
}
|
|
@@ -443,7 +546,13 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
443
546
|
if (error instanceof UndoError) return;
|
|
444
547
|
checkError(error, adminContext);
|
|
445
548
|
const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
|
|
446
|
-
fireErrorNotification(
|
|
549
|
+
fireErrorNotification({
|
|
550
|
+
config: params.errorNotification,
|
|
551
|
+
defaultMessage: 'Delete failed',
|
|
552
|
+
error,
|
|
553
|
+
resource: params.resource ?? defaultResource,
|
|
554
|
+
provider: adminContext.notificationProvider,
|
|
555
|
+
});
|
|
447
556
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
448
557
|
(options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, extractedCtx);
|
|
449
558
|
}
|
|
@@ -17,14 +17,20 @@ export function getNotificationProvider(): NotificationProvider | null {
|
|
|
17
17
|
/**
|
|
18
18
|
* Send a notification through the configured provider, or fall back to toast.
|
|
19
19
|
*/
|
|
20
|
-
export
|
|
20
|
+
export interface NotificationParams {
|
|
21
21
|
type: 'success' | 'error' | 'warning' | 'info';
|
|
22
22
|
message: string;
|
|
23
23
|
description?: string;
|
|
24
24
|
key?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Send through an explicitly scoped provider, with the built-in toast as fallback. */
|
|
28
|
+
export function notifyWithProvider(
|
|
29
|
+
params: NotificationParams,
|
|
30
|
+
provider: NotificationProvider | null | undefined,
|
|
31
|
+
): void {
|
|
32
|
+
if (provider) {
|
|
33
|
+
provider.open(params);
|
|
28
34
|
} else {
|
|
29
35
|
const { type, message, description } = params;
|
|
30
36
|
const fullMessage = description ? `${message}: ${description}` : message;
|
|
@@ -37,6 +43,10 @@ export function notify(params: {
|
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
export function notify(params: NotificationParams): void {
|
|
47
|
+
notifyWithProvider(params, notificationProvider);
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
/**
|
|
41
51
|
* Close a notification by key (only works with custom provider).
|
|
42
52
|
*/
|