@svadmin/core 0.21.0 → 0.21.2
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/auth-hooks.svelte.ts +17 -7
- package/src/context.svelte.ts +4 -2
- package/src/data-transfer.svelte.ts +3 -3
- package/src/form-hooks.svelte.ts +67 -39
- package/src/hook-utils.svelte.ts +12 -14
- package/src/hooks.svelte.ts +41 -25
- package/src/i18n.svelte.ts +3 -1
- package/src/index.ts +1 -0
- package/src/live-sse.ts +7 -2
- package/src/live-websocket.ts +1 -4
- package/src/live.svelte.ts +2 -2
- package/src/mutation-hooks.svelte.ts +50 -12
- package/src/query-hooks.svelte.ts +104 -58
- package/src/router-provider.ts +24 -3
- package/src/router.ts +18 -5
- package/src/routing-hooks.svelte.ts +3 -2
- package/src/table-hooks.svelte.ts +8 -8
- package/src/table-state.svelte.ts +7 -6
- package/src/theme.svelte.ts +15 -13
- package/src/unsaved-changes.svelte.ts +0 -1
- package/src/url-sync.ts +44 -28
- package/src/useParsed.svelte.ts +17 -3
- package/src/utility-hooks.svelte.ts +7 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
|
2
2
|
import { getAdminOptions } from './options.svelte';
|
|
3
|
-
import { getDataProviderForResource } from './context.svelte';
|
|
3
|
+
import { getDataProviderForResource, getResource } from './context.svelte';
|
|
4
4
|
import { useParsed } from './useParsed.svelte';
|
|
5
5
|
import { audit } from './audit';
|
|
6
6
|
import { UndoError } from './types';
|
|
@@ -41,6 +41,7 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
41
41
|
TError,
|
|
42
42
|
UseCreateMutateParams<TVariables>
|
|
43
43
|
>(() => ({
|
|
44
|
+
...options.mutationOptions,
|
|
44
45
|
mutationFn: async (params) => {
|
|
45
46
|
isMutating = true;
|
|
46
47
|
const resName = params.resource ?? defaultResource;
|
|
@@ -55,16 +56,25 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
55
56
|
isMutating = false;
|
|
56
57
|
}
|
|
57
58
|
},
|
|
58
|
-
|
|
59
|
+
onMutate: (options.mutationOptions as any)?.onMutate,
|
|
60
|
+
onSuccess: (data, params, context) => {
|
|
59
61
|
const resName = params.resource ?? defaultResource;
|
|
60
62
|
if (params.invalidates !== false) {
|
|
61
63
|
queryClient.invalidateQueries({ queryKey: [resName] });
|
|
62
64
|
}
|
|
63
65
|
fireSuccessNotification(params.successNotification, 'Created successfully', data.data, params.variables, resName);
|
|
64
|
-
|
|
66
|
+
const res = getResource(resName);
|
|
67
|
+
const pk = res.primaryKey ?? 'id';
|
|
68
|
+
audit({ action: 'create', resource: resName, recordId: String((data.data as Record<string, unknown>)[pk]) });
|
|
69
|
+
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
70
|
+
(options.mutationOptions.onSuccess as Function)(data, params, context);
|
|
71
|
+
}
|
|
65
72
|
},
|
|
66
|
-
onError: (error, params) => {
|
|
73
|
+
onError: (error, params, context) => {
|
|
67
74
|
fireErrorNotification(params.errorNotification, 'Create failed', error);
|
|
75
|
+
if (typeof options.mutationOptions?.onError === 'function') {
|
|
76
|
+
(options.mutationOptions.onError as Function)(error, params, context);
|
|
77
|
+
}
|
|
68
78
|
},
|
|
69
79
|
}));
|
|
70
80
|
|
|
@@ -115,6 +125,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
115
125
|
TError,
|
|
116
126
|
UseUpdateMutateParams<TVariables>
|
|
117
127
|
>(() => ({
|
|
128
|
+
...options.mutationOptions,
|
|
118
129
|
mutationFn: async (params) => {
|
|
119
130
|
isMutating = true;
|
|
120
131
|
const resName = params.resource ?? defaultResource;
|
|
@@ -143,7 +154,10 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
143
154
|
}
|
|
144
155
|
},
|
|
145
156
|
onMutate: async (params) => {
|
|
146
|
-
|
|
157
|
+
const userOnMutate = (options.mutationOptions as any)?.onMutate;
|
|
158
|
+
const userContext = typeof userOnMutate === 'function' ? await userOnMutate(params) : undefined;
|
|
159
|
+
|
|
160
|
+
if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
|
|
147
161
|
const resName = params.resource ?? defaultResource;
|
|
148
162
|
const targetId = params.id ?? defaultId;
|
|
149
163
|
|
|
@@ -151,7 +165,9 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
151
165
|
const previousDetail = queryClient.getQueryData([resName, 'one', targetId]);
|
|
152
166
|
const previousList = queryClient.getQueriesData({ queryKey: [resName, 'list'] });
|
|
153
167
|
|
|
168
|
+
const pk = getResource(resName).primaryKey ?? 'id';
|
|
154
169
|
const updater = params.optimisticUpdateMap;
|
|
170
|
+
|
|
155
171
|
if (updater?.detail) {
|
|
156
172
|
queryClient.setQueryData([resName, 'one', targetId], (old: unknown) => updater.detail!(old, params.variables, targetId));
|
|
157
173
|
} else {
|
|
@@ -164,13 +180,14 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
164
180
|
queryClient.setQueriesData({ queryKey: [resName, 'list'] }, (old: unknown) => {
|
|
165
181
|
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
166
182
|
const o = old as { data: Record<string, unknown>[] };
|
|
167
|
-
return { ...o, data: o.data.map((item) => String(item
|
|
183
|
+
return { ...o, data: o.data.map((item) => String(item[pk]) === String(targetId) ? { ...item, ...params.variables } : item) };
|
|
168
184
|
});
|
|
169
185
|
}
|
|
170
186
|
|
|
171
|
-
return { previousDetail, previousList };
|
|
187
|
+
return { _svadmin_ctx: true, userContext, previousDetail, previousList };
|
|
172
188
|
},
|
|
173
|
-
onSuccess: (data, params) => {
|
|
189
|
+
onSuccess: (data, params, context) => {
|
|
190
|
+
const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
|
|
174
191
|
const resName = params.resource ?? defaultResource;
|
|
175
192
|
const targetId = params.id ?? defaultId;
|
|
176
193
|
if (params.invalidates !== false) {
|
|
@@ -178,8 +195,12 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
178
195
|
}
|
|
179
196
|
fireSuccessNotification(params.successNotification, 'Updated successfully', data.data, params.variables, resName);
|
|
180
197
|
audit({ action: 'update', resource: resName, recordId: String(targetId) });
|
|
198
|
+
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
199
|
+
(options.mutationOptions.onSuccess as Function)(data, params, extractedCtx);
|
|
200
|
+
}
|
|
181
201
|
},
|
|
182
202
|
onError: (error, params, context: unknown) => {
|
|
203
|
+
const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
|
|
183
204
|
const resName = params.resource ?? defaultResource;
|
|
184
205
|
const targetId = params.id ?? defaultId;
|
|
185
206
|
const ctx = context as { previousDetail?: unknown; previousList?: [unknown, unknown][] } | undefined;
|
|
@@ -189,6 +210,9 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
189
210
|
}
|
|
190
211
|
if (error instanceof UndoError) return;
|
|
191
212
|
fireErrorNotification(params.errorNotification, 'Update failed', error);
|
|
213
|
+
if (typeof options.mutationOptions?.onError === 'function') {
|
|
214
|
+
(options.mutationOptions.onError as Function)(error, params, extractedCtx);
|
|
215
|
+
}
|
|
192
216
|
},
|
|
193
217
|
}));
|
|
194
218
|
|
|
@@ -235,6 +259,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
235
259
|
TError,
|
|
236
260
|
UseDeleteMutateParams<TVariables>
|
|
237
261
|
>(() => ({
|
|
262
|
+
...options.mutationOptions,
|
|
238
263
|
mutationFn: async (params) => {
|
|
239
264
|
isMutating = true;
|
|
240
265
|
const resName = params.resource ?? defaultResource;
|
|
@@ -263,22 +288,28 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
263
288
|
}
|
|
264
289
|
},
|
|
265
290
|
onMutate: async (params) => {
|
|
266
|
-
|
|
291
|
+
const userOnMutate = (options.mutationOptions as any)?.onMutate;
|
|
292
|
+
const userContext = typeof userOnMutate === 'function' ? await userOnMutate(params) : undefined;
|
|
293
|
+
|
|
294
|
+
if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
|
|
267
295
|
const resName = params.resource ?? defaultResource;
|
|
268
296
|
const targetId = params.id ?? defaultId;
|
|
269
297
|
|
|
270
298
|
await queryClient.cancelQueries({ queryKey: [resName] });
|
|
271
299
|
const previousList = queryClient.getQueriesData({ queryKey: [resName, 'list'] });
|
|
272
300
|
|
|
301
|
+
const pk = getResource(resName).primaryKey ?? 'id';
|
|
302
|
+
|
|
273
303
|
queryClient.setQueriesData({ queryKey: [resName, 'list'] }, (old: unknown) => {
|
|
274
304
|
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
275
305
|
const o = old as { data: Record<string, unknown>[] };
|
|
276
|
-
return { ...o, data: o.data.filter((item) => String(item
|
|
306
|
+
return { ...o, data: o.data.filter((item) => String(item[pk]) !== String(targetId)) };
|
|
277
307
|
});
|
|
278
308
|
|
|
279
|
-
return { previousList };
|
|
309
|
+
return { _svadmin_ctx: true, userContext, previousList };
|
|
280
310
|
},
|
|
281
|
-
onSuccess: (data, params) => {
|
|
311
|
+
onSuccess: (data, params, context) => {
|
|
312
|
+
const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
|
|
282
313
|
const resName = params.resource ?? defaultResource;
|
|
283
314
|
const targetId = params.id ?? defaultId;
|
|
284
315
|
if (params.invalidates !== false) {
|
|
@@ -286,14 +317,21 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
286
317
|
}
|
|
287
318
|
fireSuccessNotification(params.successNotification, 'Deleted successfully', data.data, params.variables, resName);
|
|
288
319
|
audit({ action: 'delete', resource: resName, recordId: String(targetId) });
|
|
320
|
+
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
321
|
+
(options.mutationOptions.onSuccess as Function)(data, params, extractedCtx);
|
|
322
|
+
}
|
|
289
323
|
},
|
|
290
324
|
onError: (error, params, context: unknown) => {
|
|
325
|
+
const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
|
|
291
326
|
const ctx = context as { previousList?: [unknown, unknown][] } | undefined;
|
|
292
327
|
if (ctx?.previousList) {
|
|
293
328
|
ctx.previousList.forEach(([qk, data]) => queryClient.setQueryData(qk as string[], data));
|
|
294
329
|
}
|
|
295
330
|
if (error instanceof UndoError) return;
|
|
296
331
|
fireErrorNotification(params.errorNotification, 'Delete failed', error);
|
|
332
|
+
if (typeof options.mutationOptions?.onError === 'function') {
|
|
333
|
+
(options.mutationOptions.onError as Function)(error, params, extractedCtx);
|
|
334
|
+
}
|
|
297
335
|
},
|
|
298
336
|
}));
|
|
299
337
|
|
|
@@ -56,34 +56,33 @@ export interface UseListOptions<TData extends BaseRecord = BaseRecord, TError =
|
|
|
56
56
|
|
|
57
57
|
export function useList<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseListOptions<TData, TError> = {}) {
|
|
58
58
|
const parsed = useParsed();
|
|
59
|
-
const resource = options.resource ?? parsed.resource ?? '';
|
|
60
59
|
const adminOptions = getAdminOptions();
|
|
61
|
-
const provider = getDataProviderForResource(resource, options.dataProviderName);
|
|
62
60
|
|
|
63
|
-
const
|
|
61
|
+
const query = createQuery<GetListResult<TData>, TError>(() => {
|
|
62
|
+
const resource = options.resource ?? parsed.resource ?? '';
|
|
63
|
+
const queryOptions = options.queryOptions;
|
|
64
|
+
const provider = getDataProviderForResource(resource, options.dataProviderName);
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const result = await provider.getList<TData>({
|
|
66
|
+
return {
|
|
67
|
+
queryKey: [resource, 'list', options.pagination, options.sorters, options.filters, options.meta],
|
|
68
|
+
queryFn: async () => provider.getList<TData>({
|
|
69
69
|
resource,
|
|
70
70
|
pagination: options.pagination,
|
|
71
71
|
sorters: options.sorters,
|
|
72
72
|
filters: options.filters,
|
|
73
73
|
meta: options.meta,
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}));
|
|
74
|
+
}),
|
|
75
|
+
enabled: queryOptions?.enabled ?? true,
|
|
76
|
+
staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
77
|
+
gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
|
|
78
|
+
refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
82
81
|
|
|
83
82
|
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
84
83
|
|
|
85
|
-
createLiveSubscription({
|
|
86
|
-
resource,
|
|
84
|
+
createLiveSubscription(() => ({
|
|
85
|
+
resource: options.resource ?? parsed.resource ?? '',
|
|
87
86
|
liveProvider: getLiveProvider(),
|
|
88
87
|
liveMode: options.liveMode ?? adminOptions.liveMode,
|
|
89
88
|
onLiveEvent: (e) => {
|
|
@@ -91,13 +90,19 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
91
90
|
adminOptions.onLiveEvent?.(e);
|
|
92
91
|
},
|
|
93
92
|
liveParams: options.liveParams,
|
|
94
|
-
enabled: queryOptions?.enabled ?? true,
|
|
95
|
-
});
|
|
93
|
+
enabled: options.queryOptions?.enabled ?? true,
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
let lastSuccess = $state(0);
|
|
97
|
+
let lastError = $state(0);
|
|
96
98
|
|
|
97
99
|
$effect(() => {
|
|
98
|
-
if (query.isSuccess && options.successNotification) {
|
|
100
|
+
if (query.isSuccess && options.successNotification && query.dataUpdatedAt > lastSuccess) {
|
|
101
|
+
lastSuccess = query.dataUpdatedAt;
|
|
102
|
+
const resource = options.resource ?? parsed.resource ?? '';
|
|
99
103
|
fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
|
|
100
|
-
} else if (query.isError) {
|
|
104
|
+
} else if (query.isError && query.errorUpdatedAt > lastError) {
|
|
105
|
+
lastError = query.errorUpdatedAt;
|
|
101
106
|
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
|
|
102
107
|
}
|
|
103
108
|
});
|
|
@@ -123,35 +128,54 @@ export interface UseOneOptions<TData extends BaseRecord = BaseRecord, TError = H
|
|
|
123
128
|
|
|
124
129
|
export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseOneOptions<TData, TError> = {}) {
|
|
125
130
|
const parsed = useParsed();
|
|
126
|
-
const resource = options.resource ?? parsed.resource ?? '';
|
|
127
|
-
const id = options.id ?? parsed.id;
|
|
128
131
|
const adminOptions = getAdminOptions();
|
|
129
|
-
const provider = getDataProviderForResource(resource, options.dataProviderName);
|
|
130
132
|
|
|
131
|
-
const
|
|
133
|
+
const query = createQuery<GetOneResult<TData>, TError>(() => {
|
|
134
|
+
const resource = options.resource ?? parsed.resource ?? '';
|
|
135
|
+
const id = options.id ?? parsed.id;
|
|
136
|
+
const queryOptions = options.queryOptions;
|
|
137
|
+
const provider = getDataProviderForResource(resource, options.dataProviderName);
|
|
132
138
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
return {
|
|
140
|
+
queryKey: [resource, 'one', id, options.meta],
|
|
141
|
+
queryFn: async () => {
|
|
142
|
+
if (id == null) throw new Error('useOne requires an id');
|
|
143
|
+
return provider.getOne<TData>({
|
|
144
|
+
resource,
|
|
145
|
+
id,
|
|
146
|
+
meta: options.meta,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
enabled: (queryOptions?.enabled ?? true) && id != null,
|
|
150
|
+
staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
151
|
+
gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
156
|
+
|
|
157
|
+
createLiveSubscription(() => ({
|
|
158
|
+
resource: options.resource ?? parsed.resource ?? '',
|
|
159
|
+
liveProvider: getLiveProvider(),
|
|
160
|
+
liveMode: options.liveMode ?? adminOptions.liveMode,
|
|
161
|
+
onLiveEvent: (e) => {
|
|
162
|
+
options.onLiveEvent?.(e);
|
|
163
|
+
adminOptions.onLiveEvent?.(e);
|
|
143
164
|
},
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
|
|
165
|
+
liveParams: options.liveParams,
|
|
166
|
+
enabled: (options.queryOptions?.enabled ?? true) && (options.id ?? parsed.id) != null,
|
|
147
167
|
}));
|
|
148
168
|
|
|
149
|
-
|
|
169
|
+
let lastSuccess = $state(0);
|
|
170
|
+
let lastError = $state(0);
|
|
150
171
|
|
|
151
172
|
$effect(() => {
|
|
152
|
-
if (query.isSuccess && options.successNotification) {
|
|
173
|
+
if (query.isSuccess && options.successNotification && query.dataUpdatedAt > lastSuccess) {
|
|
174
|
+
lastSuccess = query.dataUpdatedAt;
|
|
175
|
+
const resource = options.resource ?? parsed.resource ?? '';
|
|
153
176
|
fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
|
|
154
|
-
} else if (query.isError) {
|
|
177
|
+
} else if (query.isError && query.errorUpdatedAt > lastError) {
|
|
178
|
+
lastError = query.errorUpdatedAt;
|
|
155
179
|
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
|
|
156
180
|
}
|
|
157
181
|
});
|
|
@@ -193,29 +217,51 @@ export interface UseManyOptions<TData extends BaseRecord = BaseRecord, TError =
|
|
|
193
217
|
}
|
|
194
218
|
|
|
195
219
|
export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseManyOptions<TData, TError>) {
|
|
196
|
-
const { resource, ids, meta, dataProviderName, queryOptions } = options;
|
|
197
220
|
const adminOptions = getAdminOptions();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
221
|
+
|
|
222
|
+
const query = createQuery<GetManyResult<TData>, TError>(() => {
|
|
223
|
+
const { resource, ids, meta, dataProviderName, queryOptions } = options;
|
|
224
|
+
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
225
|
+
return {
|
|
226
|
+
queryKey: [resource, 'many', ids, meta],
|
|
227
|
+
queryFn: async () => {
|
|
228
|
+
if (!ids.length) return { data: [] };
|
|
229
|
+
if (provider.getMany) {
|
|
230
|
+
return provider.getMany<TData>({ resource, ids, meta });
|
|
231
|
+
}
|
|
232
|
+
const results = await Promise.all(ids.map(id => provider.getOne<TData>({ resource, id, meta })));
|
|
233
|
+
return { data: results.map(r => r.data) };
|
|
234
|
+
},
|
|
235
|
+
enabled: (queryOptions?.enabled ?? true) && ids.length > 0,
|
|
236
|
+
staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
241
|
+
|
|
242
|
+
createLiveSubscription(() => ({
|
|
243
|
+
resource: options.resource,
|
|
244
|
+
liveProvider: getLiveProvider(),
|
|
245
|
+
liveMode: options.liveMode ?? adminOptions.liveMode,
|
|
246
|
+
onLiveEvent: (e) => {
|
|
247
|
+
options.onLiveEvent?.(e);
|
|
248
|
+
adminOptions.onLiveEvent?.(e);
|
|
209
249
|
},
|
|
210
|
-
|
|
211
|
-
|
|
250
|
+
liveParams: options.liveParams,
|
|
251
|
+
enabled: (options.queryOptions?.enabled ?? true) && options.ids?.length > 0,
|
|
212
252
|
}));
|
|
213
253
|
|
|
214
|
-
|
|
254
|
+
let lastSuccess = $state(0);
|
|
255
|
+
let lastError = $state(0);
|
|
215
256
|
|
|
216
257
|
$effect(() => {
|
|
217
|
-
if (query.isSuccess && options.successNotification
|
|
218
|
-
|
|
258
|
+
if (query.isSuccess && options.successNotification && query.dataUpdatedAt > lastSuccess) {
|
|
259
|
+
lastSuccess = query.dataUpdatedAt;
|
|
260
|
+
fireSuccessNotification(options.successNotification, '', query.data, undefined, options.resource);
|
|
261
|
+
} else if (query.isError && query.errorUpdatedAt > lastError) {
|
|
262
|
+
lastError = query.errorUpdatedAt;
|
|
263
|
+
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
|
|
264
|
+
}
|
|
219
265
|
});
|
|
220
266
|
|
|
221
267
|
return extendQuery(query, () => ({ overtime }));
|
package/src/router-provider.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface RouterProvider {
|
|
|
18
18
|
params: Record<string, string>;
|
|
19
19
|
pathname: string;
|
|
20
20
|
};
|
|
21
|
+
/** Formats an internal path into an href string for <a> tags */
|
|
22
|
+
formatLink?: (path: string) => string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
// ─── Hash Router (default) ──────────────────────────────────
|
|
@@ -25,21 +27,29 @@ export interface RouterProvider {
|
|
|
25
27
|
export function createHashRouterProvider(): RouterProvider {
|
|
26
28
|
return {
|
|
27
29
|
go({ to, query, type = 'push' }) {
|
|
28
|
-
|
|
30
|
+
if (typeof window === 'undefined') return;
|
|
31
|
+
let url = to;
|
|
29
32
|
if (query) {
|
|
30
33
|
const params = new URLSearchParams(query).toString();
|
|
31
34
|
if (params) url += `?${params}`;
|
|
32
35
|
}
|
|
33
36
|
if (type === 'replace') {
|
|
34
|
-
window.location.
|
|
37
|
+
const urlObj = new URL(window.location.href);
|
|
38
|
+
urlObj.hash = url;
|
|
39
|
+
window.history.replaceState(null, '', urlObj.href);
|
|
40
|
+
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
|
35
41
|
} else {
|
|
36
|
-
window.location.hash =
|
|
42
|
+
window.location.hash = url;
|
|
37
43
|
}
|
|
38
44
|
},
|
|
39
45
|
back() {
|
|
46
|
+
if (typeof window === 'undefined') return;
|
|
40
47
|
history.back();
|
|
41
48
|
},
|
|
42
49
|
parse() {
|
|
50
|
+
if (typeof window === 'undefined') {
|
|
51
|
+
return { resource: undefined, action: undefined, id: undefined, params: {}, pathname: '/' };
|
|
52
|
+
}
|
|
43
53
|
const hash = window.location.hash.slice(1) || '/';
|
|
44
54
|
const [pathname, queryString] = hash.split('?');
|
|
45
55
|
const params: Record<string, string> = {};
|
|
@@ -57,6 +67,9 @@ export function createHashRouterProvider(): RouterProvider {
|
|
|
57
67
|
pathname,
|
|
58
68
|
};
|
|
59
69
|
},
|
|
70
|
+
formatLink(path) {
|
|
71
|
+
return '#' + path.replace(/^#/, '');
|
|
72
|
+
},
|
|
60
73
|
};
|
|
61
74
|
}
|
|
62
75
|
|
|
@@ -65,6 +78,7 @@ export function createHashRouterProvider(): RouterProvider {
|
|
|
65
78
|
export function createHistoryRouterProvider(basePath = ''): RouterProvider {
|
|
66
79
|
return {
|
|
67
80
|
go({ to, query, type = 'push' }) {
|
|
81
|
+
if (typeof window === 'undefined') return;
|
|
68
82
|
let url = `${basePath}${to}`;
|
|
69
83
|
if (query) {
|
|
70
84
|
const params = new URLSearchParams(query).toString();
|
|
@@ -79,9 +93,13 @@ export function createHistoryRouterProvider(basePath = ''): RouterProvider {
|
|
|
79
93
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
80
94
|
},
|
|
81
95
|
back() {
|
|
96
|
+
if (typeof window === 'undefined') return;
|
|
82
97
|
history.back();
|
|
83
98
|
},
|
|
84
99
|
parse() {
|
|
100
|
+
if (typeof window === 'undefined') {
|
|
101
|
+
return { resource: undefined, action: undefined, id: undefined, params: {}, pathname: '/' };
|
|
102
|
+
}
|
|
85
103
|
const pathname = window.location.pathname.replace(basePath, '') || '/';
|
|
86
104
|
const params: Record<string, string> = {};
|
|
87
105
|
for (const [k, v] of new URLSearchParams(window.location.search).entries()) {
|
|
@@ -96,5 +114,8 @@ export function createHistoryRouterProvider(basePath = ''): RouterProvider {
|
|
|
96
114
|
pathname,
|
|
97
115
|
};
|
|
98
116
|
},
|
|
117
|
+
formatLink(path) {
|
|
118
|
+
return basePath + path;
|
|
119
|
+
},
|
|
99
120
|
};
|
|
100
121
|
}
|
package/src/router.ts
CHANGED
|
@@ -50,21 +50,34 @@ export function matchRoute(
|
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
* Navigate to a path. Uses RouterProvider if available, falls back to hash.
|
|
55
|
-
*/
|
|
56
53
|
export function navigate(path: string, options?: { replaceState?: boolean }): void {
|
|
57
54
|
if (_routerProvider) {
|
|
58
55
|
_routerProvider.go({ to: path, type: options?.replaceState ? 'replace' : 'push' });
|
|
59
|
-
} else {
|
|
56
|
+
} else if (typeof window !== 'undefined') {
|
|
60
57
|
window.location.hash = '#' + path.replace(/^#/, '');
|
|
61
58
|
}
|
|
62
59
|
}
|
|
63
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Format a path into an href suitable for <a> tags.
|
|
63
|
+
*/
|
|
64
|
+
export function formatLink(path: string): string {
|
|
65
|
+
if (_routerProvider?.formatLink) {
|
|
66
|
+
return _routerProvider.formatLink(path);
|
|
67
|
+
}
|
|
68
|
+
return '#' + path.replace(/^#/, '');
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
export function currentPath(): string {
|
|
65
72
|
if (_routerProvider) {
|
|
66
73
|
const parsed = _routerProvider.parse();
|
|
67
|
-
|
|
74
|
+
const pathname = parsed.pathname || '/';
|
|
75
|
+
let qs = '';
|
|
76
|
+
if (parsed.params && Object.keys(parsed.params).length > 0) {
|
|
77
|
+
qs = '?' + new URLSearchParams(parsed.params).toString();
|
|
78
|
+
}
|
|
79
|
+
return pathname + qs;
|
|
68
80
|
}
|
|
81
|
+
if (typeof window === 'undefined') return '/';
|
|
69
82
|
return window.location.hash.replace(/^#/, '') || '/';
|
|
70
83
|
}
|
|
@@ -7,13 +7,14 @@ import { useParsed } from './useParsed.svelte';
|
|
|
7
7
|
export function useGetToPath() {
|
|
8
8
|
const routerProvider = getRouterProvider();
|
|
9
9
|
|
|
10
|
-
return (options: { resource?: string; action?: 'list' | 'create' | 'edit' | 'show'; id?: string | number; meta?: Record<string, unknown> }) => {
|
|
10
|
+
return (options: { resource?: string; action?: 'list' | 'create' | 'edit' | 'show' | 'clone'; id?: string | number; meta?: Record<string, unknown> }) => {
|
|
11
11
|
const { resource, action, id } = options;
|
|
12
12
|
if (!resource) return '/';
|
|
13
13
|
let path = `/${resource}`;
|
|
14
14
|
if (action === 'create') path += '/create';
|
|
15
15
|
else if (action === 'edit' && id) path += `/edit/${id}`;
|
|
16
16
|
else if (action === 'show' && id) path += `/show/${id}`;
|
|
17
|
+
else if (action === 'clone' && id) path += `/clone/${id}`;
|
|
17
18
|
|
|
18
19
|
// Apply router mapping if the router provider defines custom path transformers
|
|
19
20
|
if (routerProvider?.go) {
|
|
@@ -30,7 +31,7 @@ export function useGo() {
|
|
|
30
31
|
return (options: { to?: string; query?: Record<string, unknown>; type?: 'push' | 'replace'; resource?: string; action?: 'list' | 'create' | 'edit' | 'show' | 'clone'; id?: string | number }) => {
|
|
31
32
|
let targetUrl = options.to;
|
|
32
33
|
if (!targetUrl && options.resource) {
|
|
33
|
-
targetUrl = getToPath({ resource: options.resource, action: options.action
|
|
34
|
+
targetUrl = getToPath({ resource: options.resource, action: options.action, id: options.id });
|
|
34
35
|
}
|
|
35
36
|
if (!targetUrl) targetUrl = '/';
|
|
36
37
|
|
|
@@ -115,9 +115,9 @@ export function useTable<
|
|
|
115
115
|
writeURLState({
|
|
116
116
|
page: pagination.current,
|
|
117
117
|
pageSize: pagination.pageSize,
|
|
118
|
-
sortField:
|
|
119
|
-
sortOrder:
|
|
120
|
-
filters:
|
|
118
|
+
sortField: currentSorters[0]?.field,
|
|
119
|
+
sortOrder: currentSorters[0]?.order,
|
|
120
|
+
filters: currentFilters,
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
123
|
}
|
|
@@ -126,12 +126,12 @@ export function useTable<
|
|
|
126
126
|
const params = new URLSearchParams();
|
|
127
127
|
if (pagination.current) params.set('page', String(pagination.current));
|
|
128
128
|
if (pagination.pageSize) params.set('pageSize', String(pagination.pageSize));
|
|
129
|
-
if (
|
|
130
|
-
params.set('
|
|
131
|
-
params.set('
|
|
129
|
+
if (currentSorters[0]) {
|
|
130
|
+
params.set('sort', currentSorters[0].field);
|
|
131
|
+
params.set('order', currentSorters[0].order);
|
|
132
132
|
}
|
|
133
|
-
if (
|
|
134
|
-
params.set('filters', JSON.stringify(
|
|
133
|
+
if (currentFilters.length > 0) {
|
|
134
|
+
params.set('filters', JSON.stringify(currentFilters));
|
|
135
135
|
}
|
|
136
136
|
return `?${params.toString()}`;
|
|
137
137
|
}
|
|
@@ -94,12 +94,13 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
94
94
|
this.currentFilters = initFilters;
|
|
95
95
|
|
|
96
96
|
// Create query
|
|
97
|
+
const _this = this;
|
|
97
98
|
const { meta, syncWithLocation: _s, dataProviderName, pagination: _p, sorters: _sr, filters: _f, ...restOptions } = options;
|
|
98
99
|
this._queryResult = useList<TData, TError>({
|
|
99
100
|
resource,
|
|
100
|
-
pagination
|
|
101
|
-
sorters
|
|
102
|
-
filters
|
|
101
|
+
get pagination() { return _this.queryPagination; },
|
|
102
|
+
get sorters() { return _this.effectiveSorters; },
|
|
103
|
+
get filters() { return _this.effectiveFilters; },
|
|
103
104
|
meta,
|
|
104
105
|
dataProviderName,
|
|
105
106
|
...restOptions,
|
|
@@ -111,9 +112,9 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
111
112
|
writeURLState({
|
|
112
113
|
page: this.pagination.current,
|
|
113
114
|
pageSize: this.pagination.pageSize,
|
|
114
|
-
sortField: this.
|
|
115
|
-
sortOrder: this.
|
|
116
|
-
filters: this.
|
|
115
|
+
sortField: this.currentSorters[0]?.field,
|
|
116
|
+
sortOrder: this.currentSorters[0]?.order,
|
|
117
|
+
filters: this.currentFilters,
|
|
117
118
|
});
|
|
118
119
|
});
|
|
119
120
|
}
|