@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.
@@ -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 { getDataProviderForResource, getResource, getLiveProvider } from './context.svelte';
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(resource: string, type: 'created' | 'updated' | 'deleted', ids?: (string | number)[]) {
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 = getLiveProvider();
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 = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dataProviderName;
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 { getDataProviderForResource, getDataProvider, getLiveProvider } from './context.svelte';
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
- GetListParams, GetListResult, Pagination, Sort, Filter, BaseRecord, HttpError,
15
- GetOneParams, GetOneResult, GetManyParams, GetManyResult, CustomParams, CustomResult,
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 filters = cloneQueryKeyPart(opts.filters);
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: getLiveProvider(),
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: getLiveProvider(),
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: getLiveProvider(),
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
- export async function navigate(path: string, options?: { replaceState?: boolean }): Promise<void> {
71
- const from = currentPath();
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 (_routerProvider) {
77
- _routerProvider.go({ to: path, type: options?.replaceState ? 'replace' : 'push' });
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
- if (_routerProvider?.formatLink) {
92
- return _routerProvider.formatLink(path);
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
- if (_routerProvider) {
99
- const parsed = _routerProvider.parse();
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
- /* eslint-disable svelte/prefer-svelte-reactivity */
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 routerProvider = getRouterProvider();
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
- return () => {
64
- const routerProvider = getRouterProvider();
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 = getRouterProvider() as Record<string, unknown> | undefined;
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 getResources(); },
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
  }