@svadmin/core 0.31.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,6 +1,6 @@
1
1
  // Minimal i18n — simple key-value translation
2
2
 
3
-
3
+ import { createContext } from 'svelte';
4
4
 
5
5
  const baseLocales: Record<string, Record<string, string>> = {
6
6
  'zh-CN': {
@@ -994,52 +994,189 @@ export interface I18nProvider {
994
994
  getAvailableLocales?: () => string[];
995
995
  }
996
996
 
997
+ /** Owner-controlled values for one component-tree i18n scope. */
998
+ export interface I18nScopeOptions {
999
+ locale?: string;
1000
+ provider?: I18nProvider;
1001
+ onLocaleChange?: (locale: string) => void;
1002
+ }
1003
+
1004
+ /**
1005
+ * Tree-local i18n state. `updateOwner` synchronizes props without emitting a
1006
+ * change event, while `setLocale` represents a change requested inside the tree.
1007
+ */
1008
+ export interface I18nScope {
1009
+ readonly locale: string;
1010
+ readonly provider: I18nProvider | undefined;
1011
+ translate: (key: string, params?: Record<string, string | number>) => string;
1012
+ setLocale: (locale: string) => void;
1013
+ setProvider: (provider: I18nProvider | undefined) => void;
1014
+ getAvailableLocales: () => string[];
1015
+ updateOwner: (options: I18nScopeOptions) => void;
1016
+ }
1017
+
1018
+ const [readI18nScopeContext, writeI18nScopeContext] = createContext<I18nScope>();
1019
+
1020
+ function translateFromTables(
1021
+ locale: string,
1022
+ key: string,
1023
+ params?: Record<string, string | number>,
1024
+ ): string {
1025
+ const localeTable = locales[locale];
1026
+ let text = localeTable?.[key] ?? locales.en?.[key] ?? key;
1027
+
1028
+ if (params) {
1029
+ for (const [param, value] of Object.entries(params)) {
1030
+ text = text.replaceAll(`{${param}}`, String(value));
1031
+ }
1032
+ }
1033
+
1034
+ return text;
1035
+ }
1036
+
1037
+ function readCurrentI18nScope(): I18nScope | undefined {
1038
+ try {
1039
+ return readI18nScopeContext();
1040
+ } catch {
1041
+ // Context is only readable while Svelte owns the current render/effect.
1042
+ // Public facade calls made outside a component intentionally use legacy state.
1043
+ return undefined;
1044
+ }
1045
+ }
1046
+
1047
+ /** Create reactive i18n state that can be owned by one AdminApp tree. */
1048
+ export function createI18nScope(options: I18nScopeOptions = {}): I18nScope {
1049
+ const initialProviderLocale = options.provider?.getLocale();
1050
+ const initialLocale = options.locale ?? initialProviderLocale ?? detectLocale();
1051
+ let scopeLocale = $state(initialLocale);
1052
+ let scopeProvider = $state.raw<I18nProvider | undefined>(options.provider);
1053
+ let onLocaleChange = $state.raw<((locale: string) => void) | undefined>(options.onLocaleChange);
1054
+ let ownerControlsLocale = options.locale !== undefined;
1055
+
1056
+ function synchronizeProviderLocale(locale: string): void {
1057
+ if (scopeProvider && scopeProvider.getLocale() !== locale) {
1058
+ scopeProvider.setLocale(locale);
1059
+ }
1060
+ }
1061
+
1062
+ if (options.locale !== undefined) {
1063
+ synchronizeProviderLocale(options.locale);
1064
+ }
1065
+
1066
+ const scope: I18nScope = {
1067
+ get locale() {
1068
+ return scopeLocale;
1069
+ },
1070
+ get provider() {
1071
+ return scopeProvider;
1072
+ },
1073
+ translate(key, params) {
1074
+ // Establish the locale dependency even when a provider owns translation.
1075
+ const locale = scopeLocale;
1076
+ return scopeProvider?.translate(key, params) ?? translateFromTables(locale, key, params);
1077
+ },
1078
+ setLocale(locale) {
1079
+ const providerLocale = scopeProvider?.getLocale();
1080
+ const changed = scopeLocale !== locale || (scopeProvider !== undefined && providerLocale !== locale);
1081
+
1082
+ scopeLocale = locale;
1083
+ synchronizeProviderLocale(locale);
1084
+ if (changed) onLocaleChange?.(locale);
1085
+ },
1086
+ setProvider(provider) {
1087
+ if (scopeProvider === provider) return;
1088
+ scopeProvider = provider;
1089
+ // Provider replacement is owner configuration, not a tree locale action.
1090
+ scopeLocale = provider?.getLocale() || detectLocale();
1091
+ },
1092
+ getAvailableLocales() {
1093
+ return scopeProvider?.getAvailableLocales?.() ?? Object.keys(locales);
1094
+ },
1095
+ updateOwner(nextOptions) {
1096
+ const providerChanged = scopeProvider !== nextOptions.provider;
1097
+ const clearedControlledLocale = ownerControlsLocale && nextOptions.locale === undefined;
1098
+ scopeProvider = nextOptions.provider;
1099
+ onLocaleChange = nextOptions.onLocaleChange;
1100
+ ownerControlsLocale = nextOptions.locale !== undefined;
1101
+
1102
+ if (nextOptions.locale !== undefined) {
1103
+ scopeLocale = nextOptions.locale;
1104
+ synchronizeProviderLocale(nextOptions.locale);
1105
+ } else if (providerChanged || clearedControlledLocale) {
1106
+ scopeLocale = scopeProvider?.getLocale() || detectLocale();
1107
+ }
1108
+ },
1109
+ };
1110
+
1111
+ return scope;
1112
+ }
1113
+
1114
+ /** Attach an i18n scope to the current component tree. */
1115
+ export function provideI18nScope(scope: I18nScope): I18nScope {
1116
+ writeI18nScopeContext(scope);
1117
+ return scope;
1118
+ }
1119
+
1120
+ /** Return the current tree scope when called during a Svelte render/effect. */
1121
+ export function getI18nScope(): I18nScope | undefined {
1122
+ return readCurrentI18nScope();
1123
+ }
1124
+
997
1125
  let i18nProvider: I18nProvider | undefined;
998
1126
 
1127
+ function setLegacyLocale(locale: string): void {
1128
+ i18nProvider?.setLocale(locale);
1129
+ currentLocale = locale;
1130
+ }
1131
+
1132
+ function getLegacyLocale(): string {
1133
+ // Reading the rune keeps legacy consumers reactive.
1134
+ const locale = currentLocale;
1135
+ return i18nProvider?.getLocale() ?? locale;
1136
+ }
1137
+
1138
+ function getLegacyAvailableLocales(): string[] {
1139
+ return i18nProvider?.getAvailableLocales?.() ?? Object.keys(locales);
1140
+ }
1141
+
1142
+ function translateLegacy(key: string, params?: Record<string, string | number>): string {
1143
+ const locale = currentLocale;
1144
+ return i18nProvider?.translate(key, params) ?? translateFromTables(locale, key, params);
1145
+ }
1146
+
999
1147
  export function setI18nProvider(provider: I18nProvider | undefined): void {
1148
+ const scope = readCurrentI18nScope();
1149
+ if (scope) {
1150
+ scope.setProvider(provider);
1151
+ return;
1152
+ }
1000
1153
  i18nProvider = provider;
1001
1154
  }
1002
1155
 
1003
1156
  export function getI18nProvider(): I18nProvider | undefined {
1004
- return i18nProvider;
1157
+ const scope = readCurrentI18nScope();
1158
+ return scope ? scope.provider : i18nProvider;
1005
1159
  }
1006
1160
 
1007
1161
  export function setLocale(locale: string): void {
1008
- if (i18nProvider) {
1009
- i18nProvider.setLocale(locale);
1162
+ const scope = readCurrentI18nScope();
1163
+ if (scope) {
1164
+ scope.setLocale(locale);
1165
+ return;
1010
1166
  }
1011
- // 同步更新 local state,确保 Svelte $state 依赖追踪触发模板刷新
1012
- currentLocale = locale;
1167
+ setLegacyLocale(locale);
1013
1168
  }
1014
1169
 
1015
1170
  export function getLocale(): string {
1016
- // 读取 currentLocale 以保持 Svelte $state 响应式追踪
1017
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1018
- currentLocale;
1019
- if (i18nProvider) return i18nProvider.getLocale();
1020
- return currentLocale;
1171
+ return readCurrentI18nScope()?.locale ?? getLegacyLocale();
1021
1172
  }
1022
1173
 
1023
1174
  export function getAvailableLocales(): string[] {
1024
- if (i18nProvider?.getAvailableLocales) return i18nProvider.getAvailableLocales();
1025
- return Object.keys(locales);
1175
+ return readCurrentI18nScope()?.getAvailableLocales() ?? getLegacyAvailableLocales();
1026
1176
  }
1027
1177
 
1028
1178
  export function t(key: string, params?: Record<string, string | number>): string {
1029
- // 读取 currentLocale 以建立 Svelte $state 依赖追踪,
1030
- // 确保无论是否使用 provider,语言切换都能触发模板刷新。
1031
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1032
- currentLocale;
1033
- if (i18nProvider) return i18nProvider.translate(key, params);
1034
- const locale = locales[currentLocale];
1035
- let text = locale?.[key] ?? locales['en']?.[key] ?? key;
1036
-
1037
- if (params) {
1038
- for (const [k, v] of Object.entries(params)) {
1039
- text = text.replaceAll(`{${k}}`, String(v));
1040
- }
1041
- }
1042
- return text;
1179
+ return readCurrentI18nScope()?.translate(key, params) ?? translateLegacy(key, params);
1043
1180
  }
1044
1181
 
1045
1182
  export function addTranslations(locale: string, translations: Record<string, string>): void {
@@ -1048,7 +1185,7 @@ export function addTranslations(locale: string, translations: Record<string, str
1048
1185
 
1049
1186
  export function resetI18n(): void {
1050
1187
  currentLocale = detectLocale();
1051
- locales = initialLocales;
1188
+ locales = structuredClone(initialLocales);
1052
1189
  i18nProvider = undefined;
1053
1190
  }
1054
1191
 
@@ -1057,14 +1194,23 @@ export function resetI18n(): void {
1057
1194
  * Tracks locale changes natively.
1058
1195
  */
1059
1196
  export function useTranslation() {
1060
- const translate = (key: string, params?: Record<string, string | number>): string => {
1061
- return t(key, params);
1062
- };
1063
-
1197
+ // Capture context while the component is initializing. Event handlers and
1198
+ // async continuations cannot safely call getContext later.
1199
+ const scope = readCurrentI18nScope();
1200
+ const translate = scope
1201
+ ? (key: string, params?: Record<string, string | number>) => scope.translate(key, params)
1202
+ : translateLegacy;
1203
+ const changeLocale = scope
1204
+ ? (locale: string) => scope.setLocale(locale)
1205
+ : setLegacyLocale;
1206
+ const availableLocales = scope
1207
+ ? () => scope.getAvailableLocales()
1208
+ : getLegacyAvailableLocales;
1209
+
1064
1210
  return {
1065
1211
  get t() { return translate; },
1066
- get locale() { return getLocale(); },
1067
- setLocale,
1068
- getAvailableLocales
1212
+ get locale() { return scope?.locale ?? getLegacyLocale(); },
1213
+ setLocale: changeLocale,
1214
+ getAvailableLocales: availableLocales,
1069
1215
  };
1070
1216
  }
package/src/index.ts CHANGED
@@ -7,9 +7,10 @@ export {
7
7
  setRouterProvider, getRouterProvider,
8
8
  setLiveProvider, getLiveProvider,
9
9
  setTaskProvider, getTaskProvider,
10
+ createAdminContext, provideAdminContext, getAdminContext, captureAdminContext,
10
11
  resetContext,
11
12
  } from './context.svelte';
12
- export type { DataProviderInput } from './context.svelte';
13
+ export type { DataProviderInput, AdminContextSource, AdminContextValue, AdminContextAccessor } from './context.svelte';
13
14
  export {
14
15
  useList, useInfiniteList,
15
16
  useOne, useShow,
@@ -36,14 +37,14 @@ export { setAccessControlProvider, getAccessControlProvider, getAccessControlOpt
36
37
  export { useLive, useSubscription, usePublish } from './live.svelte';
37
38
  export { toast, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue, resetToast } from './toast.svelte';
38
39
  export { notify, closeNotification, setNotificationProvider, getNotificationProvider } from './notification.svelte';
39
- export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation, setI18nProvider, getI18nProvider, resetI18n } from './i18n.svelte';
40
- export type { I18nProvider } from './i18n.svelte';
40
+ export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation, setI18nProvider, getI18nProvider, createI18nScope, provideI18nScope, getI18nScope, resetI18n } from './i18n.svelte';
41
+ export type { I18nProvider, I18nScope, I18nScopeOptions } from './i18n.svelte';
41
42
  export { audit, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
42
43
  export type { AuditLogProvider } from './audit';
43
44
  export { setChatProvider, getChatProvider, setChatContext, getChatContext, setAgentProvider, getAgentProvider, registerApproval, resolveApproval, hasPendingApprovals, resetChatProvider } from './chatProvider.svelte';
44
45
  export type { ChatProvider, ChatMessage, ChatContext, ChatAction, AgentProvider, AgentEvent, AgentOptions, AdminTool, AdminToolParameter, ToolResult } from './chatProvider.svelte';
45
- export { getTheme, setTheme, toggleTheme, getResolvedTheme, getColorTheme, setColorTheme, getColorThemes, configureTheme, getThemeConfig, clearCssOverrides, builtinPresets, registerColorPreset, getColorPresets, resetTheme } from './theme.svelte';
46
- export type { ThemeMode, ColorTheme, ThemeStrategy, ThemeConfig, ColorPreset } from './theme.svelte';
46
+ export { getTheme, setTheme, toggleTheme, getResolvedTheme, getColorTheme, setColorTheme, getColorThemes, configureTheme, getThemeConfig, clearCssOverrides, builtinPresets, registerColorPreset, getColorPresets, registerThemeOwner, updateThemeOwner, unregisterThemeOwner, resetTheme } from './theme.svelte';
47
+ export type { ThemeMode, ColorTheme, ThemeStrategy, ThemeConfig, ColorPreset, ThemeOwnerOptions, ThemeOwnerToken } from './theme.svelte';
47
48
  export { setUnsavedChanges, getUnsavedChanges, initUnsavedChangesNotifier, resetUnsavedChanges } from './unsaved-changes.svelte';
48
49
  export { setAdminOptions, getAdminOptions, getTextTransformers } from './options.svelte';
49
50
  export type { AdminOptions, TextTransformers, OvertimeConfig } from './options.svelte';
@@ -54,7 +55,7 @@ export * from './query-keys';
54
55
  export { HttpError, UndoError } from './types';
55
56
  export type {
56
57
  DataProvider, AuthProvider, NotificationProvider, MutationMode,
57
- ValidationErrors, CrudOperator, LogicalFilter, FieldFilter,
58
+ ValidationErrors, HttpErrorOptions, CrudOperator, LogicalFilter, FieldFilter,
58
59
  CustomParams, CustomResult,
59
60
  GetListParams, GetListResult,
60
61
  GetOneParams, GetOneResult,
@@ -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') {
@@ -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,7 +84,7 @@ 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);
@@ -129,7 +130,7 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
129
130
  const opts = getOptions();
130
131
  return {
131
132
  resource: getResource(),
132
- liveProvider: getLiveProvider(),
133
+ liveProvider: adminContext.liveProvider,
133
134
  liveMode: opts.liveMode ?? adminOptions.liveMode,
134
135
  onLiveEvent: (e: LiveEvent) => {
135
136
  opts.onLiveEvent?.(e);
@@ -153,7 +154,7 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
153
154
  }
154
155
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
155
156
  lastErrorAt = query.errorUpdatedAt;
156
- checkError(query.error);
157
+ checkError(query.error, adminContext);
157
158
  fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, getResource());
158
159
  }
159
160
  });
@@ -178,6 +179,7 @@ export interface UseOneOptions<_TData extends BaseRecord = BaseRecord, _TError =
178
179
  }
179
180
 
180
181
  export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}) {
182
+ const adminContext = captureAdminContext();
181
183
  const parsed = useParsed();
182
184
  const adminOptions = getAdminOptions();
183
185
 
@@ -189,7 +191,7 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
189
191
  const opts = getOptions();
190
192
  const resource = getResource();
191
193
  const id = getId();
192
- const provider = getDataProviderForResource(resource, opts.dataProviderName);
194
+ const provider = adminContext.getDataProviderForResource(resource, opts.dataProviderName);
193
195
  const queryOptions = opts.queryOptions;
194
196
 
195
197
  return {
@@ -217,7 +219,7 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
217
219
  const resource = getResource();
218
220
  return {
219
221
  resource,
220
- liveProvider: getLiveProvider(),
222
+ liveProvider: adminContext.liveProvider,
221
223
  liveMode: opts.liveMode ?? adminOptions.liveMode,
222
224
  onLiveEvent: (e: LiveEvent) => {
223
225
  opts.onLiveEvent?.(e);
@@ -240,7 +242,7 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
240
242
  }
241
243
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
242
244
  lastErrorAt = query.errorUpdatedAt;
243
- checkError(query.error);
245
+ checkError(query.error, adminContext);
244
246
  fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, getResource());
245
247
  }
246
248
  });
@@ -288,13 +290,14 @@ export interface UseManyOptions<_TData extends BaseRecord = BaseRecord, _TError
288
290
  }
289
291
 
290
292
  export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseManyOptions<TData, TError>>) {
293
+ const adminContext = captureAdminContext();
291
294
  const adminOptions = getAdminOptions();
292
295
  const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
293
296
 
294
297
  const query = createQuery<GetManyResult<TData>, TError>(() => {
295
298
  const opts = getOptions();
296
299
  const { resource, ids, meta, dataProviderName, queryOptions } = opts;
297
- const provider = getDataProviderForResource(resource, dataProviderName);
300
+ const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
298
301
 
299
302
  return {
300
303
  queryKey: [dataProviderName, resource, 'many', ids, meta],
@@ -319,7 +322,7 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
319
322
  const opts = getOptions();
320
323
  return {
321
324
  resource: opts.resource,
322
- liveProvider: getLiveProvider(),
325
+ liveProvider: adminContext.liveProvider,
323
326
  liveMode: opts.liveMode ?? adminOptions.liveMode,
324
327
  onLiveEvent: (e: LiveEvent) => {
325
328
  opts.onLiveEvent?.(e);
@@ -342,7 +345,7 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
342
345
  }
343
346
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
344
347
  lastErrorAt = query.errorUpdatedAt;
345
- checkError(query.error);
348
+ checkError(query.error, adminContext);
346
349
  fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, opts.resource);
347
350
  }
348
351
  });
@@ -351,5 +354,5 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
351
354
  }
352
355
 
353
356
  export function useApiUrl(dataProviderName?: string): string {
354
- return getDataProvider(dataProviderName).getApiUrl();
357
+ return captureAdminContext().getDataProvider(dataProviderName).getApiUrl();
355
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) {