@svadmin/core 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/auth-hooks.svelte.ts +58 -24
- package/src/context.svelte.ts +185 -46
- package/src/data-transfer.svelte.ts +51 -41
- package/src/export-format.ts +115 -0
- package/src/form-hooks.svelte.ts +322 -58
- package/src/helpers.ts +11 -7
- package/src/hook-utils.svelte.ts +13 -12
- package/src/hooks.svelte.ts +29 -23
- package/src/http.ts +114 -33
- package/src/i18n.svelte.ts +423 -21
- package/src/index.ts +11 -8
- package/src/mutation-hooks.svelte.ts +27 -16
- package/src/permissions.svelte.ts +4 -3
- package/src/query-hooks.svelte.ts +35 -15
- package/src/router.ts +28 -8
- package/src/routing-hooks.svelte.ts +22 -18
- package/src/table-hooks.svelte.ts +5 -3
- package/src/table-state.svelte.ts +6 -3
- package/src/task-hooks.svelte.ts +27 -14
- package/src/theme.svelte.ts +248 -154
- package/src/toast.svelte.ts +1 -2
- package/src/types.ts +29 -4
- package/src/unsaved-changes.svelte.ts +4 -38
- package/src/url-sync.ts +47 -8
- package/src/useCan.ts +3 -2
- package/src/useParsed.svelte.ts +39 -15
- package/src/utility-hooks.svelte.ts +119 -7
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
|
3
3
|
import { getAdminOptions } from './options.svelte';
|
|
4
|
-
import {
|
|
4
|
+
import { captureAdminContext } from './context.svelte';
|
|
5
|
+
import type { AdminContextAccessor } from './context.svelte';
|
|
5
6
|
import { useParsed } from './useParsed.svelte';
|
|
6
7
|
import { audit } from './audit';
|
|
7
8
|
import { UndoError } from './types';
|
|
@@ -12,9 +13,14 @@ import { toast } from './toast.svelte';
|
|
|
12
13
|
|
|
13
14
|
// ─── Helper: publish live events after mutations ────────────────
|
|
14
15
|
|
|
15
|
-
export function publishLiveEvent(
|
|
16
|
+
export function publishLiveEvent(
|
|
17
|
+
resource: string,
|
|
18
|
+
type: 'created' | 'updated' | 'deleted',
|
|
19
|
+
ids?: (string | number)[],
|
|
20
|
+
adminContext: AdminContextAccessor = captureAdminContext(),
|
|
21
|
+
) {
|
|
16
22
|
try {
|
|
17
|
-
const liveProvider =
|
|
23
|
+
const liveProvider = adminContext.liveProvider;
|
|
18
24
|
if (liveProvider?.publish) {
|
|
19
25
|
liveProvider.publish({
|
|
20
26
|
type: type === 'created' ? 'INSERT' : type === 'updated' ? 'UPDATE' : 'DELETE',
|
|
@@ -42,7 +48,9 @@ export function invalidateByScopes(
|
|
|
42
48
|
): void {
|
|
43
49
|
if (scopes === false) return;
|
|
44
50
|
const effectiveScopes = (scopes && scopes.length > 0) ? scopes : defaults;
|
|
45
|
-
const dpMatch =
|
|
51
|
+
const dpMatch = dataProviderName === undefined
|
|
52
|
+
? () => true
|
|
53
|
+
: (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dataProviderName;
|
|
46
54
|
for (const scope of effectiveScopes) {
|
|
47
55
|
if (scope === 'list') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === resource && (q.queryKey[2] === 'list' || q.queryKey[2] === 'infiniteList' || q.queryKey[2] === 'select' || q.queryKey[2] === 'select-defaults') });
|
|
48
56
|
else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === resource && q.queryKey[2] === 'many' });
|
|
@@ -99,6 +107,7 @@ export interface UseCreateMutateParams<TVariables> {
|
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: UseCreateOptions = {}) {
|
|
110
|
+
const adminContext = captureAdminContext();
|
|
102
111
|
const parsed = useParsed();
|
|
103
112
|
const defaultResource = options.resource ?? parsed.resource ?? '';
|
|
104
113
|
const adminOptions = getAdminOptions();
|
|
@@ -114,7 +123,7 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
114
123
|
...options.mutationOptions,
|
|
115
124
|
mutationFn: async (params) => {
|
|
116
125
|
const resName = params.resource ?? defaultResource;
|
|
117
|
-
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
126
|
+
const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
|
|
118
127
|
return await provider.create<TData, TVariables>({
|
|
119
128
|
resource: resName,
|
|
120
129
|
variables: params.variables,
|
|
@@ -125,11 +134,11 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
125
134
|
onSuccess: (data, params, context) => {
|
|
126
135
|
const resName = params.resource ?? defaultResource;
|
|
127
136
|
fireSuccessNotification(params.successNotification, 'Created successfully', data.data, params.variables, resName);
|
|
128
|
-
const res = getResource(resName);
|
|
137
|
+
const res = adminContext.getResource(resName);
|
|
129
138
|
const pk = res.primaryKey ?? 'id';
|
|
130
139
|
const newId = (data.data as Record<string, unknown>)[pk];
|
|
131
140
|
audit({ action: 'create', resource: resName, recordId: String(newId) });
|
|
132
|
-
publishLiveEvent(resName, 'created', newId != null ? [newId as string | number] : undefined);
|
|
141
|
+
publishLiveEvent(resName, 'created', newId != null ? [newId as string | number] : undefined, adminContext);
|
|
133
142
|
// Invalidation in onSuccess for create (refine pattern — no optimistic data to reconcile on error)
|
|
134
143
|
invalidateByScopes(queryClient, resName, params.invalidates, ['list', 'many'], undefined, params.dataProviderName);
|
|
135
144
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
@@ -137,7 +146,7 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
137
146
|
}
|
|
138
147
|
},
|
|
139
148
|
onError: (error, params, context) => {
|
|
140
|
-
checkError(error);
|
|
149
|
+
checkError(error, adminContext);
|
|
141
150
|
fireErrorNotification(params.errorNotification, 'Create failed', error, params.resource ?? defaultResource);
|
|
142
151
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
143
152
|
(options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, context);
|
|
@@ -177,6 +186,7 @@ export interface UseUpdateMutateParams<TVariables> {
|
|
|
177
186
|
}
|
|
178
187
|
|
|
179
188
|
export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: UseUpdateOptions = {}) {
|
|
189
|
+
const adminContext = captureAdminContext();
|
|
180
190
|
const parsed = useParsed();
|
|
181
191
|
const defaultResource = options.resource ?? parsed.resource ?? '';
|
|
182
192
|
const defaultId = options.id ?? parsed.id;
|
|
@@ -207,7 +217,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
207
217
|
});
|
|
208
218
|
}
|
|
209
219
|
|
|
210
|
-
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
220
|
+
const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
|
|
211
221
|
return await provider.update<TData, TVariables>({
|
|
212
222
|
resource: resName,
|
|
213
223
|
id: targetId,
|
|
@@ -229,7 +239,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
229
239
|
|
|
230
240
|
const previousQueries = queryClient.getQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName });
|
|
231
241
|
|
|
232
|
-
const pk = getResource(resName).primaryKey ?? 'id';
|
|
242
|
+
const pk = adminContext.getResource(resName).primaryKey ?? 'id';
|
|
233
243
|
const updater = params.optimisticUpdateMap ?? { list: true, many: true, detail: true };
|
|
234
244
|
|
|
235
245
|
if (updater.detail !== false) {
|
|
@@ -267,7 +277,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
267
277
|
const targetId = params.id ?? defaultId;
|
|
268
278
|
fireSuccessNotification(params.successNotification, 'Updated successfully', data.data, params.variables, resName);
|
|
269
279
|
audit({ action: 'update', resource: resName, recordId: String(targetId) });
|
|
270
|
-
publishLiveEvent(resName, 'updated', targetId != null ? [targetId] : undefined);
|
|
280
|
+
publishLiveEvent(resName, 'updated', targetId != null ? [targetId] : undefined, adminContext);
|
|
271
281
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
272
282
|
(options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
|
|
273
283
|
}
|
|
@@ -289,7 +299,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
289
299
|
}
|
|
290
300
|
}
|
|
291
301
|
if (error instanceof UndoError) return;
|
|
292
|
-
checkError(error);
|
|
302
|
+
checkError(error, adminContext);
|
|
293
303
|
const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
|
|
294
304
|
fireErrorNotification(params.errorNotification, 'Update failed', error, params.resource ?? defaultResource);
|
|
295
305
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
@@ -325,6 +335,7 @@ export interface UseDeleteMutateParams<TVariables> {
|
|
|
325
335
|
}
|
|
326
336
|
|
|
327
337
|
export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: UseDeleteOptions = {}) {
|
|
338
|
+
const adminContext = captureAdminContext();
|
|
328
339
|
const parsed = useParsed();
|
|
329
340
|
const defaultResource = options.resource ?? parsed.resource ?? '';
|
|
330
341
|
const defaultId = options.id ?? parsed.id;
|
|
@@ -355,7 +366,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
355
366
|
});
|
|
356
367
|
}
|
|
357
368
|
|
|
358
|
-
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
369
|
+
const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
|
|
359
370
|
return await provider.deleteOne<TData, TVariables>({
|
|
360
371
|
resource: resName,
|
|
361
372
|
id: targetId,
|
|
@@ -377,7 +388,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
377
388
|
|
|
378
389
|
const previousQueries = queryClient.getQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName });
|
|
379
390
|
|
|
380
|
-
const pk = getResource(resName).primaryKey ?? 'id';
|
|
391
|
+
const pk = adminContext.getResource(resName).primaryKey ?? 'id';
|
|
381
392
|
|
|
382
393
|
if (targetId != null) queryClient.removeQueries({ predicate: (q) => dp(q) && q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === targetId });
|
|
383
394
|
|
|
@@ -408,7 +419,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
408
419
|
|
|
409
420
|
fireSuccessNotification(params.successNotification, 'Deleted successfully', data.data, params.variables, resName);
|
|
410
421
|
audit({ action: 'delete', resource: resName, recordId: String(targetId) });
|
|
411
|
-
publishLiveEvent(resName, 'deleted', targetId != null ? [targetId] : undefined);
|
|
422
|
+
publishLiveEvent(resName, 'deleted', targetId != null ? [targetId] : undefined, adminContext);
|
|
412
423
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
413
424
|
(options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
|
|
414
425
|
}
|
|
@@ -430,7 +441,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
430
441
|
}
|
|
431
442
|
}
|
|
432
443
|
if (error instanceof UndoError) return;
|
|
433
|
-
checkError(error);
|
|
444
|
+
checkError(error, adminContext);
|
|
434
445
|
const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
|
|
435
446
|
fireErrorNotification(params.errorNotification, 'Delete failed', error, params.resource ?? defaultResource);
|
|
436
447
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
@@ -9,6 +9,7 @@ export interface CanParams {
|
|
|
9
9
|
resource: string;
|
|
10
10
|
action: Action;
|
|
11
11
|
params?: { id?: string | number; [key: string]: unknown };
|
|
12
|
+
meta?: Record<string, unknown>;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface CanResult {
|
|
@@ -81,8 +82,8 @@ export function getAccessControlOptions() {
|
|
|
81
82
|
* Supports both single capability requests or an array of batched checks.
|
|
82
83
|
*/
|
|
83
84
|
export async function canAccessAsync(params: CanParams[]): Promise<CanResult[]>;
|
|
84
|
-
export async function canAccessAsync(resource: string, action: Action, params?: Record<string, unknown>): Promise<CanResult>;
|
|
85
|
-
export async function canAccessAsync(resourceOrBatch: string | CanParams[], action?: Action, params?: Record<string, unknown>): Promise<CanResult | CanResult[]> {
|
|
85
|
+
export async function canAccessAsync(resource: string, action: Action, params?: Record<string, unknown>, meta?: Record<string, unknown>): Promise<CanResult>;
|
|
86
|
+
export async function canAccessAsync(resourceOrBatch: string | CanParams[], action?: Action, params?: Record<string, unknown>, meta?: Record<string, unknown>): Promise<CanResult | CanResult[]> {
|
|
86
87
|
if (!provider) {
|
|
87
88
|
return Array.isArray(resourceOrBatch)
|
|
88
89
|
? resourceOrBatch.map(() => ({ can: true }))
|
|
@@ -93,7 +94,7 @@ export async function canAccessAsync(resourceOrBatch: string | CanParams[], acti
|
|
|
93
94
|
return provider.can(resourceOrBatch);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
return provider.can({ resource: resourceOrBatch, action: action as string, params }) as Promise<CanResult>;
|
|
97
|
+
return provider.can({ resource: resourceOrBatch, action: action as string, params, meta }) as Promise<CanResult>;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
// ─── Feature Gate ─────────────────────────────────────────────
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createQuery } from '@tanstack/svelte-query';
|
|
2
2
|
import { getAdminOptions } from './options.svelte';
|
|
3
|
-
import {
|
|
3
|
+
import { captureAdminContext } from './context.svelte';
|
|
4
4
|
import { useParsed } from './useParsed.svelte';
|
|
5
5
|
import {
|
|
6
6
|
checkError,
|
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
} from './hook-utils.svelte';
|
|
12
12
|
import type { NotificationConfig, OvertimeOptions, LiveSubscriptionParams } from './hook-utils.svelte';
|
|
13
13
|
import type {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
GetListResult, Pagination, Sort, Filter, BaseRecord, HttpError,
|
|
15
|
+
GetOneResult, GetManyResult,
|
|
16
16
|
KnownResources,
|
|
17
17
|
} from './types';
|
|
18
18
|
import type { LiveMode, LiveEvent } from './live.svelte';
|
|
@@ -74,6 +74,7 @@ export interface UseListOptions<_TData extends BaseRecord = BaseRecord, _TError
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function useList<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseListOptions<TData, TError>> = {}) {
|
|
77
|
+
const adminContext = captureAdminContext();
|
|
77
78
|
const parsed = useParsed();
|
|
78
79
|
const adminOptions = getAdminOptions();
|
|
79
80
|
|
|
@@ -83,13 +84,30 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
83
84
|
const query = createQuery<GetListResult<TData>, TError>(() => {
|
|
84
85
|
const opts = getOptions();
|
|
85
86
|
const resource = getResource();
|
|
86
|
-
const provider = getDataProviderForResource(resource, opts.dataProviderName);
|
|
87
|
+
const provider = adminContext.getDataProviderForResource(resource, opts.dataProviderName);
|
|
87
88
|
const queryOptions = opts.queryOptions;
|
|
88
89
|
const pagination = cloneQueryKeyPart(opts.pagination);
|
|
89
90
|
const sorters = cloneQueryKeyPart(opts.sorters);
|
|
90
|
-
const
|
|
91
|
+
const explicitFilters = cloneQueryKeyPart(opts.filters);
|
|
91
92
|
const meta = cloneQueryKeyPart(opts.meta);
|
|
92
|
-
|
|
93
|
+
|
|
94
|
+
// 自动注入嵌套资源 parent filter
|
|
95
|
+
// 仅消费路径解析出的 parentParams(如 /teams/123/users 的 teamId),
|
|
96
|
+
// 不再从 query params 中误取,避免 ?tenantId=1 等被静默转成 filter。
|
|
97
|
+
//
|
|
98
|
+
// 安全边界:仅在 useList 未显式指定 resource(回退到 parsed.resource),
|
|
99
|
+
// 或显式 resource 与当前路由资源一致时注入。
|
|
100
|
+
const parentFilters: typeof explicitFilters = [];
|
|
101
|
+
const injectParent = !opts.resource || opts.resource === parsed.resource;
|
|
102
|
+
if (injectParent && parsed.parentParams) {
|
|
103
|
+
for (const [paramKey, paramVal] of Object.entries(parsed.parentParams)) {
|
|
104
|
+
if (paramKey.endsWith('Id') && paramVal) {
|
|
105
|
+
parentFilters.push({ field: paramKey, operator: 'eq' as const, value: paramVal });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const filters = [...parentFilters, ...(explicitFilters ?? [])];
|
|
110
|
+
|
|
93
111
|
return {
|
|
94
112
|
queryKey: [opts.dataProviderName, resource, 'list', pagination, sorters, filters, meta],
|
|
95
113
|
queryFn: async () => provider.getList<TData>({
|
|
@@ -112,7 +130,7 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
112
130
|
const opts = getOptions();
|
|
113
131
|
return {
|
|
114
132
|
resource: getResource(),
|
|
115
|
-
liveProvider:
|
|
133
|
+
liveProvider: adminContext.liveProvider,
|
|
116
134
|
liveMode: opts.liveMode ?? adminOptions.liveMode,
|
|
117
135
|
onLiveEvent: (e: LiveEvent) => {
|
|
118
136
|
opts.onLiveEvent?.(e);
|
|
@@ -136,7 +154,7 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
136
154
|
}
|
|
137
155
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
138
156
|
lastErrorAt = query.errorUpdatedAt;
|
|
139
|
-
checkError(query.error);
|
|
157
|
+
checkError(query.error, adminContext);
|
|
140
158
|
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, getResource());
|
|
141
159
|
}
|
|
142
160
|
});
|
|
@@ -161,6 +179,7 @@ export interface UseOneOptions<_TData extends BaseRecord = BaseRecord, _TError =
|
|
|
161
179
|
}
|
|
162
180
|
|
|
163
181
|
export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}) {
|
|
182
|
+
const adminContext = captureAdminContext();
|
|
164
183
|
const parsed = useParsed();
|
|
165
184
|
const adminOptions = getAdminOptions();
|
|
166
185
|
|
|
@@ -172,7 +191,7 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
172
191
|
const opts = getOptions();
|
|
173
192
|
const resource = getResource();
|
|
174
193
|
const id = getId();
|
|
175
|
-
const provider = getDataProviderForResource(resource, opts.dataProviderName);
|
|
194
|
+
const provider = adminContext.getDataProviderForResource(resource, opts.dataProviderName);
|
|
176
195
|
const queryOptions = opts.queryOptions;
|
|
177
196
|
|
|
178
197
|
return {
|
|
@@ -200,7 +219,7 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
200
219
|
const resource = getResource();
|
|
201
220
|
return {
|
|
202
221
|
resource,
|
|
203
|
-
liveProvider:
|
|
222
|
+
liveProvider: adminContext.liveProvider,
|
|
204
223
|
liveMode: opts.liveMode ?? adminOptions.liveMode,
|
|
205
224
|
onLiveEvent: (e: LiveEvent) => {
|
|
206
225
|
opts.onLiveEvent?.(e);
|
|
@@ -223,7 +242,7 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
223
242
|
}
|
|
224
243
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
225
244
|
lastErrorAt = query.errorUpdatedAt;
|
|
226
|
-
checkError(query.error);
|
|
245
|
+
checkError(query.error, adminContext);
|
|
227
246
|
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, getResource());
|
|
228
247
|
}
|
|
229
248
|
});
|
|
@@ -271,13 +290,14 @@ export interface UseManyOptions<_TData extends BaseRecord = BaseRecord, _TError
|
|
|
271
290
|
}
|
|
272
291
|
|
|
273
292
|
export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseManyOptions<TData, TError>>) {
|
|
293
|
+
const adminContext = captureAdminContext();
|
|
274
294
|
const adminOptions = getAdminOptions();
|
|
275
295
|
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
276
296
|
|
|
277
297
|
const query = createQuery<GetManyResult<TData>, TError>(() => {
|
|
278
298
|
const opts = getOptions();
|
|
279
299
|
const { resource, ids, meta, dataProviderName, queryOptions } = opts;
|
|
280
|
-
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
300
|
+
const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
|
|
281
301
|
|
|
282
302
|
return {
|
|
283
303
|
queryKey: [dataProviderName, resource, 'many', ids, meta],
|
|
@@ -302,7 +322,7 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
302
322
|
const opts = getOptions();
|
|
303
323
|
return {
|
|
304
324
|
resource: opts.resource,
|
|
305
|
-
liveProvider:
|
|
325
|
+
liveProvider: adminContext.liveProvider,
|
|
306
326
|
liveMode: opts.liveMode ?? adminOptions.liveMode,
|
|
307
327
|
onLiveEvent: (e: LiveEvent) => {
|
|
308
328
|
opts.onLiveEvent?.(e);
|
|
@@ -325,7 +345,7 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
325
345
|
}
|
|
326
346
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
327
347
|
lastErrorAt = query.errorUpdatedAt;
|
|
328
|
-
checkError(query.error);
|
|
348
|
+
checkError(query.error, adminContext);
|
|
329
349
|
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, opts.resource);
|
|
330
350
|
}
|
|
331
351
|
});
|
|
@@ -334,5 +354,5 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
334
354
|
}
|
|
335
355
|
|
|
336
356
|
export function useApiUrl(dataProviderName?: string): string {
|
|
337
|
-
return getDataProvider(dataProviderName).getApiUrl();
|
|
357
|
+
return captureAdminContext().getDataProvider(dataProviderName).getApiUrl();
|
|
338
358
|
}
|
package/src/router.ts
CHANGED
|
@@ -67,14 +67,22 @@ export function matchRoute(
|
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Navigate with an explicit tree-scoped provider while preserving the same
|
|
72
|
+
* guards, path synchronization, and after hooks as the legacy active router.
|
|
73
|
+
*/
|
|
74
|
+
export async function navigateWithProvider(
|
|
75
|
+
provider: RouterProvider | undefined,
|
|
76
|
+
path: string,
|
|
77
|
+
options?: { replaceState?: boolean },
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
const from = currentPathWithProvider(provider);
|
|
72
80
|
for (const guard of _beforeGuards) {
|
|
73
81
|
const allowed = await guard(path, from);
|
|
74
82
|
if (!allowed) return;
|
|
75
83
|
}
|
|
76
|
-
if (
|
|
77
|
-
|
|
84
|
+
if (provider) {
|
|
85
|
+
provider.go({ to: path, type: options?.replaceState ? 'replace' : 'push' });
|
|
78
86
|
} else if (typeof window !== 'undefined') {
|
|
79
87
|
window.location.hash = '#' + path.replace(/^#/, '');
|
|
80
88
|
}
|
|
@@ -84,19 +92,31 @@ export async function navigate(path: string, options?: { replaceState?: boolean
|
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
export async function navigate(path: string, options?: { replaceState?: boolean }): Promise<void> {
|
|
96
|
+
await navigateWithProvider(_routerProvider, path, options);
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
/**
|
|
88
100
|
* Format a path into an href suitable for <a> tags.
|
|
89
101
|
*/
|
|
90
102
|
export function formatLink(path: string): string {
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
return formatLinkWithProvider(_routerProvider, path);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function formatLinkWithProvider(provider: RouterProvider | undefined, path: string): string {
|
|
107
|
+
if (provider?.formatLink) {
|
|
108
|
+
return provider.formatLink(path);
|
|
93
109
|
}
|
|
94
110
|
return '#' + path.replace(/^#/, '');
|
|
95
111
|
}
|
|
96
112
|
|
|
97
113
|
export function currentPath(): string {
|
|
98
|
-
|
|
99
|
-
|
|
114
|
+
return currentPathWithProvider(_routerProvider);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function currentPathWithProvider(provider: RouterProvider | undefined): string {
|
|
118
|
+
if (provider) {
|
|
119
|
+
const parsed = provider.parse();
|
|
100
120
|
const pathname = parsed.pathname || '/';
|
|
101
121
|
let qs = '';
|
|
102
122
|
if (parsed.params && Object.keys(parsed.params).length > 0) {
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
import { navigate } from './router';
|
|
3
|
-
import { getResource, getResources, getRouterProvider } from './context.svelte';
|
|
1
|
+
import { captureAdminContext } from './context.svelte';
|
|
4
2
|
import { useParsed } from './useParsed.svelte';
|
|
5
3
|
|
|
6
4
|
// ─── routing-hooks.svelte.ts ───────────────────────────────────
|
|
7
5
|
|
|
8
6
|
export function useGetToPath() {
|
|
9
|
-
const
|
|
7
|
+
const adminContext = captureAdminContext();
|
|
10
8
|
const parsed = useParsed();
|
|
11
9
|
|
|
12
10
|
return (options: { resource?: string; action?: 'list' | 'create' | 'edit' | 'show' | 'clone'; id?: string | number; meta?: Record<string, unknown> }) => {
|
|
@@ -31,7 +29,7 @@ export function useGetToPath() {
|
|
|
31
29
|
else if (action === 'clone' && id) path += `/clone/${id}`;
|
|
32
30
|
|
|
33
31
|
// Apply router mapping if the router provider defines custom path transformers
|
|
34
|
-
if (routerProvider?.go) {
|
|
32
|
+
if (adminContext.routerProvider?.go) {
|
|
35
33
|
// SvelteKit path generation can be injected here.
|
|
36
34
|
}
|
|
37
35
|
return path;
|
|
@@ -39,6 +37,7 @@ export function useGetToPath() {
|
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
export function useGo() {
|
|
40
|
+
const adminContext = captureAdminContext();
|
|
42
41
|
const getToPath = useGetToPath();
|
|
43
42
|
|
|
44
43
|
return (options: { to?: string; query?: Record<string, unknown>; type?: 'push' | 'replace'; resource?: string; action?: 'list' | 'create' | 'edit' | 'show' | 'clone'; id?: string | number }) => {
|
|
@@ -55,19 +54,13 @@ export function useGo() {
|
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
navigate(targetUrl, options.type === 'replace' ? { replaceState: true } : undefined);
|
|
57
|
+
void adminContext.navigate(targetUrl, options.type === 'replace' ? { replaceState: true } : undefined);
|
|
59
58
|
};
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
export function useBack() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (routerProvider?.back) {
|
|
66
|
-
routerProvider.back();
|
|
67
|
-
} else if (typeof window !== 'undefined') {
|
|
68
|
-
window.history.back();
|
|
69
|
-
}
|
|
70
|
-
};
|
|
62
|
+
const adminContext = captureAdminContext();
|
|
63
|
+
return () => adminContext.back();
|
|
71
64
|
}
|
|
72
65
|
|
|
73
66
|
/**
|
|
@@ -75,13 +68,15 @@ export function useBack() {
|
|
|
75
68
|
* (to avoid full page reloads in SSR apps)
|
|
76
69
|
*/
|
|
77
70
|
export function useLink() {
|
|
71
|
+
const adminContext = captureAdminContext();
|
|
78
72
|
return () => {
|
|
79
|
-
const routerProvider =
|
|
73
|
+
const routerProvider = adminContext.routerProvider as unknown as Record<string, unknown> | undefined;
|
|
80
74
|
return (routerProvider?.Link as string) ?? 'a';
|
|
81
75
|
};
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
export function useResource(resourceName?: string) {
|
|
79
|
+
const adminContext = captureAdminContext();
|
|
85
80
|
const parsed = useParsed();
|
|
86
81
|
|
|
87
82
|
// Derived resource — re-evaluates when parsed.resource or resourceName changes
|
|
@@ -89,7 +84,7 @@ export function useResource(resourceName?: string) {
|
|
|
89
84
|
const target = resourceName ?? parsed.resource;
|
|
90
85
|
if (!target) return undefined;
|
|
91
86
|
try {
|
|
92
|
-
return getResource(target);
|
|
87
|
+
return adminContext.getResource(target);
|
|
93
88
|
} catch {
|
|
94
89
|
return undefined;
|
|
95
90
|
}
|
|
@@ -97,12 +92,21 @@ export function useResource(resourceName?: string) {
|
|
|
97
92
|
|
|
98
93
|
return {
|
|
99
94
|
get resource() { return resolvedResource; },
|
|
100
|
-
get resources() { return
|
|
95
|
+
get resources() { return adminContext.resources; },
|
|
101
96
|
get identifier() { return resolvedResource?.identifier ?? resolvedResource?.name; },
|
|
97
|
+
/** 当前 URL 解析出的 parent 资源信息(嵌套资源路由) */
|
|
98
|
+
get parent() {
|
|
99
|
+
const parentParams: Record<string, string> = {};
|
|
100
|
+
// 只取路径解析出的 parent params,排除 query params 的误判
|
|
101
|
+
for (const [k, v] of Object.entries(parsed.parentParams)) {
|
|
102
|
+
if (v) parentParams[k] = v;
|
|
103
|
+
}
|
|
104
|
+
return Object.keys(parentParams).length > 0 ? parentParams : undefined;
|
|
105
|
+
},
|
|
102
106
|
/** Resolve a resource by name — useful for dynamic lookups inside callbacks */
|
|
103
107
|
select: (name: string) => {
|
|
104
108
|
try {
|
|
105
|
-
const res = getResource(name);
|
|
109
|
+
const res = adminContext.getResource(name);
|
|
106
110
|
return { resource: res, identifier: res.identifier ?? res.name };
|
|
107
111
|
} catch {
|
|
108
112
|
return { resource: undefined, identifier: undefined };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useParsed } from './useParsed.svelte';
|
|
2
2
|
import { useList } from './query-hooks.svelte';
|
|
3
3
|
import { readURLState, writeURLState } from './url-sync';
|
|
4
|
+
import { captureAdminContext } from './context.svelte';
|
|
4
5
|
import { getAdminOptions } from './options.svelte';
|
|
5
6
|
import type { Pagination, Sort, Filter, BaseRecord, HttpError, KnownResources, GetListResult } from './types';
|
|
6
7
|
import type { UseListOptions, MaybeGetter } from './query-hooks.svelte';
|
|
@@ -29,6 +30,7 @@ export function useTable<
|
|
|
29
30
|
TError = HttpError,
|
|
30
31
|
TSearchVariables = Record<string, unknown>
|
|
31
32
|
>(optionsOrGetter: MaybeGetter<UseTableOptions<TQueryFnData, TError, TSearchVariables>> = {}) {
|
|
33
|
+
const adminContext = captureAdminContext();
|
|
32
34
|
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
33
35
|
const parsed = useParsed();
|
|
34
36
|
|
|
@@ -50,7 +52,7 @@ export function useTable<
|
|
|
50
52
|
let initFilters = initialOpts.filters?.initial ?? [];
|
|
51
53
|
|
|
52
54
|
if (syncWithLocation && typeof window !== 'undefined') {
|
|
53
|
-
const urlState = readURLState();
|
|
55
|
+
const urlState = readURLState(adminContext);
|
|
54
56
|
if (urlState.page || urlState.pageSize) {
|
|
55
57
|
initPagination = {
|
|
56
58
|
current: urlState.page ?? initPagination.current,
|
|
@@ -128,12 +130,12 @@ export function useTable<
|
|
|
128
130
|
sortField: currentSorters[0]?.field,
|
|
129
131
|
sortOrder: currentSorters[0]?.order,
|
|
130
132
|
filters: currentFilters,
|
|
131
|
-
});
|
|
133
|
+
}, adminContext);
|
|
132
134
|
});
|
|
133
135
|
|
|
134
136
|
$effect(() => {
|
|
135
137
|
const handler = () => {
|
|
136
|
-
const urlState = readURLState();
|
|
138
|
+
const urlState = readURLState(adminContext);
|
|
137
139
|
const newPage = urlState.page ?? 1;
|
|
138
140
|
if (newPage !== pagination.current) {
|
|
139
141
|
pagination = { ...pagination, current: newPage };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
/* eslint-disable svelte/prefer-svelte-reactivity */
|
|
2
1
|
// TableState — Svelte 5 Class State Pattern for table data management
|
|
3
2
|
// Reactive class that encapsulates all table state with fine-grained $state/$derived
|
|
4
3
|
|
|
5
4
|
import { useParsed } from './useParsed.svelte';
|
|
6
5
|
import { useList } from './query-hooks.svelte';
|
|
7
6
|
import { readURLState, writeURLState } from './url-sync';
|
|
7
|
+
import { captureAdminContext } from './context.svelte';
|
|
8
|
+
import type { AdminContextAccessor } from './context.svelte';
|
|
8
9
|
import type { Pagination, Sort, Filter, BaseRecord, HttpError, KnownResources, GetListResult } from './types';
|
|
9
10
|
import type { UseListOptions } from './query-hooks.svelte';
|
|
10
11
|
|
|
@@ -53,11 +54,13 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
53
54
|
private filterDefaultBehavior: FilterSetMode;
|
|
54
55
|
private syncWithLocation: boolean;
|
|
55
56
|
private paginationMode: string;
|
|
57
|
+
private adminContext: AdminContextAccessor;
|
|
56
58
|
|
|
57
59
|
// ─── Query result ─────────────────────────────────────────────
|
|
58
60
|
private _queryResult;
|
|
59
61
|
|
|
60
62
|
constructor(options: TableStateOptions<TData, TError> = {}) {
|
|
63
|
+
this.adminContext = captureAdminContext();
|
|
61
64
|
const parsed = useParsed();
|
|
62
65
|
const resource = options.resource ?? parsed.resource ?? '';
|
|
63
66
|
|
|
@@ -75,7 +78,7 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
75
78
|
let initFilters = options.filters?.initial ?? [];
|
|
76
79
|
|
|
77
80
|
if (this.syncWithLocation && typeof window !== 'undefined') {
|
|
78
|
-
const urlState = readURLState();
|
|
81
|
+
const urlState = readURLState(this.adminContext);
|
|
79
82
|
if (urlState.page || urlState.pageSize) {
|
|
80
83
|
initPagination = {
|
|
81
84
|
current: urlState.page ?? initPagination.current,
|
|
@@ -116,7 +119,7 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
116
119
|
sortField: this.currentSorters[0]?.field,
|
|
117
120
|
sortOrder: this.currentSorters[0]?.order,
|
|
118
121
|
filters: this.currentFilters,
|
|
119
|
-
});
|
|
122
|
+
}, this.adminContext);
|
|
120
123
|
});
|
|
121
124
|
}
|
|
122
125
|
}
|