@svadmin/core 0.21.1 → 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/context.svelte.ts +4 -2
- package/src/data-transfer.svelte.ts +3 -3
- package/src/form-hooks.svelte.ts +56 -37
- package/src/hook-utils.svelte.ts +12 -14
- package/src/hooks.svelte.ts +36 -25
- package/src/index.ts +1 -0
- package/src/live.svelte.ts +2 -2
- package/src/mutation-hooks.svelte.ts +28 -12
- package/src/query-hooks.svelte.ts +91 -69
- package/src/router-provider.ts +19 -0
- package/src/router.ts +10 -3
- package/src/table-state.svelte.ts +4 -3
- package/src/theme.svelte.ts +7 -5
- package/src/url-sync.ts +44 -28
- package/src/useParsed.svelte.ts +10 -2
- package/src/utility-hooks.svelte.ts +7 -1
package/package.json
CHANGED
package/src/context.svelte.ts
CHANGED
|
@@ -72,8 +72,10 @@ export function setAuthProvider(provider: AuthProvider): void {
|
|
|
72
72
|
authProvider = provider;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export function getAuthProvider(): AuthProvider
|
|
76
|
-
|
|
75
|
+
export function getAuthProvider(): AuthProvider;
|
|
76
|
+
export function getAuthProvider(options: { optional: true }): AuthProvider | null;
|
|
77
|
+
export function getAuthProvider(options?: { optional?: boolean }): AuthProvider | null {
|
|
78
|
+
if (!authProvider && !options?.optional) throw new Error('AuthProvider not found. Did you call setAuthProvider in App.svelte?');
|
|
77
79
|
return authProvider;
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -21,7 +21,7 @@ export interface UseExportOptions<TData extends BaseRecord = BaseRecord> {
|
|
|
21
21
|
*/
|
|
22
22
|
export function useExport<TData extends BaseRecord = BaseRecord>(options: UseExportOptions<TData> = {}) {
|
|
23
23
|
const parsed = useParsed();
|
|
24
|
-
const resource = options.resource ?? parsed.resource ?? '';
|
|
24
|
+
const resource = $derived(options.resource ?? parsed.resource ?? '');
|
|
25
25
|
let isLoading = $state(false);
|
|
26
26
|
|
|
27
27
|
async function triggerExport() {
|
|
@@ -55,7 +55,7 @@ export function useExport<TData extends BaseRecord = BaseRecord>(options: UseExp
|
|
|
55
55
|
? allRecords.map(options.mapData)
|
|
56
56
|
: allRecords.map(r => r as unknown as Record<string, unknown>);
|
|
57
57
|
|
|
58
|
-
if (options.download !== false) {
|
|
58
|
+
if (options.download !== false && typeof document !== 'undefined') {
|
|
59
59
|
const fields = Object.keys(mapped[0]);
|
|
60
60
|
const header = fields.join(',');
|
|
61
61
|
const rows = mapped.map(record =>
|
|
@@ -109,7 +109,7 @@ export interface UseImportOptions<TData = Record<string, unknown>> {
|
|
|
109
109
|
*/
|
|
110
110
|
export function useImport<TData = Record<string, unknown>>(options: UseImportOptions<TData> = {}) {
|
|
111
111
|
const parsed = useParsed();
|
|
112
|
-
const resource = options.resource ?? parsed.resource ?? '';
|
|
112
|
+
const resource = $derived(options.resource ?? parsed.resource ?? '');
|
|
113
113
|
let isLoading = $state(false);
|
|
114
114
|
let mutationResult = $state<{ succeeded: unknown[]; errored: { request: unknown; error: unknown }[] } | null>(null);
|
|
115
115
|
|
package/src/form-hooks.svelte.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useQueryClient } from '@tanstack/svelte-query';
|
|
2
2
|
import { useParsed } from './useParsed.svelte';
|
|
3
3
|
import { getAdminOptions } from './options.svelte';
|
|
4
|
-
import { getDataProviderForResource } from './context.svelte';
|
|
4
|
+
import { getDataProviderForResource, getResource } from './context.svelte';
|
|
5
5
|
import { createQuery, createMutation } from '@tanstack/svelte-query';
|
|
6
6
|
import { notify } from './notification.svelte';
|
|
7
7
|
import { t } from './i18n.svelte';
|
|
8
8
|
import { audit } from './audit';
|
|
9
|
-
import { navigate } from './router';
|
|
9
|
+
import { navigate, currentPath } from './router';
|
|
10
10
|
import { HttpError } from './types';
|
|
11
11
|
import type { BaseRecord, MutationMode, KnownResources } from './types';
|
|
12
12
|
|
|
@@ -110,14 +110,16 @@ export interface UseFormReturn<
|
|
|
110
110
|
/** Reset form to initial/query values. Clears tainted and errors. */
|
|
111
111
|
reset: () => void;
|
|
112
112
|
|
|
113
|
-
// ─── State
|
|
113
|
+
// ─── State ──────────────────────────────────────────────────────
|
|
114
114
|
readonly loading: boolean;
|
|
115
115
|
readonly submitting: boolean;
|
|
116
|
-
readonly resource: string;
|
|
117
116
|
readonly action: 'create' | 'edit' | 'clone';
|
|
117
|
+
readonly resource: string;
|
|
118
118
|
readonly id: string | number | undefined;
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
readonly isDirty: boolean;
|
|
120
|
+
setId: (id: string | number | undefined) => void;
|
|
121
|
+
setAction: (action: 'create' | 'edit' | 'clone') => void;
|
|
122
|
+
mutationMode: MutationMode;
|
|
121
123
|
redirect: (to: 'list' | 'edit' | 'show' | false) => void;
|
|
122
124
|
|
|
123
125
|
// ─── AutoSave ─────────────────────────────────────────────────
|
|
@@ -140,13 +142,13 @@ export function useForm<
|
|
|
140
142
|
const parsed = useParsed();
|
|
141
143
|
const adminOptions = getAdminOptions();
|
|
142
144
|
|
|
143
|
-
const resource = options.resource ?? parsed.resource ?? '';
|
|
144
|
-
|
|
145
|
+
const resource = $derived(options.resource ?? parsed.resource ?? '');
|
|
146
|
+
let action = $state<'create' | 'edit' | 'clone'>(options.action ?? (parsed.action === 'list' ? 'create' : parsed.action as 'create' | 'edit' | 'clone') ?? 'create');
|
|
145
147
|
let currentId = $state<string | number | undefined>(options.id ?? parsed.id);
|
|
146
148
|
|
|
147
|
-
const defaultRedirectOpt = action === 'clone' ? adminOptions.redirect?.afterClone : action === 'edit' ? adminOptions.redirect?.afterEdit : adminOptions.redirect?.afterCreate;
|
|
149
|
+
const defaultRedirectOpt = $derived(action === 'clone' ? adminOptions.redirect?.afterClone : action === 'edit' ? adminOptions.redirect?.afterEdit : adminOptions.redirect?.afterCreate);
|
|
148
150
|
const {
|
|
149
|
-
redirect:
|
|
151
|
+
redirect: redirectOption,
|
|
150
152
|
successNotification, errorNotification,
|
|
151
153
|
onMutationSuccess, onMutationError,
|
|
152
154
|
meta: hookMeta, queryMeta: hookQueryMeta, mutationMeta: hookMutationMeta,
|
|
@@ -159,13 +161,17 @@ export function useForm<
|
|
|
159
161
|
onChange: onChangeFn,
|
|
160
162
|
onSubmit: onSubmitFn,
|
|
161
163
|
} = options;
|
|
164
|
+
// Reactive redirect: uses $derived `defaultRedirectOpt` so redirect target
|
|
165
|
+
// updates when `action` changes at runtime (e.g., via `setAction`).
|
|
166
|
+
const redirectDefault = $derived(redirectOption ?? defaultRedirectOpt ?? 'list');
|
|
162
167
|
|
|
163
|
-
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
164
|
-
const parsedMeta =
|
|
168
|
+
const provider = $derived(getDataProviderForResource(resource, dataProviderName));
|
|
169
|
+
const parsedMeta = useParsed().params || {};
|
|
165
170
|
const queryMeta = { ...parsedMeta, ...hookMeta, ...hookQueryMeta };
|
|
166
171
|
const mutationMeta = { ...parsedMeta, ...hookMeta, ...hookMutationMeta };
|
|
167
172
|
|
|
168
|
-
function setId(newId: string | number) { currentId = newId; }
|
|
173
|
+
function setId(newId: string | number | undefined) { currentId = newId; }
|
|
174
|
+
function setAction(newAction: 'create' | 'edit' | 'clone') { action = newAction; }
|
|
169
175
|
|
|
170
176
|
// ─── Form values (single source of truth) ───────────────────────
|
|
171
177
|
let values = $state<TVariables>((options.defaultValues ?? {}) as TVariables);
|
|
@@ -253,28 +259,29 @@ export function useForm<
|
|
|
253
259
|
}
|
|
254
260
|
|
|
255
261
|
// ─── Query (edit/clone mode) ────────────────────────────────────
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
// Always create the query — use `enabled` to conditionally activate.
|
|
263
|
+
// This ensures setAction('edit') at runtime will trigger a fetch.
|
|
264
|
+
const query = createQuery(() => ({
|
|
265
|
+
queryKey: [resource, 'one', currentId],
|
|
266
|
+
queryFn: async () => {
|
|
267
|
+
const result = await provider.getOne<BaseRecord>({ resource, id: currentId!, meta: queryMeta });
|
|
268
|
+
return result.data;
|
|
269
|
+
},
|
|
270
|
+
enabled: (queryOptions?.enabled ?? true) && (action === 'edit' || action === 'clone') && currentId != null,
|
|
271
|
+
staleTime: queryOptions?.staleTime,
|
|
272
|
+
}));
|
|
267
273
|
|
|
268
274
|
// Auto-populate values from query data
|
|
269
275
|
let queryInitializedId: string | number | undefined = undefined;
|
|
270
|
-
|
|
276
|
+
{
|
|
271
277
|
$effect.pre(() => {
|
|
272
278
|
if (queryInitializedId === currentId) return;
|
|
273
279
|
const data = query.data as Record<string, unknown> | undefined;
|
|
274
|
-
|
|
280
|
+
const pk = getResource(resource).primaryKey ?? 'id';
|
|
281
|
+
if (data && (currentId == null || String(data[pk]) === String(currentId) || !data[pk])) {
|
|
275
282
|
// In clone mode, strip out identifiers
|
|
276
283
|
const targetData = action === 'clone'
|
|
277
|
-
? Object.fromEntries(Object.entries(data).filter(([k]) => k !== 'id' && k !== '_id'))
|
|
284
|
+
? Object.fromEntries(Object.entries(data).filter(([k]) => k !== 'id' && k !== '_id' && k !== pk))
|
|
278
285
|
: data;
|
|
279
286
|
// Merge: defaultValues < query data
|
|
280
287
|
const merged = { ...(options.defaultValues ?? {}), ...targetData } as TVariables;
|
|
@@ -294,10 +301,11 @@ export function useForm<
|
|
|
294
301
|
onSuccess: (data: { data: TData }) => {
|
|
295
302
|
if (invalidateScopes !== false) queryClient.invalidateQueries({ queryKey: [resource] });
|
|
296
303
|
if (successNotification !== false) notify({ type: 'success', message: typeof successNotification === 'string' ? successNotification : t('common.createSuccess') });
|
|
297
|
-
|
|
304
|
+
const pk = getResource(resource).primaryKey ?? 'id';
|
|
305
|
+
audit({ action: 'create', resource, recordId: String((data.data as Record<string, unknown>)[pk]) });
|
|
298
306
|
onMutationSuccess?.(data);
|
|
299
307
|
if (redirectOverride !== false) {
|
|
300
|
-
const newId = (data.data as Record<string, unknown>)
|
|
308
|
+
const newId = (data.data as Record<string, unknown>)[pk];
|
|
301
309
|
if (newId != null) currentId = newId as string | number;
|
|
302
310
|
doRedirect(redirectOverride ?? redirectDefault);
|
|
303
311
|
}
|
|
@@ -319,9 +327,14 @@ export function useForm<
|
|
|
319
327
|
}));
|
|
320
328
|
|
|
321
329
|
function doRedirect(to: 'list' | 'edit' | 'show' | false) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
330
|
+
const path = currentPath().split('?')[0];
|
|
331
|
+
const parts = path.split('/');
|
|
332
|
+
const resIdx = parts.lastIndexOf(String(resource));
|
|
333
|
+
const base = resIdx >= 0 ? parts.slice(0, resIdx + 1).join('/') : `/${resource}`;
|
|
334
|
+
|
|
335
|
+
if (to === 'list') navigate(base);
|
|
336
|
+
else if (to === 'edit' && currentId) navigate(`${base}/edit/${currentId}`);
|
|
337
|
+
else if (to === 'show' && currentId) navigate(`${base}/show/${currentId}`);
|
|
325
338
|
}
|
|
326
339
|
|
|
327
340
|
// ─── Submit ─────────────────────────────────────────────────────
|
|
@@ -353,7 +366,8 @@ export function useForm<
|
|
|
353
366
|
let lastAutoSaveError = $state<unknown>(null);
|
|
354
367
|
|
|
355
368
|
function triggerAutoSave() {
|
|
356
|
-
|
|
369
|
+
const currentAction = action; // read $state reactively at call time
|
|
370
|
+
if (!autoSaveOpts?.enabled || currentAction === 'create' || currentAction === 'clone') return;
|
|
357
371
|
if (autoSaveTimer) clearTimeout(autoSaveTimer);
|
|
358
372
|
|
|
359
373
|
autoSaveTimer = setTimeout(async () => {
|
|
@@ -419,11 +433,16 @@ export function useForm<
|
|
|
419
433
|
reset,
|
|
420
434
|
|
|
421
435
|
// State
|
|
422
|
-
get loading() { return query
|
|
436
|
+
get loading() { return query.isLoading ?? false; },
|
|
423
437
|
get submitting() { return createMut.isPending || updateMut.isPending; },
|
|
424
|
-
|
|
438
|
+
get action() { return action; },
|
|
439
|
+
get resource() { return resource; },
|
|
425
440
|
get id() { return currentId; },
|
|
426
|
-
|
|
441
|
+
get isDirty() { return Object.keys(tainted).length > 0; },
|
|
442
|
+
setId,
|
|
443
|
+
setAction,
|
|
444
|
+
mutationMode,
|
|
445
|
+
redirect: doRedirect,
|
|
427
446
|
|
|
428
447
|
// AutoSave
|
|
429
448
|
triggerAutoSave,
|
|
@@ -437,6 +456,6 @@ export function useForm<
|
|
|
437
456
|
|
|
438
457
|
// Raw escape hatches
|
|
439
458
|
query,
|
|
440
|
-
mutation
|
|
459
|
+
get mutation() { return action === 'edit' ? updateMut : createMut; },
|
|
441
460
|
};
|
|
442
461
|
}
|
package/src/hook-utils.svelte.ts
CHANGED
|
@@ -54,26 +54,24 @@ export interface LiveSubscriptionParams {
|
|
|
54
54
|
enabled?: boolean;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function createLiveSubscription(
|
|
58
|
-
const {
|
|
59
|
-
resource,
|
|
60
|
-
liveProvider,
|
|
61
|
-
liveMode = 'off',
|
|
62
|
-
onLiveEvent,
|
|
63
|
-
enabled = true,
|
|
64
|
-
} = params;
|
|
65
|
-
|
|
66
|
-
if (!liveProvider || liveMode === 'off' || !enabled) return;
|
|
67
|
-
|
|
57
|
+
export function createLiveSubscription(paramsFn: () => LiveSubscriptionParams): void {
|
|
68
58
|
const queryClient = useQueryClient();
|
|
69
59
|
|
|
70
60
|
$effect(() => {
|
|
61
|
+
const params = paramsFn();
|
|
62
|
+
const liveProvider = params.liveProvider;
|
|
63
|
+
const liveMode = params.liveMode ?? 'off';
|
|
64
|
+
const enabled = params.enabled ?? true;
|
|
65
|
+
|
|
66
|
+
if (!liveProvider || liveMode === 'off' || !enabled) return;
|
|
67
|
+
|
|
71
68
|
const unsubscribe = liveProvider.subscribe({
|
|
72
|
-
resource,
|
|
69
|
+
resource: params.resource,
|
|
70
|
+
liveParams: params.liveParams,
|
|
73
71
|
callback: (event: LiveEvent) => {
|
|
74
|
-
onLiveEvent?.(event);
|
|
72
|
+
params.onLiveEvent?.(event);
|
|
75
73
|
if (liveMode === 'auto') {
|
|
76
|
-
queryClient.invalidateQueries({ queryKey: [resource] });
|
|
74
|
+
queryClient.invalidateQueries({ queryKey: [params.resource] });
|
|
77
75
|
}
|
|
78
76
|
},
|
|
79
77
|
});
|
package/src/hooks.svelte.ts
CHANGED
|
@@ -54,9 +54,10 @@ export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError =
|
|
|
54
54
|
const parsed = useParsed();
|
|
55
55
|
const resource = options.resource ?? parsed.resource ?? '';
|
|
56
56
|
const adminOptions = getAdminOptions();
|
|
57
|
-
const provider = getDataProviderForResource(resource, options.dataProviderName);
|
|
58
57
|
|
|
59
|
-
const query = createInfiniteQuery<{ data: TData[]; total: number }, TError>(() =>
|
|
58
|
+
const query = createInfiniteQuery<{ data: TData[]; total: number }, TError>(() => {
|
|
59
|
+
const provider = getDataProviderForResource(resource, options.dataProviderName);
|
|
60
|
+
return {
|
|
60
61
|
queryKey: [resource, 'infiniteList', options.sorters, options.filters, options.meta],
|
|
61
62
|
queryFn: async ({ pageParam = 1 }) => {
|
|
62
63
|
const result = await provider.getList<TData>({
|
|
@@ -76,7 +77,8 @@ export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError =
|
|
|
76
77
|
},
|
|
77
78
|
enabled: options.queryOptions?.enabled ?? true,
|
|
78
79
|
staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
79
|
-
|
|
80
|
+
};
|
|
81
|
+
});
|
|
80
82
|
|
|
81
83
|
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
82
84
|
|
|
@@ -108,7 +110,6 @@ export interface UseSelectOptions<TData extends BaseRecord = BaseRecord, TOption
|
|
|
108
110
|
export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { label: string; value: string | number }>(options: UseSelectOptions<TData, TOption>) {
|
|
109
111
|
const { resource, optionLabel = 'title', optionValue = 'id', sorters, filters, pagination, meta, dataProviderName, onSearch, debounce: debounceMs = 300 } = options;
|
|
110
112
|
const adminOptions = getAdminOptions();
|
|
111
|
-
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
112
113
|
|
|
113
114
|
let searchText = $state('');
|
|
114
115
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -122,26 +123,32 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
|
|
|
122
123
|
|
|
123
124
|
const effectivePageSize = options.fetchSize ?? pagination?.pageSize ?? 999;
|
|
124
125
|
|
|
125
|
-
const query = createQuery<{ data: TData[]; total: number }>(() =>
|
|
126
|
+
const query = createQuery<{ data: TData[]; total: number }>(() => {
|
|
127
|
+
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
128
|
+
return {
|
|
126
129
|
queryKey: [resource, 'select', allFilters, sorters, pagination, meta],
|
|
127
130
|
queryFn: () => provider.getList<TData>({ resource, sorters, filters: allFilters, pagination: { current: 1, pageSize: effectivePageSize }, meta }),
|
|
128
131
|
enabled: options.queryOptions?.enabled ?? true,
|
|
129
132
|
staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
130
|
-
|
|
133
|
+
};
|
|
134
|
+
});
|
|
131
135
|
|
|
132
136
|
// Parallel query to ensure default values are always available in options
|
|
133
137
|
const defaultValueIds = options.defaultValue ?? [];
|
|
134
138
|
const defaultValueQuery = defaultValueIds.length > 0
|
|
135
|
-
? createQuery<{ data: TData[] }>(() =>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
? createQuery<{ data: TData[] }>(() => {
|
|
140
|
+
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
141
|
+
return {
|
|
142
|
+
queryKey: [resource, 'select-defaults', defaultValueIds],
|
|
143
|
+
queryFn: async () => {
|
|
144
|
+
if (provider.getMany) return provider.getMany<TData>({ resource, ids: defaultValueIds, meta });
|
|
145
|
+
const results = await Promise.all(defaultValueIds.map(id => provider.getOne<TData>({ resource, id, meta })));
|
|
146
|
+
return { data: results.map(r => r.data) };
|
|
147
|
+
},
|
|
148
|
+
enabled: (options.defaultValueQueryOptions?.enabled ?? true) && defaultValueIds.length > 0,
|
|
149
|
+
staleTime: options.defaultValueQueryOptions?.staleTime ?? Infinity,
|
|
150
|
+
};
|
|
151
|
+
})
|
|
145
152
|
: null;
|
|
146
153
|
|
|
147
154
|
const selectOptions = $derived.by(() => {
|
|
@@ -151,14 +158,14 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
|
|
|
151
158
|
const defaultQueryResult = defaultValueQuery as { data: { data: TData[] } | undefined } | null;
|
|
152
159
|
const defaultData: TData[] = defaultQueryResult?.data?.data ?? [];
|
|
153
160
|
const allData: TData[] = [...data];
|
|
154
|
-
const
|
|
161
|
+
const resolveValue = (item: TData) => typeof optionValue === 'function' ? optionValue(item) : (item as Record<string, unknown>)[optionValue];
|
|
162
|
+
const existingIds = new Set(data.map((d: TData) => String(resolveValue(d))));
|
|
155
163
|
for (const item of defaultData) {
|
|
156
|
-
|
|
157
|
-
if (!existingIds.has(itemId)) allData.push(item);
|
|
164
|
+
if (!existingIds.has(String(resolveValue(item)))) allData.push(item);
|
|
158
165
|
}
|
|
159
166
|
return allData.map((item: TData) => {
|
|
160
167
|
const label = typeof optionLabel === 'function' ? optionLabel(item) : String((item as Record<string, unknown>)[optionLabel] ?? '');
|
|
161
|
-
const value =
|
|
168
|
+
const value = resolveValue(item);
|
|
162
169
|
return { label, value } as unknown as TOption;
|
|
163
170
|
});
|
|
164
171
|
});
|
|
@@ -199,9 +206,10 @@ export interface UseCustomOptions<TData = unknown, TError = HttpError> {
|
|
|
199
206
|
|
|
200
207
|
export function useCustom<TData = unknown, TError = HttpError>(options: UseCustomOptions<TData, TError>) {
|
|
201
208
|
const adminOptions = getAdminOptions();
|
|
202
|
-
const provider = getDataProvider(options.dataProviderName);
|
|
203
209
|
|
|
204
|
-
const query = createQuery<{ data: TData }, TError>(() =>
|
|
210
|
+
const query = createQuery<{ data: TData }, TError>(() => {
|
|
211
|
+
const provider = getDataProvider(options.dataProviderName);
|
|
212
|
+
return {
|
|
205
213
|
queryKey: ['custom', options.url, options.method, options.config, options.meta],
|
|
206
214
|
queryFn: async () => {
|
|
207
215
|
if (!provider.custom) throw new Error('DataProvider does not support custom method');
|
|
@@ -218,7 +226,8 @@ export function useCustom<TData = unknown, TError = HttpError>(options: UseCusto
|
|
|
218
226
|
},
|
|
219
227
|
enabled: options.queryOptions?.enabled ?? true,
|
|
220
228
|
staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
221
|
-
|
|
229
|
+
};
|
|
230
|
+
});
|
|
222
231
|
|
|
223
232
|
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
224
233
|
|
|
@@ -228,11 +237,11 @@ export function useCustom<TData = unknown, TError = HttpError>(options: UseCusto
|
|
|
228
237
|
// ─── useCustomMutation ──────────────────────────────────────────────
|
|
229
238
|
|
|
230
239
|
export function useCustomMutation<TData = unknown, TError = HttpError, TVariables = unknown>(dataProviderName?: string) {
|
|
231
|
-
const provider = getDataProvider(dataProviderName);
|
|
232
240
|
const queryClient = useQueryClient();
|
|
233
241
|
|
|
234
242
|
const mutation = createMutation<{ data: TData }, TError, { url: string; method: 'get' | 'post' | 'put' | 'patch' | 'delete'; values?: TVariables; meta?: Record<string, unknown> }>(() => ({
|
|
235
243
|
mutationFn: async (params) => {
|
|
244
|
+
const provider = getDataProvider(dataProviderName);
|
|
236
245
|
if (!provider.custom) throw new Error('DataProvider does not support custom method');
|
|
237
246
|
return provider.custom<TData>({ url: params.url, method: params.method, payload: params.values, meta: params.meta });
|
|
238
247
|
},
|
|
@@ -331,8 +340,10 @@ export function useInvalidate() {
|
|
|
331
340
|
const scopes = params.invalidates || ['resourceAll'];
|
|
332
341
|
for (const scope of scopes) {
|
|
333
342
|
if (scope === 'resourceAll') queryClient.invalidateQueries({ queryKey: [params.resource] });
|
|
334
|
-
else if (scope === 'detail' && params.id) queryClient.invalidateQueries({ queryKey: [params.resource, 'one', params.id] });
|
|
343
|
+
else if ((scope === 'detail' || scope === 'one') && params.id) queryClient.invalidateQueries({ queryKey: [params.resource, 'one', params.id] });
|
|
344
|
+
else if (scope === 'one') queryClient.invalidateQueries({ queryKey: [params.resource, 'one'] });
|
|
335
345
|
else if (scope === 'list') queryClient.invalidateQueries({ queryKey: [params.resource, 'list'] });
|
|
346
|
+
else if (scope === 'many') queryClient.invalidateQueries({ queryKey: [params.resource, 'many'] });
|
|
336
347
|
}
|
|
337
348
|
};
|
|
338
349
|
}
|
package/src/index.ts
CHANGED
package/src/live.svelte.ts
CHANGED
|
@@ -13,8 +13,8 @@ export interface LiveEvent {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface LiveProvider {
|
|
16
|
-
subscribe(params: { resource: string; callback: (event: LiveEvent) => void }): () => void;
|
|
17
|
-
unsubscribe?(params: { resource: string }): void;
|
|
16
|
+
subscribe(params: { resource: string; liveParams?: Record<string, unknown>; callback: (event: LiveEvent) => void }): () => void;
|
|
17
|
+
unsubscribe?(params: { resource: string; liveParams?: Record<string, unknown> }): void;
|
|
18
18
|
publish?(event: LiveEvent): void;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -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';
|
|
@@ -63,7 +63,9 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
63
63
|
queryClient.invalidateQueries({ queryKey: [resName] });
|
|
64
64
|
}
|
|
65
65
|
fireSuccessNotification(params.successNotification, 'Created successfully', data.data, params.variables, resName);
|
|
66
|
-
|
|
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]) });
|
|
67
69
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
68
70
|
(options.mutationOptions.onSuccess as Function)(data, params, context);
|
|
69
71
|
}
|
|
@@ -152,7 +154,10 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
152
154
|
}
|
|
153
155
|
},
|
|
154
156
|
onMutate: async (params) => {
|
|
155
|
-
|
|
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 };
|
|
156
161
|
const resName = params.resource ?? defaultResource;
|
|
157
162
|
const targetId = params.id ?? defaultId;
|
|
158
163
|
|
|
@@ -160,7 +165,9 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
160
165
|
const previousDetail = queryClient.getQueryData([resName, 'one', targetId]);
|
|
161
166
|
const previousList = queryClient.getQueriesData({ queryKey: [resName, 'list'] });
|
|
162
167
|
|
|
168
|
+
const pk = getResource(resName).primaryKey ?? 'id';
|
|
163
169
|
const updater = params.optimisticUpdateMap;
|
|
170
|
+
|
|
164
171
|
if (updater?.detail) {
|
|
165
172
|
queryClient.setQueryData([resName, 'one', targetId], (old: unknown) => updater.detail!(old, params.variables, targetId));
|
|
166
173
|
} else {
|
|
@@ -173,13 +180,14 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
173
180
|
queryClient.setQueriesData({ queryKey: [resName, 'list'] }, (old: unknown) => {
|
|
174
181
|
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
175
182
|
const o = old as { data: Record<string, unknown>[] };
|
|
176
|
-
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) };
|
|
177
184
|
});
|
|
178
185
|
}
|
|
179
186
|
|
|
180
|
-
return { previousDetail, previousList };
|
|
187
|
+
return { _svadmin_ctx: true, userContext, previousDetail, previousList };
|
|
181
188
|
},
|
|
182
189
|
onSuccess: (data, params, context) => {
|
|
190
|
+
const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
|
|
183
191
|
const resName = params.resource ?? defaultResource;
|
|
184
192
|
const targetId = params.id ?? defaultId;
|
|
185
193
|
if (params.invalidates !== false) {
|
|
@@ -188,10 +196,11 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
188
196
|
fireSuccessNotification(params.successNotification, 'Updated successfully', data.data, params.variables, resName);
|
|
189
197
|
audit({ action: 'update', resource: resName, recordId: String(targetId) });
|
|
190
198
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
191
|
-
(options.mutationOptions.onSuccess as Function)(data, params,
|
|
199
|
+
(options.mutationOptions.onSuccess as Function)(data, params, extractedCtx);
|
|
192
200
|
}
|
|
193
201
|
},
|
|
194
202
|
onError: (error, params, context: unknown) => {
|
|
203
|
+
const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
|
|
195
204
|
const resName = params.resource ?? defaultResource;
|
|
196
205
|
const targetId = params.id ?? defaultId;
|
|
197
206
|
const ctx = context as { previousDetail?: unknown; previousList?: [unknown, unknown][] } | undefined;
|
|
@@ -202,7 +211,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
202
211
|
if (error instanceof UndoError) return;
|
|
203
212
|
fireErrorNotification(params.errorNotification, 'Update failed', error);
|
|
204
213
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
205
|
-
(options.mutationOptions.onError as Function)(error, params,
|
|
214
|
+
(options.mutationOptions.onError as Function)(error, params, extractedCtx);
|
|
206
215
|
}
|
|
207
216
|
},
|
|
208
217
|
}));
|
|
@@ -279,22 +288,28 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
279
288
|
}
|
|
280
289
|
},
|
|
281
290
|
onMutate: async (params) => {
|
|
282
|
-
|
|
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 };
|
|
283
295
|
const resName = params.resource ?? defaultResource;
|
|
284
296
|
const targetId = params.id ?? defaultId;
|
|
285
297
|
|
|
286
298
|
await queryClient.cancelQueries({ queryKey: [resName] });
|
|
287
299
|
const previousList = queryClient.getQueriesData({ queryKey: [resName, 'list'] });
|
|
288
300
|
|
|
301
|
+
const pk = getResource(resName).primaryKey ?? 'id';
|
|
302
|
+
|
|
289
303
|
queryClient.setQueriesData({ queryKey: [resName, 'list'] }, (old: unknown) => {
|
|
290
304
|
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
291
305
|
const o = old as { data: Record<string, unknown>[] };
|
|
292
|
-
return { ...o, data: o.data.filter((item) => String(item
|
|
306
|
+
return { ...o, data: o.data.filter((item) => String(item[pk]) !== String(targetId)) };
|
|
293
307
|
});
|
|
294
308
|
|
|
295
|
-
return { previousList };
|
|
309
|
+
return { _svadmin_ctx: true, userContext, previousList };
|
|
296
310
|
},
|
|
297
311
|
onSuccess: (data, params, context) => {
|
|
312
|
+
const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
|
|
298
313
|
const resName = params.resource ?? defaultResource;
|
|
299
314
|
const targetId = params.id ?? defaultId;
|
|
300
315
|
if (params.invalidates !== false) {
|
|
@@ -303,10 +318,11 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
303
318
|
fireSuccessNotification(params.successNotification, 'Deleted successfully', data.data, params.variables, resName);
|
|
304
319
|
audit({ action: 'delete', resource: resName, recordId: String(targetId) });
|
|
305
320
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
306
|
-
(options.mutationOptions.onSuccess as Function)(data, params,
|
|
321
|
+
(options.mutationOptions.onSuccess as Function)(data, params, extractedCtx);
|
|
307
322
|
}
|
|
308
323
|
},
|
|
309
324
|
onError: (error, params, context: unknown) => {
|
|
325
|
+
const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
|
|
310
326
|
const ctx = context as { previousList?: [unknown, unknown][] } | undefined;
|
|
311
327
|
if (ctx?.previousList) {
|
|
312
328
|
ctx.previousList.forEach(([qk, data]) => queryClient.setQueryData(qk as string[], data));
|
|
@@ -314,7 +330,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
314
330
|
if (error instanceof UndoError) return;
|
|
315
331
|
fireErrorNotification(params.errorNotification, 'Delete failed', error);
|
|
316
332
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
317
|
-
(options.mutationOptions.onError as Function)(error, params,
|
|
333
|
+
(options.mutationOptions.onError as Function)(error, params, extractedCtx);
|
|
318
334
|
}
|
|
319
335
|
},
|
|
320
336
|
}));
|
|
@@ -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,33 +128,34 @@ 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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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);
|
|
138
|
+
|
|
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
|
+
});
|
|
148
154
|
|
|
149
155
|
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
150
156
|
|
|
151
|
-
createLiveSubscription({
|
|
152
|
-
resource,
|
|
157
|
+
createLiveSubscription(() => ({
|
|
158
|
+
resource: options.resource ?? parsed.resource ?? '',
|
|
153
159
|
liveProvider: getLiveProvider(),
|
|
154
160
|
liveMode: options.liveMode ?? adminOptions.liveMode,
|
|
155
161
|
onLiveEvent: (e) => {
|
|
@@ -157,13 +163,19 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
157
163
|
adminOptions.onLiveEvent?.(e);
|
|
158
164
|
},
|
|
159
165
|
liveParams: options.liveParams,
|
|
160
|
-
enabled: (queryOptions?.enabled ?? true) && id != null,
|
|
161
|
-
});
|
|
166
|
+
enabled: (options.queryOptions?.enabled ?? true) && (options.id ?? parsed.id) != null,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
let lastSuccess = $state(0);
|
|
170
|
+
let lastError = $state(0);
|
|
162
171
|
|
|
163
172
|
$effect(() => {
|
|
164
|
-
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 ?? '';
|
|
165
176
|
fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
|
|
166
|
-
} else if (query.isError) {
|
|
177
|
+
} else if (query.isError && query.errorUpdatedAt > lastError) {
|
|
178
|
+
lastError = query.errorUpdatedAt;
|
|
167
179
|
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
|
|
168
180
|
}
|
|
169
181
|
});
|
|
@@ -205,28 +217,30 @@ export interface UseManyOptions<TData extends BaseRecord = BaseRecord, TError =
|
|
|
205
217
|
}
|
|
206
218
|
|
|
207
219
|
export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseManyOptions<TData, TError>) {
|
|
208
|
-
const { resource, ids, meta, dataProviderName, queryOptions } = options;
|
|
209
220
|
const adminOptions = getAdminOptions();
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
});
|
|
225
239
|
|
|
226
240
|
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
227
241
|
|
|
228
|
-
createLiveSubscription({
|
|
229
|
-
resource,
|
|
242
|
+
createLiveSubscription(() => ({
|
|
243
|
+
resource: options.resource,
|
|
230
244
|
liveProvider: getLiveProvider(),
|
|
231
245
|
liveMode: options.liveMode ?? adminOptions.liveMode,
|
|
232
246
|
onLiveEvent: (e) => {
|
|
@@ -234,12 +248,20 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
234
248
|
adminOptions.onLiveEvent?.(e);
|
|
235
249
|
},
|
|
236
250
|
liveParams: options.liveParams,
|
|
237
|
-
enabled: (queryOptions?.enabled ?? true) && ids
|
|
238
|
-
});
|
|
251
|
+
enabled: (options.queryOptions?.enabled ?? true) && options.ids?.length > 0,
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
let lastSuccess = $state(0);
|
|
255
|
+
let lastError = $state(0);
|
|
239
256
|
|
|
240
257
|
$effect(() => {
|
|
241
|
-
if (query.isSuccess && options.successNotification
|
|
242
|
-
|
|
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
|
+
}
|
|
243
265
|
});
|
|
244
266
|
|
|
245
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,6 +27,7 @@ export interface RouterProvider {
|
|
|
25
27
|
export function createHashRouterProvider(): RouterProvider {
|
|
26
28
|
return {
|
|
27
29
|
go({ to, query, type = 'push' }) {
|
|
30
|
+
if (typeof window === 'undefined') return;
|
|
28
31
|
let url = to;
|
|
29
32
|
if (query) {
|
|
30
33
|
const params = new URLSearchParams(query).toString();
|
|
@@ -34,14 +37,19 @@ export function createHashRouterProvider(): RouterProvider {
|
|
|
34
37
|
const urlObj = new URL(window.location.href);
|
|
35
38
|
urlObj.hash = url;
|
|
36
39
|
window.history.replaceState(null, '', urlObj.href);
|
|
40
|
+
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
|
37
41
|
} else {
|
|
38
42
|
window.location.hash = url;
|
|
39
43
|
}
|
|
40
44
|
},
|
|
41
45
|
back() {
|
|
46
|
+
if (typeof window === 'undefined') return;
|
|
42
47
|
history.back();
|
|
43
48
|
},
|
|
44
49
|
parse() {
|
|
50
|
+
if (typeof window === 'undefined') {
|
|
51
|
+
return { resource: undefined, action: undefined, id: undefined, params: {}, pathname: '/' };
|
|
52
|
+
}
|
|
45
53
|
const hash = window.location.hash.slice(1) || '/';
|
|
46
54
|
const [pathname, queryString] = hash.split('?');
|
|
47
55
|
const params: Record<string, string> = {};
|
|
@@ -59,6 +67,9 @@ export function createHashRouterProvider(): RouterProvider {
|
|
|
59
67
|
pathname,
|
|
60
68
|
};
|
|
61
69
|
},
|
|
70
|
+
formatLink(path) {
|
|
71
|
+
return '#' + path.replace(/^#/, '');
|
|
72
|
+
},
|
|
62
73
|
};
|
|
63
74
|
}
|
|
64
75
|
|
|
@@ -67,6 +78,7 @@ export function createHashRouterProvider(): RouterProvider {
|
|
|
67
78
|
export function createHistoryRouterProvider(basePath = ''): RouterProvider {
|
|
68
79
|
return {
|
|
69
80
|
go({ to, query, type = 'push' }) {
|
|
81
|
+
if (typeof window === 'undefined') return;
|
|
70
82
|
let url = `${basePath}${to}`;
|
|
71
83
|
if (query) {
|
|
72
84
|
const params = new URLSearchParams(query).toString();
|
|
@@ -81,9 +93,13 @@ export function createHistoryRouterProvider(basePath = ''): RouterProvider {
|
|
|
81
93
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
82
94
|
},
|
|
83
95
|
back() {
|
|
96
|
+
if (typeof window === 'undefined') return;
|
|
84
97
|
history.back();
|
|
85
98
|
},
|
|
86
99
|
parse() {
|
|
100
|
+
if (typeof window === 'undefined') {
|
|
101
|
+
return { resource: undefined, action: undefined, id: undefined, params: {}, pathname: '/' };
|
|
102
|
+
}
|
|
87
103
|
const pathname = window.location.pathname.replace(basePath, '') || '/';
|
|
88
104
|
const params: Record<string, string> = {};
|
|
89
105
|
for (const [k, v] of new URLSearchParams(window.location.search).entries()) {
|
|
@@ -98,5 +114,8 @@ export function createHistoryRouterProvider(basePath = ''): RouterProvider {
|
|
|
98
114
|
pathname,
|
|
99
115
|
};
|
|
100
116
|
},
|
|
117
|
+
formatLink(path) {
|
|
118
|
+
return basePath + path;
|
|
119
|
+
},
|
|
101
120
|
};
|
|
102
121
|
}
|
package/src/router.ts
CHANGED
|
@@ -50,9 +50,6 @@ 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' });
|
|
@@ -61,6 +58,16 @@ export function navigate(path: string, options?: { replaceState?: boolean }): vo
|
|
|
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();
|
|
@@ -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,
|
package/src/theme.svelte.ts
CHANGED
|
@@ -335,8 +335,10 @@ function applyTheme(m: ThemeMode): void {
|
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
// Apply on init
|
|
339
|
-
|
|
338
|
+
// Apply on init (browser only)
|
|
339
|
+
if (typeof window !== 'undefined') {
|
|
340
|
+
applyTheme(getStoredTheme());
|
|
341
|
+
}
|
|
340
342
|
|
|
341
343
|
// Listen for system preference changes
|
|
342
344
|
if (typeof window !== 'undefined') {
|
|
@@ -390,9 +392,9 @@ function applyColorTheme(ct: ColorTheme): void {
|
|
|
390
392
|
}
|
|
391
393
|
}
|
|
392
394
|
|
|
393
|
-
// Apply on init
|
|
394
|
-
|
|
395
|
-
|
|
395
|
+
// Apply on init (browser only)
|
|
396
|
+
if (typeof window !== 'undefined') {
|
|
397
|
+
applyColorTheme(getStoredColorTheme());
|
|
396
398
|
const initPreset = builtinPresets[getStoredColorTheme()];
|
|
397
399
|
if (initPreset) applyColorPreset(initPreset);
|
|
398
400
|
}
|
package/src/url-sync.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// URL state sync — sync pagination/sort/filters with hash params
|
|
2
|
-
|
|
3
1
|
import type { Filter } from './types';
|
|
2
|
+
import { getRouterProvider } from './context.svelte';
|
|
4
3
|
|
|
5
4
|
export interface URLState {
|
|
6
5
|
page?: number;
|
|
@@ -13,29 +12,28 @@ export interface URLState {
|
|
|
13
12
|
|
|
14
13
|
export function readURLState(): URLState {
|
|
15
14
|
if (typeof window === 'undefined') return {};
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
if (queryIdx === -1) return {};
|
|
15
|
+
const rp = getRouterProvider();
|
|
16
|
+
if (!rp) return {};
|
|
19
17
|
|
|
20
|
-
const params =
|
|
18
|
+
const { params } = rp.parse();
|
|
21
19
|
const state: URLState = {};
|
|
22
20
|
|
|
23
|
-
const page = params
|
|
21
|
+
const page = params['page'];
|
|
24
22
|
if (page) state.page = parseInt(page, 10);
|
|
25
23
|
|
|
26
|
-
const pageSize = params
|
|
24
|
+
const pageSize = params['pageSize'];
|
|
27
25
|
if (pageSize) state.pageSize = parseInt(pageSize, 10);
|
|
28
26
|
|
|
29
|
-
const sortField = params
|
|
27
|
+
const sortField = params['sort'];
|
|
30
28
|
if (sortField) state.sortField = sortField;
|
|
31
29
|
|
|
32
|
-
const sortOrder = params
|
|
30
|
+
const sortOrder = params['order'];
|
|
33
31
|
if (sortOrder === 'asc' || sortOrder === 'desc') state.sortOrder = sortOrder;
|
|
34
32
|
|
|
35
|
-
const search = params
|
|
33
|
+
const search = params['q'];
|
|
36
34
|
if (search) state.search = search;
|
|
37
35
|
|
|
38
|
-
const filtersRaw = params
|
|
36
|
+
const filtersRaw = params['filters'];
|
|
39
37
|
if (filtersRaw) {
|
|
40
38
|
try {
|
|
41
39
|
state.filters = JSON.parse(filtersRaw);
|
|
@@ -47,24 +45,42 @@ export function readURLState(): URLState {
|
|
|
47
45
|
|
|
48
46
|
export function writeURLState(state: URLState): void {
|
|
49
47
|
if (typeof window === 'undefined') return;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const params =
|
|
55
|
-
|
|
56
|
-
if (state.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (state.
|
|
48
|
+
const rp = getRouterProvider();
|
|
49
|
+
if (!rp) return;
|
|
50
|
+
|
|
51
|
+
const current = rp.parse();
|
|
52
|
+
const params: Record<string, string> = { ...current.params };
|
|
53
|
+
|
|
54
|
+
if (state.page && state.page > 1) params['page'] = String(state.page);
|
|
55
|
+
else delete params['page'];
|
|
56
|
+
|
|
57
|
+
if (state.pageSize && state.pageSize !== 10) params['pageSize'] = String(state.pageSize);
|
|
58
|
+
else delete params['pageSize'];
|
|
59
|
+
|
|
60
|
+
if (state.sortField) params['sort'] = state.sortField;
|
|
61
|
+
else delete params['sort'];
|
|
62
|
+
|
|
63
|
+
if (state.sortOrder) params['order'] = state.sortOrder;
|
|
64
|
+
else delete params['order'];
|
|
65
|
+
|
|
66
|
+
if (state.search) params['q'] = state.search;
|
|
67
|
+
else delete params['q'];
|
|
68
|
+
|
|
60
69
|
if (state.filters && state.filters.length > 0) {
|
|
61
|
-
params
|
|
70
|
+
params['filters'] = JSON.stringify(state.filters);
|
|
71
|
+
} else {
|
|
72
|
+
delete params['filters'];
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
// Prevent redundant navigation if nothing actually changed
|
|
76
|
+
const qsOld = new URLSearchParams(current.params).toString();
|
|
77
|
+
const qsNew = new URLSearchParams(params).toString();
|
|
78
|
+
|
|
79
|
+
if (qsOld !== qsNew) {
|
|
80
|
+
rp.go({
|
|
81
|
+
to: current.pathname,
|
|
82
|
+
query: params,
|
|
83
|
+
type: 'replace'
|
|
84
|
+
});
|
|
69
85
|
}
|
|
70
86
|
}
|
package/src/useParsed.svelte.ts
CHANGED
|
@@ -10,6 +10,15 @@ interface ParsedRoute {
|
|
|
10
10
|
params: Record<string, string>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
let globalPath = $state('/');
|
|
14
|
+
if (typeof window !== 'undefined') {
|
|
15
|
+
// Initialize
|
|
16
|
+
globalPath = currentPath();
|
|
17
|
+
// Listen to navigation events
|
|
18
|
+
window.addEventListener('hashchange', () => { globalPath = currentPath(); });
|
|
19
|
+
window.addEventListener('popstate', () => { globalPath = currentPath(); });
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* Parse the current hash URL into a structured route object.
|
|
15
24
|
* Returns reactive properties that update when the URL changes.
|
|
@@ -21,11 +30,10 @@ interface ParsedRoute {
|
|
|
21
30
|
* // parsed.id === '123'
|
|
22
31
|
*/
|
|
23
32
|
export function useParsed(): ParsedRoute {
|
|
24
|
-
const path = $derived(currentPath());
|
|
25
33
|
const resources = $derived((() => { try { return getResources(); } catch { return []; } })());
|
|
26
34
|
|
|
27
35
|
const parsed = $derived.by(() => {
|
|
28
|
-
const p =
|
|
36
|
+
const p = globalPath;
|
|
29
37
|
const result: ParsedRoute = { params: {} };
|
|
30
38
|
|
|
31
39
|
// Parse query params from hash
|
|
@@ -42,7 +42,13 @@ export function useModalForm<
|
|
|
42
42
|
let visible = $state(options.defaultVisible ?? false);
|
|
43
43
|
|
|
44
44
|
function show(id?: string | number) {
|
|
45
|
-
if (id !== undefined)
|
|
45
|
+
if (id !== undefined) {
|
|
46
|
+
formState.setAction('edit');
|
|
47
|
+
formState.setId(id);
|
|
48
|
+
} else {
|
|
49
|
+
formState.setAction('create');
|
|
50
|
+
formState.setId(undefined);
|
|
51
|
+
}
|
|
46
52
|
visible = true;
|
|
47
53
|
}
|
|
48
54
|
|