@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
package/package.json
CHANGED
package/src/auth-hooks.svelte.ts
CHANGED
|
@@ -123,7 +123,9 @@ export function useGetIdentity() {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
if (typeof window !== 'undefined') {
|
|
127
|
+
fetch();
|
|
128
|
+
}
|
|
127
129
|
|
|
128
130
|
return {
|
|
129
131
|
get data() { return data; },
|
|
@@ -141,13 +143,19 @@ export function useIsAuthenticated() {
|
|
|
141
143
|
let isLoading = $state(true);
|
|
142
144
|
|
|
143
145
|
if (provider) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
if (typeof window !== 'undefined') {
|
|
147
|
+
provider.check().then((result: CheckResult) => {
|
|
148
|
+
isAuthenticated = result.authenticated;
|
|
149
|
+
isLoading = false;
|
|
150
|
+
}).catch(() => {
|
|
151
|
+
isAuthenticated = false;
|
|
152
|
+
isLoading = false;
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
// In SSR we assume false to prevent hydration mismatch before check
|
|
148
156
|
isAuthenticated = false;
|
|
149
157
|
isLoading = false;
|
|
150
|
-
}
|
|
158
|
+
}
|
|
151
159
|
} else {
|
|
152
160
|
// No auth provider — treat as authenticated
|
|
153
161
|
isAuthenticated = true;
|
|
@@ -230,7 +238,9 @@ export function usePermissions<T = unknown>() {
|
|
|
230
238
|
});
|
|
231
239
|
}
|
|
232
240
|
|
|
233
|
-
|
|
241
|
+
if (typeof window !== 'undefined') {
|
|
242
|
+
fetch();
|
|
243
|
+
}
|
|
234
244
|
|
|
235
245
|
return {
|
|
236
246
|
get isLoading() { return isLoading; },
|
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,12 +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
|
|
|
149
|
+
const defaultRedirectOpt = $derived(action === 'clone' ? adminOptions.redirect?.afterClone : action === 'edit' ? adminOptions.redirect?.afterEdit : adminOptions.redirect?.afterCreate);
|
|
147
150
|
const {
|
|
148
|
-
redirect:
|
|
151
|
+
redirect: redirectOption,
|
|
149
152
|
successNotification, errorNotification,
|
|
150
153
|
onMutationSuccess, onMutationError,
|
|
151
154
|
meta: hookMeta, queryMeta: hookQueryMeta, mutationMeta: hookMutationMeta,
|
|
@@ -158,13 +161,17 @@ export function useForm<
|
|
|
158
161
|
onChange: onChangeFn,
|
|
159
162
|
onSubmit: onSubmitFn,
|
|
160
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');
|
|
161
167
|
|
|
162
|
-
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
163
|
-
const parsedMeta =
|
|
168
|
+
const provider = $derived(getDataProviderForResource(resource, dataProviderName));
|
|
169
|
+
const parsedMeta = useParsed().params || {};
|
|
164
170
|
const queryMeta = { ...parsedMeta, ...hookMeta, ...hookQueryMeta };
|
|
165
171
|
const mutationMeta = { ...parsedMeta, ...hookMeta, ...hookMutationMeta };
|
|
166
172
|
|
|
167
|
-
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; }
|
|
168
175
|
|
|
169
176
|
// ─── Form values (single source of truth) ───────────────────────
|
|
170
177
|
let values = $state<TVariables>((options.defaultValues ?? {}) as TVariables);
|
|
@@ -252,30 +259,35 @@ export function useForm<
|
|
|
252
259
|
}
|
|
253
260
|
|
|
254
261
|
// ─── Query (edit/clone mode) ────────────────────────────────────
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
}));
|
|
266
273
|
|
|
267
274
|
// Auto-populate values from query data
|
|
268
|
-
let
|
|
269
|
-
|
|
275
|
+
let queryInitializedId: string | number | undefined = undefined;
|
|
276
|
+
{
|
|
270
277
|
$effect.pre(() => {
|
|
271
|
-
if (
|
|
278
|
+
if (queryInitializedId === currentId) return;
|
|
272
279
|
const data = query.data as Record<string, unknown> | undefined;
|
|
273
|
-
|
|
280
|
+
const pk = getResource(resource).primaryKey ?? 'id';
|
|
281
|
+
if (data && (currentId == null || String(data[pk]) === String(currentId) || !data[pk])) {
|
|
282
|
+
// In clone mode, strip out identifiers
|
|
283
|
+
const targetData = action === 'clone'
|
|
284
|
+
? Object.fromEntries(Object.entries(data).filter(([k]) => k !== 'id' && k !== '_id' && k !== pk))
|
|
285
|
+
: data;
|
|
274
286
|
// Merge: defaultValues < query data
|
|
275
|
-
const merged = { ...(options.defaultValues ?? {}), ...
|
|
287
|
+
const merged = { ...(options.defaultValues ?? {}), ...targetData } as TVariables;
|
|
276
288
|
values = merged;
|
|
277
289
|
initialValues = { ...merged } as TVariables;
|
|
278
|
-
|
|
290
|
+
queryInitializedId = currentId;
|
|
279
291
|
}
|
|
280
292
|
});
|
|
281
293
|
}
|
|
@@ -289,9 +301,14 @@ export function useForm<
|
|
|
289
301
|
onSuccess: (data: { data: TData }) => {
|
|
290
302
|
if (invalidateScopes !== false) queryClient.invalidateQueries({ queryKey: [resource] });
|
|
291
303
|
if (successNotification !== false) notify({ type: 'success', message: typeof successNotification === 'string' ? successNotification : t('common.createSuccess') });
|
|
292
|
-
|
|
304
|
+
const pk = getResource(resource).primaryKey ?? 'id';
|
|
305
|
+
audit({ action: 'create', resource, recordId: String((data.data as Record<string, unknown>)[pk]) });
|
|
293
306
|
onMutationSuccess?.(data);
|
|
294
|
-
if (redirectOverride !== false)
|
|
307
|
+
if (redirectOverride !== false) {
|
|
308
|
+
const newId = (data.data as Record<string, unknown>)[pk];
|
|
309
|
+
if (newId != null) currentId = newId as string | number;
|
|
310
|
+
doRedirect(redirectOverride ?? redirectDefault);
|
|
311
|
+
}
|
|
295
312
|
},
|
|
296
313
|
onError: (error: Error) => { handleHttpError(error); onMutationError?.(error); },
|
|
297
314
|
}));
|
|
@@ -310,9 +327,14 @@ export function useForm<
|
|
|
310
327
|
}));
|
|
311
328
|
|
|
312
329
|
function doRedirect(to: 'list' | 'edit' | 'show' | false) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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}`);
|
|
316
338
|
}
|
|
317
339
|
|
|
318
340
|
// ─── Submit ─────────────────────────────────────────────────────
|
|
@@ -344,7 +366,8 @@ export function useForm<
|
|
|
344
366
|
let lastAutoSaveError = $state<unknown>(null);
|
|
345
367
|
|
|
346
368
|
function triggerAutoSave() {
|
|
347
|
-
|
|
369
|
+
const currentAction = action; // read $state reactively at call time
|
|
370
|
+
if (!autoSaveOpts?.enabled || currentAction === 'create' || currentAction === 'clone') return;
|
|
348
371
|
if (autoSaveTimer) clearTimeout(autoSaveTimer);
|
|
349
372
|
|
|
350
373
|
autoSaveTimer = setTimeout(async () => {
|
|
@@ -410,11 +433,16 @@ export function useForm<
|
|
|
410
433
|
reset,
|
|
411
434
|
|
|
412
435
|
// State
|
|
413
|
-
get loading() { return query
|
|
436
|
+
get loading() { return query.isLoading ?? false; },
|
|
414
437
|
get submitting() { return createMut.isPending || updateMut.isPending; },
|
|
415
|
-
|
|
438
|
+
get action() { return action; },
|
|
439
|
+
get resource() { return resource; },
|
|
416
440
|
get id() { return currentId; },
|
|
417
|
-
|
|
441
|
+
get isDirty() { return Object.keys(tainted).length > 0; },
|
|
442
|
+
setId,
|
|
443
|
+
setAction,
|
|
444
|
+
mutationMode,
|
|
445
|
+
redirect: doRedirect,
|
|
418
446
|
|
|
419
447
|
// AutoSave
|
|
420
448
|
triggerAutoSave,
|
|
@@ -428,6 +456,6 @@ export function useForm<
|
|
|
428
456
|
|
|
429
457
|
// Raw escape hatches
|
|
430
458
|
query,
|
|
431
|
-
mutation
|
|
459
|
+
get mutation() { return action === 'edit' ? updateMut : createMut; },
|
|
432
460
|
};
|
|
433
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
|
},
|
|
@@ -328,7 +337,14 @@ export function useInvalidate() {
|
|
|
328
337
|
queryClient.invalidateQueries();
|
|
329
338
|
return;
|
|
330
339
|
}
|
|
331
|
-
|
|
340
|
+
const scopes = params.invalidates || ['resourceAll'];
|
|
341
|
+
for (const scope of scopes) {
|
|
342
|
+
if (scope === 'resourceAll') queryClient.invalidateQueries({ queryKey: [params.resource] });
|
|
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'] });
|
|
345
|
+
else if (scope === 'list') queryClient.invalidateQueries({ queryKey: [params.resource, 'list'] });
|
|
346
|
+
else if (scope === 'many') queryClient.invalidateQueries({ queryKey: [params.resource, 'many'] });
|
|
347
|
+
}
|
|
332
348
|
};
|
|
333
349
|
}
|
|
334
350
|
|
package/src/i18n.svelte.ts
CHANGED
|
@@ -46,6 +46,7 @@ const locales: Record<string, Record<string, string>> = {
|
|
|
46
46
|
'common.darkMode': '暗色模式',
|
|
47
47
|
'common.lightMode': '亮色模式',
|
|
48
48
|
'common.detail': '详情',
|
|
49
|
+
'common.show': '查看',
|
|
49
50
|
'common.loadFailed': '加载失败: {message}',
|
|
50
51
|
'common.pageNotFound': '页面未找到',
|
|
51
52
|
'common.error': '出错了',
|
|
@@ -283,6 +284,7 @@ const locales: Record<string, Record<string, string>> = {
|
|
|
283
284
|
'common.darkMode': 'Dark mode',
|
|
284
285
|
'common.lightMode': 'Light mode',
|
|
285
286
|
'common.detail': 'Detail',
|
|
287
|
+
'common.show': 'Show',
|
|
286
288
|
'common.loadFailed': 'Load failed: {message}',
|
|
287
289
|
'common.pageNotFound': 'Page not found',
|
|
288
290
|
'common.error': 'Something went wrong',
|
|
@@ -513,7 +515,7 @@ export function t(key: string, params?: Record<string, string | number>): string
|
|
|
513
515
|
|
|
514
516
|
if (params) {
|
|
515
517
|
for (const [k, v] of Object.entries(params)) {
|
|
516
|
-
text = text.
|
|
518
|
+
text = text.replaceAll(`{${k}}`, String(v));
|
|
517
519
|
}
|
|
518
520
|
}
|
|
519
521
|
return text;
|
package/src/index.ts
CHANGED
package/src/live-sse.ts
CHANGED
|
@@ -29,6 +29,7 @@ export function createSSELiveProvider(options: SSELiveProviderOptions): LiveProv
|
|
|
29
29
|
const subscribers = new Map<string, Set<Callback>>();
|
|
30
30
|
let eventSource: EventSource | null = null;
|
|
31
31
|
let status: 'connecting' | 'connected' | 'disconnected' = 'disconnected';
|
|
32
|
+
const registeredListeners = new Set<string>();
|
|
32
33
|
|
|
33
34
|
function connect() {
|
|
34
35
|
if (eventSource) return;
|
|
@@ -64,7 +65,8 @@ export function createSSELiveProvider(options: SSELiveProviderOptions): LiveProv
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function addNamedListener(resource: string) {
|
|
67
|
-
if (!eventSource) return;
|
|
68
|
+
if (!eventSource || registeredListeners.has(resource)) return;
|
|
69
|
+
registeredListeners.add(resource);
|
|
68
70
|
eventSource.addEventListener(resource, ((event: MessageEvent) => {
|
|
69
71
|
try {
|
|
70
72
|
const parsed = JSON.parse(event.data);
|
|
@@ -109,12 +111,15 @@ export function createSSELiveProvider(options: SSELiveProviderOptions): LiveProv
|
|
|
109
111
|
eventSource = null;
|
|
110
112
|
}
|
|
111
113
|
status = 'disconnected';
|
|
114
|
+
registeredListeners.clear();
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
return {
|
|
115
118
|
subscribe({ resource, callback }) {
|
|
119
|
+
let isNewResource = false;
|
|
116
120
|
if (!subscribers.has(resource)) {
|
|
117
121
|
subscribers.set(resource, new Set());
|
|
122
|
+
isNewResource = true;
|
|
118
123
|
}
|
|
119
124
|
subscribers.get(resource)!.add(callback);
|
|
120
125
|
|
|
@@ -123,7 +128,7 @@ export function createSSELiveProvider(options: SSELiveProviderOptions): LiveProv
|
|
|
123
128
|
connect();
|
|
124
129
|
}
|
|
125
130
|
// Add named listener for this resource if connected
|
|
126
|
-
if (eventSource && resource !== '*') {
|
|
131
|
+
if (eventSource && resource !== '*' && isNewResource) {
|
|
127
132
|
addNamedListener(resource);
|
|
128
133
|
}
|
|
129
134
|
|
package/src/live-websocket.ts
CHANGED
|
@@ -104,10 +104,7 @@ export function createWebSocketLiveProvider(options: WebSocketLiveProviderOption
|
|
|
104
104
|
status = 'disconnected';
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
if (typeof WebSocket !== 'undefined') {
|
|
109
|
-
connect();
|
|
110
|
-
}
|
|
107
|
+
|
|
111
108
|
|
|
112
109
|
return {
|
|
113
110
|
subscribe({ resource, callback }) {
|
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
|
|