@svadmin/core 0.22.1 → 0.23.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 +5 -0
- package/src/auth-hooks.svelte.ts +34 -24
- package/src/chatProvider.svelte.ts +8 -21
- package/src/context.svelte.ts +20 -0
- package/src/data-transfer.svelte.ts +13 -10
- package/src/form-hooks.svelte.ts +103 -43
- package/src/helpers-pure.ts +159 -0
- package/src/helpers.test.ts +177 -0
- package/src/helpers.ts +9 -118
- package/src/hook-utils.svelte.ts +27 -6
- package/src/hooks.svelte.ts +100 -17
- package/src/i18n.svelte.ts +23 -17
- package/src/index.ts +10 -3
- package/src/live.svelte.ts +35 -19
- package/src/mutation-hooks.svelte.ts +162 -73
- package/src/notification.svelte.ts +9 -5
- package/src/options.svelte.ts +42 -19
- package/src/permissions.ts +4 -0
- package/src/query-hooks.svelte.ts +70 -20
- package/src/router-provider.ts +3 -1
- package/src/router.ts +23 -2
- package/src/routing-hooks.svelte.ts +37 -12
- package/src/table-hooks.svelte.ts +61 -15
- package/src/table-state.svelte.ts +2 -1
- package/src/theme.svelte.ts +26 -24
- package/src/toast.svelte.ts +22 -11
- package/src/types.ts +3 -1
- package/src/url-sync.ts +28 -16
- package/src/utility-hooks.svelte.ts +14 -4
|
@@ -1,19 +1,86 @@
|
|
|
1
1
|
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
|
2
2
|
import { getAdminOptions } from './options.svelte';
|
|
3
|
-
import { getDataProviderForResource, getResource } from './context.svelte';
|
|
3
|
+
import { getDataProviderForResource, getResource, getLiveProvider } from './context.svelte';
|
|
4
4
|
import { useParsed } from './useParsed.svelte';
|
|
5
5
|
import { audit } from './audit';
|
|
6
6
|
import { UndoError } from './types';
|
|
7
7
|
import type { BaseRecord, HttpError, CreateParams, UpdateParams, DeleteParams, KnownResources } from './types';
|
|
8
|
-
import { createOvertimeTracker, fireSuccessNotification, fireErrorNotification } from './hook-utils.svelte';
|
|
8
|
+
import { checkError, createOvertimeTracker, fireSuccessNotification, fireErrorNotification } from './hook-utils.svelte';
|
|
9
9
|
import type { NotificationConfig, OvertimeOptions } from './hook-utils.svelte';
|
|
10
10
|
import { toast } from './toast.svelte';
|
|
11
11
|
|
|
12
|
+
// ─── Helper: publish live events after mutations ────────────────
|
|
13
|
+
|
|
14
|
+
function publishLiveEvent(resource: string, type: 'created' | 'updated' | 'deleted', ids?: (string | number)[]) {
|
|
15
|
+
try {
|
|
16
|
+
const liveProvider = getLiveProvider();
|
|
17
|
+
if (liveProvider?.publish) {
|
|
18
|
+
liveProvider.publish({
|
|
19
|
+
type: type === 'created' ? 'INSERT' : type === 'updated' ? 'UPDATE' : 'DELETE',
|
|
20
|
+
resource,
|
|
21
|
+
payload: { ids },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
} catch { /* LiveProvider not configured — skip silently */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Invalidate queries by scope. Respects per-mutation `invalidates` override.
|
|
29
|
+
* Default invalidation scopes per operation:
|
|
30
|
+
* - create: ['list', 'many']
|
|
31
|
+
* - update: ['list', 'many', 'detail']
|
|
32
|
+
* - delete: ['list', 'many']
|
|
33
|
+
*/
|
|
34
|
+
export function invalidateByScopes(
|
|
35
|
+
queryClient: ReturnType<typeof useQueryClient>,
|
|
36
|
+
resource: string,
|
|
37
|
+
scopes: string[] | false | undefined,
|
|
38
|
+
defaults: string[],
|
|
39
|
+
id?: string | number
|
|
40
|
+
): void {
|
|
41
|
+
if (scopes === false) return;
|
|
42
|
+
const effectiveScopes = (scopes && scopes.length > 0) ? scopes : defaults;
|
|
43
|
+
for (const scope of effectiveScopes) {
|
|
44
|
+
if (scope === 'list') queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === resource && (q.queryKey[2] === 'list' || q.queryKey[2] === 'infiniteList' || q.queryKey[2] === 'select') });
|
|
45
|
+
else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === resource && q.queryKey[2] === 'many' });
|
|
46
|
+
else if ((scope === 'detail' || scope === 'one') && id != null) queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === resource && q.queryKey[2] === 'one' && q.queryKey[3] === id });
|
|
47
|
+
else if (scope === 'resourceAll') queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === resource });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function deepMerge(target: any, source: any): any {
|
|
52
|
+
if (!target || typeof target !== 'object') return source;
|
|
53
|
+
if (!source || typeof source !== 'object') return source;
|
|
54
|
+
if (Array.isArray(source) || source instanceof Date) return source;
|
|
55
|
+
const result = { ...target };
|
|
56
|
+
for (const key in source) {
|
|
57
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) && !(source[key] instanceof Date)) {
|
|
58
|
+
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
59
|
+
} else {
|
|
60
|
+
result[key] = source[key];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
12
66
|
// ─── useCreate ─────────────────────────────────────────────────
|
|
13
67
|
|
|
68
|
+
interface MutationContext {
|
|
69
|
+
_svadmin_ctx?: boolean;
|
|
70
|
+
userContext?: unknown;
|
|
71
|
+
previousQueries?: [unknown, unknown][];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface MutationCallbacks {
|
|
75
|
+
onMutate?: (variables: unknown) => unknown | Promise<unknown>;
|
|
76
|
+
onSuccess?: (data: unknown, variables: unknown, context: unknown) => void;
|
|
77
|
+
onError?: (error: unknown, variables: unknown, context: unknown) => void;
|
|
78
|
+
onSettled?: (data: unknown | undefined, error: unknown | null, variables: unknown, context: unknown) => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
14
81
|
export interface UseCreateOptions {
|
|
15
82
|
resource?: KnownResources;
|
|
16
|
-
mutationOptions?:
|
|
83
|
+
mutationOptions?: MutationCallbacks;
|
|
17
84
|
overtimeOptions?: OvertimeOptions;
|
|
18
85
|
}
|
|
19
86
|
|
|
@@ -33,8 +100,7 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
33
100
|
const adminOptions = getAdminOptions();
|
|
34
101
|
const queryClient = useQueryClient();
|
|
35
102
|
|
|
36
|
-
|
|
37
|
-
const overtime = createOvertimeTracker(() => isMutating, options.overtimeOptions ?? adminOptions.overtime);
|
|
103
|
+
const overtime = createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime);
|
|
38
104
|
|
|
39
105
|
const mutation = createMutation<
|
|
40
106
|
{ data: TData },
|
|
@@ -43,34 +109,31 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
43
109
|
>(() => ({
|
|
44
110
|
...options.mutationOptions,
|
|
45
111
|
mutationFn: async (params) => {
|
|
46
|
-
isMutating = true;
|
|
47
112
|
const resName = params.resource ?? defaultResource;
|
|
48
113
|
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
49
|
-
|
|
50
|
-
return await provider.create<TData, TVariables>({
|
|
114
|
+
return await provider.create<TData, TVariables>({
|
|
51
115
|
resource: resName,
|
|
52
116
|
variables: params.variables,
|
|
53
117
|
meta: params.meta,
|
|
54
118
|
});
|
|
55
|
-
} finally {
|
|
56
|
-
isMutating = false;
|
|
57
|
-
}
|
|
58
119
|
},
|
|
59
|
-
onMutate:
|
|
120
|
+
onMutate: options.mutationOptions?.onMutate,
|
|
60
121
|
onSuccess: (data, params, context) => {
|
|
61
122
|
const resName = params.resource ?? defaultResource;
|
|
62
|
-
if (params.invalidates !== false) {
|
|
63
|
-
queryClient.invalidateQueries({ queryKey: [resName] });
|
|
64
|
-
}
|
|
65
123
|
fireSuccessNotification(params.successNotification, 'Created successfully', data.data, params.variables, resName);
|
|
66
124
|
const res = getResource(resName);
|
|
67
125
|
const pk = res.primaryKey ?? 'id';
|
|
68
|
-
|
|
126
|
+
const newId = (data.data as Record<string, unknown>)[pk];
|
|
127
|
+
audit({ action: 'create', resource: resName, recordId: String(newId) });
|
|
128
|
+
publishLiveEvent(resName, 'created', newId != null ? [newId as string | number] : undefined);
|
|
129
|
+
// Invalidation in onSuccess for create (refine pattern — no optimistic data to reconcile on error)
|
|
130
|
+
invalidateByScopes(queryClient, resName, params.invalidates, ['list', 'many']);
|
|
69
131
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
70
132
|
(options.mutationOptions.onSuccess as Function)(data, params, context);
|
|
71
133
|
}
|
|
72
134
|
},
|
|
73
135
|
onError: (error, params, context) => {
|
|
136
|
+
checkError(error);
|
|
74
137
|
fireErrorNotification(params.errorNotification, 'Create failed', error);
|
|
75
138
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
76
139
|
(options.mutationOptions.onError as Function)(error, params, context);
|
|
@@ -88,7 +151,7 @@ export interface UseUpdateOptions {
|
|
|
88
151
|
id?: string | number;
|
|
89
152
|
mutationMode?: 'pessimistic' | 'optimistic' | 'undoable';
|
|
90
153
|
undoableTimeout?: number;
|
|
91
|
-
mutationOptions?:
|
|
154
|
+
mutationOptions?: MutationCallbacks;
|
|
92
155
|
overtimeOptions?: OvertimeOptions;
|
|
93
156
|
}
|
|
94
157
|
|
|
@@ -103,8 +166,9 @@ export interface UseUpdateMutateParams<TVariables> {
|
|
|
103
166
|
dataProviderName?: string;
|
|
104
167
|
onCancel?: () => void;
|
|
105
168
|
optimisticUpdateMap?: {
|
|
106
|
-
detail?: (previous: unknown, variables: TVariables, id?: string | number) => unknown;
|
|
107
|
-
list?: (previous: unknown, variables: TVariables, id?: string | number) => unknown;
|
|
169
|
+
detail?: boolean | ((previous: unknown, variables: TVariables, id?: string | number) => unknown);
|
|
170
|
+
list?: boolean | ((previous: unknown, variables: TVariables, id?: string | number) => unknown);
|
|
171
|
+
many?: boolean | ((previous: unknown, variables: TVariables, id?: string | number) => unknown);
|
|
108
172
|
};
|
|
109
173
|
}
|
|
110
174
|
|
|
@@ -117,8 +181,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
117
181
|
const mutationMode = options.mutationMode ?? adminOptions.mutationMode ?? 'pessimistic';
|
|
118
182
|
const undoableTimeout = options.undoableTimeout ?? adminOptions.undoableTimeout ?? 5000;
|
|
119
183
|
|
|
120
|
-
|
|
121
|
-
const overtime = createOvertimeTracker(() => isMutating, options.overtimeOptions ?? adminOptions.overtime);
|
|
184
|
+
const overtime = createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime);
|
|
122
185
|
|
|
123
186
|
const mutation = createMutation<
|
|
124
187
|
{ data: TData },
|
|
@@ -127,7 +190,6 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
127
190
|
>(() => ({
|
|
128
191
|
...options.mutationOptions,
|
|
129
192
|
mutationFn: async (params) => {
|
|
130
|
-
isMutating = true;
|
|
131
193
|
const resName = params.resource ?? defaultResource;
|
|
132
194
|
const targetId = params.id ?? defaultId;
|
|
133
195
|
if (targetId == null) throw new Error('useUpdate requires an id');
|
|
@@ -142,73 +204,84 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
142
204
|
}
|
|
143
205
|
|
|
144
206
|
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
145
|
-
|
|
146
|
-
return await provider.update<TData, TVariables>({
|
|
207
|
+
return await provider.update<TData, TVariables>({
|
|
147
208
|
resource: resName,
|
|
148
209
|
id: targetId,
|
|
149
210
|
variables: params.variables,
|
|
150
211
|
meta: params.meta,
|
|
151
212
|
});
|
|
152
|
-
} finally {
|
|
153
|
-
isMutating = false;
|
|
154
|
-
}
|
|
155
213
|
},
|
|
156
214
|
onMutate: async (params) => {
|
|
157
|
-
const userOnMutate =
|
|
215
|
+
const userOnMutate = options.mutationOptions?.onMutate;
|
|
158
216
|
const userContext = typeof userOnMutate === 'function' ? await userOnMutate(params) : undefined;
|
|
159
217
|
|
|
160
218
|
if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
|
|
161
219
|
const resName = params.resource ?? defaultResource;
|
|
162
220
|
const targetId = params.id ?? defaultId;
|
|
163
221
|
|
|
164
|
-
await queryClient.cancelQueries({
|
|
165
|
-
|
|
166
|
-
const
|
|
222
|
+
await queryClient.cancelQueries({ predicate: (q) => q.queryKey[1] === resName });
|
|
223
|
+
|
|
224
|
+
const previousQueries = queryClient.getQueriesData({ predicate: (q) => q.queryKey[1] === resName });
|
|
167
225
|
|
|
168
226
|
const pk = getResource(resName).primaryKey ?? 'id';
|
|
169
|
-
const updater = params.optimisticUpdateMap;
|
|
227
|
+
const updater = params.optimisticUpdateMap ?? { list: true, many: true, detail: true };
|
|
170
228
|
|
|
171
|
-
if (updater
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
229
|
+
if (updater.detail !== false) {
|
|
230
|
+
const detailFn = updater.detail;
|
|
231
|
+
if (typeof detailFn === 'function') {
|
|
232
|
+
queryClient.setQueriesData({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === targetId }, (old: unknown) => detailFn(old, params.variables, targetId));
|
|
233
|
+
} else {
|
|
234
|
+
queryClient.setQueriesData({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === targetId }, (old: Record<string, unknown> | undefined) => old ? { ...old, data: deepMerge((old as Record<string, unknown>).data || {}, params.variables) } : old);
|
|
235
|
+
}
|
|
175
236
|
}
|
|
176
237
|
|
|
177
|
-
if (updater
|
|
178
|
-
queryClient.setQueriesData({
|
|
179
|
-
|
|
180
|
-
queryClient.setQueriesData({ queryKey: [resName, 'list'] }, (old: unknown) => {
|
|
238
|
+
if (updater.list !== false) {
|
|
239
|
+
queryClient.setQueriesData({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'list' }, (old: unknown) => {
|
|
240
|
+
if (typeof updater.list === 'function') return updater.list!(old, params.variables, targetId);
|
|
181
241
|
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
182
242
|
const o = old as { data: Record<string, unknown>[] };
|
|
183
|
-
return { ...o, data: o.data.map((item) => String(item[pk]) === String(targetId) ?
|
|
243
|
+
return { ...o, data: o.data.map((item) => String(item[pk]) === String(targetId) ? deepMerge(item, params.variables) : item) };
|
|
184
244
|
});
|
|
185
245
|
}
|
|
186
246
|
|
|
187
|
-
|
|
247
|
+
if (updater.many !== false) {
|
|
248
|
+
queryClient.setQueriesData({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'many' }, (old: unknown) => {
|
|
249
|
+
if (typeof updater.many === 'function') return updater.many!(old, params.variables, targetId);
|
|
250
|
+
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
251
|
+
const o = old as { data: Record<string, unknown>[] };
|
|
252
|
+
return { ...o, data: o.data.map((item) => String(item[pk]) === String(targetId) ? deepMerge(item, params.variables) : item) };
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { _svadmin_ctx: true, userContext, previousQueries };
|
|
188
257
|
},
|
|
189
258
|
onSuccess: (data, params, context) => {
|
|
190
|
-
const extractedCtx = (context as
|
|
259
|
+
const extractedCtx = (context as MutationContext)?._svadmin_ctx ? (context as MutationContext).userContext : context;
|
|
191
260
|
const resName = params.resource ?? defaultResource;
|
|
192
261
|
const targetId = params.id ?? defaultId;
|
|
193
|
-
if (params.invalidates !== false) {
|
|
194
|
-
queryClient.invalidateQueries({ queryKey: [resName] });
|
|
195
|
-
}
|
|
196
262
|
fireSuccessNotification(params.successNotification, 'Updated successfully', data.data, params.variables, resName);
|
|
197
263
|
audit({ action: 'update', resource: resName, recordId: String(targetId) });
|
|
264
|
+
publishLiveEvent(resName, 'updated', targetId != null ? [targetId] : undefined);
|
|
198
265
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
199
266
|
(options.mutationOptions.onSuccess as Function)(data, params, extractedCtx);
|
|
200
267
|
}
|
|
201
268
|
},
|
|
202
|
-
|
|
203
|
-
|
|
269
|
+
onSettled: (_data, error, params) => {
|
|
270
|
+
if (error instanceof UndoError) return;
|
|
204
271
|
const resName = params.resource ?? defaultResource;
|
|
205
272
|
const targetId = params.id ?? defaultId;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
273
|
+
invalidateByScopes(queryClient, resName, params.invalidates, ['list', 'many', 'detail'], targetId != null ? targetId : undefined);
|
|
274
|
+
},
|
|
275
|
+
onError: (error, params, context: unknown) => {
|
|
276
|
+
const ctx = context as MutationContext | undefined;
|
|
277
|
+
if (ctx?.previousQueries) {
|
|
278
|
+
for (const [queryKey, data] of ctx.previousQueries) {
|
|
279
|
+
queryClient.setQueryData(queryKey as string[], data);
|
|
280
|
+
}
|
|
210
281
|
}
|
|
211
282
|
if (error instanceof UndoError) return;
|
|
283
|
+
checkError(error);
|
|
284
|
+
const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
|
|
212
285
|
fireErrorNotification(params.errorNotification, 'Update failed', error);
|
|
213
286
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
214
287
|
(options.mutationOptions.onError as Function)(error, params, extractedCtx);
|
|
@@ -226,7 +299,7 @@ export interface UseDeleteOptions {
|
|
|
226
299
|
id?: string | number;
|
|
227
300
|
mutationMode?: 'pessimistic' | 'optimistic' | 'undoable';
|
|
228
301
|
undoableTimeout?: number;
|
|
229
|
-
mutationOptions?:
|
|
302
|
+
mutationOptions?: MutationCallbacks;
|
|
230
303
|
overtimeOptions?: OvertimeOptions;
|
|
231
304
|
}
|
|
232
305
|
|
|
@@ -251,8 +324,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
251
324
|
const mutationMode = options.mutationMode ?? adminOptions.mutationMode ?? 'pessimistic';
|
|
252
325
|
const undoableTimeout = options.undoableTimeout ?? adminOptions.undoableTimeout ?? 5000;
|
|
253
326
|
|
|
254
|
-
|
|
255
|
-
const overtime = createOvertimeTracker(() => isMutating, options.overtimeOptions ?? adminOptions.overtime);
|
|
327
|
+
const overtime = createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime);
|
|
256
328
|
|
|
257
329
|
const mutation = createMutation<
|
|
258
330
|
{ data: TData },
|
|
@@ -261,7 +333,6 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
261
333
|
>(() => ({
|
|
262
334
|
...options.mutationOptions,
|
|
263
335
|
mutationFn: async (params) => {
|
|
264
|
-
isMutating = true;
|
|
265
336
|
const resName = params.resource ?? defaultResource;
|
|
266
337
|
const targetId = params.id ?? defaultId;
|
|
267
338
|
if (targetId == null) throw new Error('useDelete requires an id');
|
|
@@ -276,58 +347,76 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
276
347
|
}
|
|
277
348
|
|
|
278
349
|
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
279
|
-
|
|
280
|
-
return await provider.deleteOne<TData, TVariables>({
|
|
350
|
+
return await provider.deleteOne<TData, TVariables>({
|
|
281
351
|
resource: resName,
|
|
282
352
|
id: targetId,
|
|
283
353
|
variables: params.variables,
|
|
284
354
|
meta: params.meta,
|
|
285
355
|
});
|
|
286
|
-
} finally {
|
|
287
|
-
isMutating = false;
|
|
288
|
-
}
|
|
289
356
|
},
|
|
290
357
|
onMutate: async (params) => {
|
|
291
|
-
const userOnMutate =
|
|
358
|
+
const userOnMutate = options.mutationOptions?.onMutate;
|
|
292
359
|
const userContext = typeof userOnMutate === 'function' ? await userOnMutate(params) : undefined;
|
|
293
360
|
|
|
294
361
|
if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
|
|
295
362
|
const resName = params.resource ?? defaultResource;
|
|
296
363
|
const targetId = params.id ?? defaultId;
|
|
297
364
|
|
|
298
|
-
await queryClient.cancelQueries({
|
|
299
|
-
|
|
365
|
+
await queryClient.cancelQueries({ predicate: (q) => q.queryKey[1] === resName });
|
|
366
|
+
|
|
367
|
+
const previousQueries = queryClient.getQueriesData({ predicate: (q) => q.queryKey[1] === resName });
|
|
300
368
|
|
|
301
369
|
const pk = getResource(resName).primaryKey ?? 'id';
|
|
302
370
|
|
|
303
|
-
queryClient.
|
|
371
|
+
if (targetId != null) queryClient.removeQueries({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === targetId });
|
|
372
|
+
|
|
373
|
+
queryClient.setQueriesData({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'list' }, (old: unknown) => {
|
|
374
|
+
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
375
|
+
const o = old as { data: Record<string, unknown>[]; total?: number };
|
|
376
|
+
const filtered = o.data.filter((item) => String(item[pk]) !== String(targetId));
|
|
377
|
+
return { ...o, data: filtered, total: (o.total ?? o.data.length) - (o.data.length - filtered.length) };
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
queryClient.setQueriesData({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'many' }, (old: unknown) => {
|
|
304
381
|
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
305
382
|
const o = old as { data: Record<string, unknown>[] };
|
|
306
383
|
return { ...o, data: o.data.filter((item) => String(item[pk]) !== String(targetId)) };
|
|
307
384
|
});
|
|
308
385
|
|
|
309
|
-
return { _svadmin_ctx: true, userContext,
|
|
386
|
+
return { _svadmin_ctx: true, userContext, previousQueries };
|
|
310
387
|
},
|
|
311
388
|
onSuccess: (data, params, context) => {
|
|
312
|
-
const extractedCtx = (context as
|
|
389
|
+
const extractedCtx = (context as MutationContext)?._svadmin_ctx ? (context as MutationContext).userContext : context;
|
|
313
390
|
const resName = params.resource ?? defaultResource;
|
|
314
391
|
const targetId = params.id ?? defaultId;
|
|
315
|
-
|
|
316
|
-
|
|
392
|
+
|
|
393
|
+
// Remove detail cache entry (refine pattern — no stale show page)
|
|
394
|
+
if (targetId != null) {
|
|
395
|
+
queryClient.removeQueries({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === targetId });
|
|
317
396
|
}
|
|
397
|
+
|
|
318
398
|
fireSuccessNotification(params.successNotification, 'Deleted successfully', data.data, params.variables, resName);
|
|
319
399
|
audit({ action: 'delete', resource: resName, recordId: String(targetId) });
|
|
400
|
+
publishLiveEvent(resName, 'deleted', targetId != null ? [targetId] : undefined);
|
|
320
401
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
321
402
|
(options.mutationOptions.onSuccess as Function)(data, params, extractedCtx);
|
|
322
403
|
}
|
|
323
404
|
},
|
|
405
|
+
onSettled: (_data, error, params) => {
|
|
406
|
+
if (error instanceof UndoError) return;
|
|
407
|
+
const resName = params.resource ?? defaultResource;
|
|
408
|
+
invalidateByScopes(queryClient, resName, params.invalidates, ['list', 'many']);
|
|
409
|
+
},
|
|
324
410
|
onError: (error, params, context: unknown) => {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
411
|
+
const ctx = context as MutationContext | undefined;
|
|
412
|
+
if (ctx?.previousQueries) {
|
|
413
|
+
for (const [queryKey, data] of ctx.previousQueries) {
|
|
414
|
+
queryClient.setQueryData(queryKey as string[], data);
|
|
415
|
+
}
|
|
329
416
|
}
|
|
330
417
|
if (error instanceof UndoError) return;
|
|
418
|
+
checkError(error);
|
|
419
|
+
const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
|
|
331
420
|
fireErrorNotification(params.errorNotification, 'Delete failed', error);
|
|
332
421
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
333
422
|
(options.mutationOptions.onError as Function)(error, params, extractedCtx);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { toast } from './toast.svelte';
|
|
5
5
|
import type { NotificationProvider } from './types';
|
|
6
6
|
|
|
7
|
-
let notificationProvider
|
|
7
|
+
let notificationProvider = $state<NotificationProvider | null>(null);
|
|
8
8
|
|
|
9
9
|
export function setNotificationProvider(provider: NotificationProvider): void {
|
|
10
10
|
notificationProvider = provider;
|
|
@@ -29,10 +29,10 @@ export function notify(params: {
|
|
|
29
29
|
// Fallback to built-in toast
|
|
30
30
|
const { type, message } = params;
|
|
31
31
|
switch (type) {
|
|
32
|
-
case 'success': toast.success(message); break;
|
|
33
|
-
case 'error': toast.error(message); break;
|
|
34
|
-
case 'warning': toast.warning(message); break;
|
|
35
|
-
case 'info': toast.info(message); break;
|
|
32
|
+
case 'success': toast.success(message, undefined, { key: params.key }); break;
|
|
33
|
+
case 'error': toast.error(message, undefined, { key: params.key }); break;
|
|
34
|
+
case 'warning': toast.warning(message, undefined, { key: params.key }); break;
|
|
35
|
+
case 'info': toast.info(message, undefined, { key: params.key }); break;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -43,3 +43,7 @@ export function notify(params: {
|
|
|
43
43
|
export function closeNotification(key: string): void {
|
|
44
44
|
notificationProvider?.close(key);
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
export function resetNotificationProvider(): void {
|
|
48
|
+
notificationProvider = null;
|
|
49
|
+
}
|
package/src/options.svelte.ts
CHANGED
|
@@ -54,33 +54,56 @@ const defaultTextTransformers: TextTransformers = {
|
|
|
54
54
|
singular: (text: string) => text.endsWith('s') ? text.slice(0, -1) : text,
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
function createDefaultOptions(): AdminOptions {
|
|
58
|
+
return {
|
|
59
|
+
mutationMode: 'pessimistic',
|
|
60
|
+
warnWhenUnsavedChanges: false,
|
|
61
|
+
syncWithLocation: false,
|
|
62
|
+
liveMode: 'auto',
|
|
63
|
+
disableServerSideValidation: false,
|
|
64
|
+
undoableTimeout: 5000,
|
|
65
|
+
textTransformers: defaultTextTransformers,
|
|
66
|
+
redirect: { afterCreate: 'list', afterEdit: 'list', afterClone: 'list' },
|
|
67
|
+
breadcrumb: 'default',
|
|
68
|
+
disableRouteChangeHandler: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
69
71
|
|
|
70
|
-
let adminOptions = $state<AdminOptions>(
|
|
72
|
+
let adminOptions = $state<AdminOptions>(createDefaultOptions());
|
|
71
73
|
|
|
72
|
-
export function setAdminOptions(options: AdminOptions): void {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
export function setAdminOptions(options: AdminOptions, merge?: boolean): void {
|
|
75
|
+
if (merge) {
|
|
76
|
+
adminOptions = {
|
|
77
|
+
...adminOptions,
|
|
78
|
+
...options,
|
|
79
|
+
textTransformers: { ...adminOptions.textTransformers, ...options.textTransformers },
|
|
80
|
+
redirect: { ...adminOptions.redirect, ...options.redirect },
|
|
81
|
+
reactQuery: { ...adminOptions.reactQuery, ...options.reactQuery },
|
|
82
|
+
title: { ...adminOptions.title, ...options.title },
|
|
83
|
+
overtime: { ...adminOptions.overtime, ...options.overtime },
|
|
84
|
+
};
|
|
85
|
+
} else {
|
|
86
|
+
const defaults = createDefaultOptions();
|
|
87
|
+
adminOptions = {
|
|
88
|
+
...defaults,
|
|
89
|
+
...options,
|
|
90
|
+
textTransformers: { ...defaultTextTransformers, ...options.textTransformers },
|
|
91
|
+
redirect: { ...defaults.redirect, ...options.redirect },
|
|
92
|
+
reactQuery: { ...defaults.reactQuery, ...options.reactQuery },
|
|
93
|
+
title: { ...defaults.title, ...options.title },
|
|
94
|
+
overtime: { ...defaults.overtime, ...options.overtime },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
export function getAdminOptions(): AdminOptions {
|
|
81
100
|
return adminOptions;
|
|
82
101
|
}
|
|
83
102
|
|
|
103
|
+
export function resetAdminOptions(): void {
|
|
104
|
+
adminOptions = createDefaultOptions();
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
/** Get the resolved text transformers (always returns full set) */
|
|
85
108
|
export function getTextTransformers(): TextTransformers {
|
|
86
109
|
const opts = getAdminOptions();
|
package/src/permissions.ts
CHANGED
|
@@ -67,6 +67,10 @@ export function getAccessControlProvider(): AccessControlProvider | null {
|
|
|
67
67
|
return provider;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export function resetAccessControlProvider(): void {
|
|
71
|
+
provider = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
70
74
|
/** Get global button options from the AccessControlProvider */
|
|
71
75
|
export function getAccessControlOptions() {
|
|
72
76
|
return provider?.options ?? {};
|