@svadmin/core 0.31.0 → 0.32.1
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 +3 -3
- package/src/auth-hooks.svelte.ts +42 -25
- package/src/context.svelte.ts +185 -46
- package/src/data-transfer.svelte.ts +9 -62
- package/src/export-format.ts +35 -31
- 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 +181 -35
- package/src/index.ts +7 -6
- package/src/mutation-hooks.svelte.ts +27 -16
- package/src/query-hooks.svelte.ts +16 -13
- package/src/router.ts +28 -8
- package/src/routing-hooks.svelte.ts +13 -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 +26 -4
- package/src/unsaved-changes.svelte.ts +4 -38
- package/src/url-sync.ts +47 -8
- package/src/useParsed.svelte.ts +16 -11
- package/src/utility-hooks.svelte.ts +8 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.1",
|
|
4
4
|
"author": "zuohuadong",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"main": "src/index.ts",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@happy-dom/global-registrator": "^20.10.0",
|
|
13
|
-
"@sveltejs/vite-plugin-svelte": "^7.
|
|
13
|
+
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
|
14
14
|
"@testing-library/dom": "^10.4.1",
|
|
15
|
-
"@testing-library/svelte": "^5.
|
|
15
|
+
"@testing-library/svelte": "^5.4.2",
|
|
16
16
|
"@testing-library/user-event": "^14.6.1",
|
|
17
17
|
"@types/bun": "^1.3.14",
|
|
18
18
|
"@types/jsdom": "^28.0.3",
|
package/src/auth-hooks.svelte.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// Each hook encapsulates the auth call + loading state + error handling + redirect
|
|
3
3
|
// Uses module-level $state — no component init-time constraints.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { captureAdminContext } from './context.svelte';
|
|
6
|
+
import type { AdminContextAccessor } from './context.svelte';
|
|
7
7
|
import { notify } from './notification.svelte';
|
|
8
|
-
import { t } from './i18n.svelte';
|
|
8
|
+
import { t, useTranslation } from './i18n.svelte';
|
|
9
9
|
import type { AuthActionResult, CheckResult, Identity, AuthProvider } from './types';
|
|
10
10
|
import { useQueryClient } from '@tanstack/svelte-query';
|
|
11
11
|
|
|
@@ -19,14 +19,16 @@ interface CreateAuthMutationOptions {
|
|
|
19
19
|
method: keyof AuthProvider;
|
|
20
20
|
successMessage?: string | null;
|
|
21
21
|
errorMessage?: string | false;
|
|
22
|
-
onSuccess?: (result: AuthActionResult) => void
|
|
22
|
+
onSuccess?: (result: AuthActionResult, adminContext: AdminContextAccessor) => void | Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function createAuthMutation(options: CreateAuthMutationOptions) {
|
|
26
|
-
const
|
|
26
|
+
const adminContext = captureAdminContext();
|
|
27
|
+
const i18n = useTranslation();
|
|
27
28
|
let isLoading = $state(false);
|
|
28
29
|
|
|
29
30
|
async function mutate(params?: Record<string, unknown>): Promise<AuthActionResult> {
|
|
31
|
+
const provider = adminContext.authProvider;
|
|
30
32
|
if (!provider) throw new Error('AuthProvider not configured');
|
|
31
33
|
const fn = provider[options.method] as ((params?: Record<string, unknown>) => Promise<AuthActionResult>) | undefined;
|
|
32
34
|
if (!fn) throw new Error(`AuthProvider.${options.method} not implemented`);
|
|
@@ -36,14 +38,14 @@ function createAuthMutation(options: CreateAuthMutationOptions) {
|
|
|
36
38
|
const result = await fn.call(provider, params);
|
|
37
39
|
if (result.success) {
|
|
38
40
|
if (options.successMessage) notify({ type: 'success', message: options.successMessage });
|
|
39
|
-
if (options.onSuccess) options.onSuccess(result);
|
|
41
|
+
if (options.onSuccess) await options.onSuccess(result, adminContext);
|
|
40
42
|
} else {
|
|
41
|
-
const msg = result.error?.message ?? (typeof options.errorMessage === 'string' ? options.errorMessage : t('common.operationFailed'));
|
|
43
|
+
const msg = result.error?.message ?? (typeof options.errorMessage === 'string' ? options.errorMessage : i18n.t('common.operationFailed'));
|
|
42
44
|
if (options.errorMessage !== false) notify({ type: 'error', message: msg });
|
|
43
45
|
}
|
|
44
46
|
return result;
|
|
45
47
|
} catch (err) {
|
|
46
|
-
const msg = err instanceof Error ? err.message : (typeof options.errorMessage === 'string' ? options.errorMessage : t('common.operationFailed'));
|
|
48
|
+
const msg = err instanceof Error ? err.message : (typeof options.errorMessage === 'string' ? options.errorMessage : i18n.t('common.operationFailed'));
|
|
47
49
|
if (options.errorMessage !== false) notify({ type: 'error', message: msg });
|
|
48
50
|
return { success: false, error: { message: msg } };
|
|
49
51
|
} finally {
|
|
@@ -64,23 +66,26 @@ export function useLogin(opts?: { errorMessage?: string | false }) {
|
|
|
64
66
|
method: 'login',
|
|
65
67
|
successMessage: t('common.operationSuccess'),
|
|
66
68
|
errorMessage: opts?.errorMessage ?? t('common.loginFailed'),
|
|
67
|
-
onSuccess: (result) => { navigate(result.redirectTo ?? '/'); }
|
|
69
|
+
onSuccess: async (result, adminContext) => { await adminContext.navigate(result.redirectTo ?? '/'); }
|
|
68
70
|
});
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
// ─── useLogout ────────────────────────────────────────────────
|
|
72
74
|
|
|
73
75
|
export function useLogout() {
|
|
76
|
+
let queryClient: ReturnType<typeof useQueryClient> | undefined;
|
|
77
|
+
try {
|
|
78
|
+
queryClient = useQueryClient();
|
|
79
|
+
} catch {
|
|
80
|
+
// Auth hooks remain usable without TanStack Query; cache clearing is best-effort.
|
|
81
|
+
}
|
|
74
82
|
return createAuthMutation({
|
|
75
83
|
method: 'logout',
|
|
76
84
|
successMessage: null,
|
|
77
|
-
onSuccess: (result) => {
|
|
85
|
+
onSuccess: async (result, adminContext) => {
|
|
78
86
|
_logoutVersion++;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
queryClient.clear();
|
|
82
|
-
} catch { /* intentional */}
|
|
83
|
-
navigate(result.redirectTo ?? '/login');
|
|
87
|
+
queryClient?.clear();
|
|
88
|
+
await adminContext.navigate(result.redirectTo ?? '/login');
|
|
84
89
|
}
|
|
85
90
|
});
|
|
86
91
|
}
|
|
@@ -91,7 +96,9 @@ export function useRegister() {
|
|
|
91
96
|
return createAuthMutation({
|
|
92
97
|
method: 'register',
|
|
93
98
|
successMessage: t('auth.registerSuccess'),
|
|
94
|
-
onSuccess: (result) => {
|
|
99
|
+
onSuccess: async (result, adminContext) => {
|
|
100
|
+
if (result.redirectTo) await adminContext.navigate(result.redirectTo);
|
|
101
|
+
}
|
|
95
102
|
});
|
|
96
103
|
}
|
|
97
104
|
|
|
@@ -110,7 +117,9 @@ export function useUpdatePassword() {
|
|
|
110
117
|
return createAuthMutation({
|
|
111
118
|
method: 'updatePassword',
|
|
112
119
|
successMessage: t('common.operationSuccess'),
|
|
113
|
-
onSuccess: (result) => {
|
|
120
|
+
onSuccess: async (result, adminContext) => {
|
|
121
|
+
if (result.redirectTo) await adminContext.navigate(result.redirectTo);
|
|
122
|
+
}
|
|
114
123
|
});
|
|
115
124
|
}
|
|
116
125
|
|
|
@@ -120,7 +129,9 @@ export function useUpdateIdentity() {
|
|
|
120
129
|
return createAuthMutation({
|
|
121
130
|
method: 'updateIdentity',
|
|
122
131
|
successMessage: t('common.operationSuccess'),
|
|
123
|
-
onSuccess: (result) => {
|
|
132
|
+
onSuccess: async (result, adminContext) => {
|
|
133
|
+
if (result.redirectTo) await adminContext.navigate(result.redirectTo);
|
|
134
|
+
}
|
|
124
135
|
});
|
|
125
136
|
}
|
|
126
137
|
|
|
@@ -128,19 +139,22 @@ export function useUpdateProfile() {
|
|
|
128
139
|
return createAuthMutation({
|
|
129
140
|
method: 'updateProfile',
|
|
130
141
|
successMessage: t('common.operationSuccess'),
|
|
131
|
-
onSuccess: (result) => {
|
|
142
|
+
onSuccess: async (result, adminContext) => {
|
|
143
|
+
if (result.redirectTo) await adminContext.navigate(result.redirectTo);
|
|
144
|
+
}
|
|
132
145
|
});
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
// ─── useGetIdentity ──────────────────────────────────────────
|
|
136
149
|
|
|
137
150
|
export function useGetIdentity() {
|
|
138
|
-
const
|
|
151
|
+
const adminContext = captureAdminContext();
|
|
139
152
|
let data = $state<Identity | null>(null);
|
|
140
153
|
let isLoading = $state(true);
|
|
141
154
|
let error = $state<Error | null>(null);
|
|
142
155
|
|
|
143
156
|
function fetch() {
|
|
157
|
+
const provider = adminContext.authProvider;
|
|
144
158
|
if (!provider) { isLoading = false; return; }
|
|
145
159
|
isLoading = true;
|
|
146
160
|
provider.getIdentity().then(identity => {
|
|
@@ -172,12 +186,13 @@ export function useGetIdentity() {
|
|
|
172
186
|
// ─── useIsAuthenticated ──────────────────────────────────────
|
|
173
187
|
|
|
174
188
|
export function useIsAuthenticated() {
|
|
175
|
-
const
|
|
189
|
+
const adminContext = captureAdminContext();
|
|
176
190
|
let isAuthenticated = $state(false);
|
|
177
191
|
let isLoading = $state(true);
|
|
178
192
|
let checkResult = $state<CheckResult | null>(null);
|
|
179
193
|
|
|
180
194
|
function check() {
|
|
195
|
+
const provider = adminContext.authProvider;
|
|
181
196
|
if (!provider) {
|
|
182
197
|
isAuthenticated = true;
|
|
183
198
|
isLoading = false;
|
|
@@ -227,9 +242,10 @@ export function useIsAuthenticated() {
|
|
|
227
242
|
* If it returns { redirectTo }, navigates there.
|
|
228
243
|
*/
|
|
229
244
|
export function useOnError() {
|
|
230
|
-
const
|
|
245
|
+
const adminContext = captureAdminContext();
|
|
231
246
|
|
|
232
247
|
async function mutate(error: unknown) {
|
|
248
|
+
const provider = adminContext.authProvider;
|
|
233
249
|
if (!provider?.onError) {
|
|
234
250
|
console.warn('[svadmin] useOnError: authProvider.onError not implemented');
|
|
235
251
|
return;
|
|
@@ -238,9 +254,9 @@ export function useOnError() {
|
|
|
238
254
|
const result = await provider.onError(error);
|
|
239
255
|
if (result.logout) {
|
|
240
256
|
await provider.logout?.();
|
|
241
|
-
navigate(result.redirectTo ?? '/login');
|
|
257
|
+
await adminContext.navigate(result.redirectTo ?? '/login');
|
|
242
258
|
} else if (result.redirectTo) {
|
|
243
|
-
navigate(result.redirectTo);
|
|
259
|
+
await adminContext.navigate(result.redirectTo);
|
|
244
260
|
}
|
|
245
261
|
} catch (err) {
|
|
246
262
|
console.warn('[svadmin] useOnError failed:', err);
|
|
@@ -268,13 +284,14 @@ export function useOnError() {
|
|
|
268
284
|
* ```
|
|
269
285
|
*/
|
|
270
286
|
export function usePermissions<T = unknown>() {
|
|
271
|
-
const
|
|
287
|
+
const adminContext = captureAdminContext();
|
|
272
288
|
let permissions = $state<T | null>(null);
|
|
273
289
|
let isLoading = $state(true);
|
|
274
290
|
let error = $state<Error | null>(null);
|
|
275
291
|
let version = $state(0);
|
|
276
292
|
|
|
277
293
|
function fetch() {
|
|
294
|
+
const provider = adminContext.authProvider;
|
|
278
295
|
if (!provider?.getPermissions) { isLoading = false; return; }
|
|
279
296
|
isLoading = true;
|
|
280
297
|
error = null;
|
package/src/context.svelte.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
// Svelte 5 Context —
|
|
2
|
-
//
|
|
1
|
+
// Svelte 5 Context — application trees use scoped context while the module-level
|
|
2
|
+
// setters remain as a compatibility facade for non-component consumers.
|
|
3
3
|
|
|
4
|
+
import { createContext } from 'svelte';
|
|
4
5
|
import type { DataProvider, AuthProvider, ResourceDefinition, TaskProvider } from './types';
|
|
5
6
|
import type { RouterProvider } from './router-provider';
|
|
7
|
+
import type { LiveProvider } from './live.svelte';
|
|
6
8
|
import { resetNotificationProvider } from './notification.svelte';
|
|
7
9
|
import { resetAuditLogProvider } from './audit';
|
|
8
10
|
import { resetAccessControlProvider } from './permissions.svelte';
|
|
@@ -11,7 +13,15 @@ import { resetI18n } from './i18n.svelte';
|
|
|
11
13
|
import { resetChatProvider } from './chatProvider.svelte';
|
|
12
14
|
import { resetToast } from './toast.svelte';
|
|
13
15
|
import { resetTheme } from './theme.svelte';
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
currentPath as currentLegacyPath,
|
|
18
|
+
currentPathWithProvider,
|
|
19
|
+
formatLink as formatLegacyLink,
|
|
20
|
+
formatLinkWithProvider,
|
|
21
|
+
navigate as navigateLegacy,
|
|
22
|
+
navigateWithProvider,
|
|
23
|
+
resetRouter,
|
|
24
|
+
} from './router';
|
|
15
25
|
import { resetUnsavedChanges } from './unsaved-changes.svelte';
|
|
16
26
|
import { resetGlobalPath } from './useParsed.svelte';
|
|
17
27
|
import { resetLogoutVersion } from './auth-hooks.svelte';
|
|
@@ -26,46 +36,176 @@ import { resetSidebarCollapsed } from './hooks.svelte';
|
|
|
26
36
|
*/
|
|
27
37
|
export type DataProviderInput = DataProvider | Record<string, DataProvider>;
|
|
28
38
|
|
|
39
|
+
export interface AdminContextSource {
|
|
40
|
+
readonly dataProvider: DataProviderInput;
|
|
41
|
+
readonly authProvider?: AuthProvider | null;
|
|
42
|
+
readonly resources: ResourceDefinition[];
|
|
43
|
+
readonly routerProvider?: RouterProvider;
|
|
44
|
+
readonly liveProvider?: LiveProvider;
|
|
45
|
+
readonly taskProvider?: TaskProvider;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AdminContextValue {
|
|
49
|
+
readonly providers: Record<string, DataProvider>;
|
|
50
|
+
readonly authProvider: AuthProvider | null;
|
|
51
|
+
readonly resources: ResourceDefinition[];
|
|
52
|
+
readonly routerProvider: RouterProvider | undefined;
|
|
53
|
+
readonly liveProvider: LiveProvider | undefined;
|
|
54
|
+
readonly taskProvider: TaskProvider | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Initialization-time snapshot of the owning AdminContext tree.
|
|
59
|
+
*
|
|
60
|
+
* The accessor keeps the scoped context object (whose fields are reactive
|
|
61
|
+
* getters) instead of calling Svelte's getContext from later DOM/query
|
|
62
|
+
* callbacks. When no scoped context exists, getters intentionally continue to
|
|
63
|
+
* read the module-level compatibility setters dynamically.
|
|
64
|
+
*/
|
|
65
|
+
export interface AdminContextAccessor {
|
|
66
|
+
readonly providers: Record<string, DataProvider> | null;
|
|
67
|
+
readonly authProvider: AuthProvider | null;
|
|
68
|
+
readonly resources: ResourceDefinition[];
|
|
69
|
+
readonly routerProvider: RouterProvider | undefined;
|
|
70
|
+
readonly liveProvider: LiveProvider | undefined;
|
|
71
|
+
readonly taskProvider: TaskProvider | undefined;
|
|
72
|
+
getDataProvider(name?: string): DataProvider;
|
|
73
|
+
getDataProviderNames(): string[];
|
|
74
|
+
getDataProviderForResource(resourceName: string, overrideName?: string): DataProvider;
|
|
75
|
+
getResource(nameOrIdentifier: string): ResourceDefinition;
|
|
76
|
+
currentPath(): string;
|
|
77
|
+
formatLink(path: string): string;
|
|
78
|
+
navigate(path: string, options?: { replaceState?: boolean }): Promise<void>;
|
|
79
|
+
back(): void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const [getRequiredAdminContext, setAdminContext] = createContext<AdminContextValue>();
|
|
83
|
+
|
|
84
|
+
function normalizeDataProviders(provider: DataProviderInput): Record<string, DataProvider> {
|
|
85
|
+
return isDataProvider(provider)
|
|
86
|
+
? { default: provider }
|
|
87
|
+
: provider;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createAdminContext(source: AdminContextSource): AdminContextValue {
|
|
91
|
+
return {
|
|
92
|
+
get providers() { return normalizeDataProviders(source.dataProvider); },
|
|
93
|
+
get authProvider() { return source.authProvider ?? null; },
|
|
94
|
+
get resources() { return source.resources; },
|
|
95
|
+
get routerProvider() { return source.routerProvider; },
|
|
96
|
+
get liveProvider() { return source.liveProvider; },
|
|
97
|
+
get taskProvider() { return source.taskProvider; },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Provide request/tree-scoped admin state to all descendants. */
|
|
102
|
+
export function provideAdminContext(source: AdminContextSource): AdminContextValue {
|
|
103
|
+
const context = createAdminContext(source);
|
|
104
|
+
setAdminContext(context);
|
|
105
|
+
return context;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Return the current tree context when called during component initialization/render. */
|
|
109
|
+
export function getAdminContext(): AdminContextValue | undefined {
|
|
110
|
+
try {
|
|
111
|
+
return getRequiredAdminContext();
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
29
117
|
let providers = $state<Record<string, DataProvider> | null>(null);
|
|
30
118
|
|
|
119
|
+
/** Capture the current tree once so delayed callbacks never re-enter getContext. */
|
|
120
|
+
export function captureAdminContext(): AdminContextAccessor {
|
|
121
|
+
const scopedContext = getAdminContext();
|
|
122
|
+
|
|
123
|
+
const accessor: AdminContextAccessor = {
|
|
124
|
+
get providers() { return scopedContext ? scopedContext.providers : providers; },
|
|
125
|
+
get authProvider() { return scopedContext ? scopedContext.authProvider : authProvider; },
|
|
126
|
+
get resources() { return scopedContext ? scopedContext.resources : resources ?? []; },
|
|
127
|
+
get routerProvider() { return scopedContext ? scopedContext.routerProvider : routerProvider; },
|
|
128
|
+
get liveProvider() { return scopedContext ? scopedContext.liveProvider : liveProviderState; },
|
|
129
|
+
get taskProvider() { return scopedContext ? scopedContext.taskProvider : taskProviderState; },
|
|
130
|
+
getDataProvider(name) {
|
|
131
|
+
const activeProviders = accessor.providers;
|
|
132
|
+
if (!activeProviders) throw new Error('DataProvider not found. Did you call setDataProvider in App.svelte?');
|
|
133
|
+
const key = name ?? 'default';
|
|
134
|
+
const provider = activeProviders[key];
|
|
135
|
+
if (!provider) {
|
|
136
|
+
throw new Error(`DataProvider "${key}" not found. Available: ${Object.keys(activeProviders).join(', ')}`);
|
|
137
|
+
}
|
|
138
|
+
return provider;
|
|
139
|
+
},
|
|
140
|
+
getDataProviderNames() {
|
|
141
|
+
return accessor.providers ? Object.keys(accessor.providers) : [];
|
|
142
|
+
},
|
|
143
|
+
getDataProviderForResource(resourceName, overrideName) {
|
|
144
|
+
if (overrideName) {
|
|
145
|
+
try {
|
|
146
|
+
return accessor.getDataProvider(overrideName);
|
|
147
|
+
} catch {
|
|
148
|
+
// Preserve the historical fallback to the resource/default provider.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const resource = accessor.getResource(resourceName);
|
|
153
|
+
const dataProviderName = resource.meta?.dataProviderName as string | undefined;
|
|
154
|
+
if (dataProviderName) return accessor.getDataProvider(dataProviderName);
|
|
155
|
+
} catch {
|
|
156
|
+
// Preserve the historical fallback when a resource is not registered.
|
|
157
|
+
}
|
|
158
|
+
return accessor.getDataProvider();
|
|
159
|
+
},
|
|
160
|
+
getResource(nameOrIdentifier) {
|
|
161
|
+
const resource = accessor.resources.find(
|
|
162
|
+
(candidate) => candidate.identifier === nameOrIdentifier || candidate.name === nameOrIdentifier,
|
|
163
|
+
);
|
|
164
|
+
if (!resource) throw new Error(`Resource "${nameOrIdentifier}" not found in resource definitions.`);
|
|
165
|
+
return resource;
|
|
166
|
+
},
|
|
167
|
+
currentPath() {
|
|
168
|
+
if (scopedContext) return currentPathWithProvider(scopedContext.routerProvider);
|
|
169
|
+
return currentLegacyPath();
|
|
170
|
+
},
|
|
171
|
+
formatLink(path) {
|
|
172
|
+
if (scopedContext) return formatLinkWithProvider(scopedContext.routerProvider, path);
|
|
173
|
+
return formatLegacyLink(path);
|
|
174
|
+
},
|
|
175
|
+
async navigate(path, options) {
|
|
176
|
+
if (scopedContext) {
|
|
177
|
+
await navigateWithProvider(scopedContext.routerProvider, path, options);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await navigateLegacy(path, options);
|
|
181
|
+
},
|
|
182
|
+
back() {
|
|
183
|
+
const activeRouter = accessor.routerProvider;
|
|
184
|
+
if (activeRouter?.back) {
|
|
185
|
+
activeRouter.back();
|
|
186
|
+
} else if (typeof window !== 'undefined') {
|
|
187
|
+
window.history.back();
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return accessor;
|
|
193
|
+
}
|
|
194
|
+
|
|
31
195
|
export function setDataProvider(provider: DataProviderInput): void {
|
|
32
|
-
|
|
33
|
-
providers = { default: provider } as Record<string, DataProvider>;
|
|
34
|
-
} else {
|
|
35
|
-
providers = provider as Record<string, DataProvider>;
|
|
36
|
-
}
|
|
196
|
+
providers = normalizeDataProviders(provider);
|
|
37
197
|
}
|
|
38
198
|
|
|
39
199
|
export function getDataProvider(name?: string): DataProvider {
|
|
40
|
-
|
|
41
|
-
const key = name ?? 'default';
|
|
42
|
-
const provider = providers[key];
|
|
43
|
-
if (!provider) {
|
|
44
|
-
throw new Error(`DataProvider "${key}" not found. Available: ${Object.keys(providers).join(', ')}`);
|
|
45
|
-
}
|
|
46
|
-
return provider;
|
|
200
|
+
return captureAdminContext().getDataProvider(name);
|
|
47
201
|
}
|
|
48
202
|
|
|
49
203
|
export function getDataProviderNames(): string[] {
|
|
50
|
-
return
|
|
204
|
+
return captureAdminContext().getDataProviderNames();
|
|
51
205
|
}
|
|
52
206
|
|
|
53
207
|
export function getDataProviderForResource(resourceName: string, overrideName?: string): DataProvider {
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
return getDataProvider(overrideName);
|
|
57
|
-
} catch {
|
|
58
|
-
// ignore
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
const resource = getResource(resourceName);
|
|
63
|
-
const dpName = resource.meta?.dataProviderName as string | undefined;
|
|
64
|
-
if (dpName) return getDataProvider(dpName);
|
|
65
|
-
} catch {
|
|
66
|
-
// ignore
|
|
67
|
-
}
|
|
68
|
-
return getDataProvider();
|
|
208
|
+
return captureAdminContext().getDataProviderForResource(resourceName, overrideName);
|
|
69
209
|
}
|
|
70
210
|
|
|
71
211
|
function isDataProvider(value: unknown): value is DataProvider {
|
|
@@ -88,8 +228,9 @@ export function setAuthProvider(provider: AuthProvider | null | undefined): void
|
|
|
88
228
|
export function getAuthProvider(): AuthProvider;
|
|
89
229
|
export function getAuthProvider(options: { optional: true }): AuthProvider | null;
|
|
90
230
|
export function getAuthProvider(options?: { optional?: boolean }): AuthProvider | null {
|
|
91
|
-
|
|
92
|
-
|
|
231
|
+
const activeAuthProvider = captureAdminContext().authProvider;
|
|
232
|
+
if (!activeAuthProvider && !options?.optional) throw new Error('AuthProvider not found. Did you call setAuthProvider in App.svelte?');
|
|
233
|
+
return activeAuthProvider;
|
|
93
234
|
}
|
|
94
235
|
|
|
95
236
|
// ─── Resources ──────────────────────────────────────────────────
|
|
@@ -101,39 +242,36 @@ export function setResources(newResources: ResourceDefinition[]): void {
|
|
|
101
242
|
}
|
|
102
243
|
|
|
103
244
|
export function getResources(): ResourceDefinition[] {
|
|
104
|
-
return resources
|
|
245
|
+
return captureAdminContext().resources;
|
|
105
246
|
}
|
|
106
247
|
|
|
107
248
|
export function getResource(nameOrIdentifier: string): ResourceDefinition {
|
|
108
|
-
|
|
109
|
-
const resource = rList.find(r => r.identifier === nameOrIdentifier || r.name === nameOrIdentifier);
|
|
110
|
-
if (!resource) throw new Error(`Resource "${nameOrIdentifier}" not found in resource definitions.`);
|
|
111
|
-
return resource;
|
|
249
|
+
return captureAdminContext().getResource(nameOrIdentifier);
|
|
112
250
|
}
|
|
113
251
|
|
|
114
252
|
// ─── Router Provider ────────────────────────────────────────────
|
|
115
253
|
|
|
116
254
|
let routerProvider = $state<RouterProvider | undefined>(undefined);
|
|
117
255
|
|
|
118
|
-
export function setRouterProvider(provider: RouterProvider): void {
|
|
256
|
+
export function setRouterProvider(provider: RouterProvider | undefined): void {
|
|
119
257
|
routerProvider = provider;
|
|
120
258
|
}
|
|
121
259
|
|
|
122
260
|
export function getRouterProvider(): RouterProvider | undefined {
|
|
123
|
-
return routerProvider;
|
|
261
|
+
return captureAdminContext().routerProvider;
|
|
124
262
|
}
|
|
125
263
|
|
|
126
264
|
|
|
127
265
|
// ─── Live Provider ──────────────────────────────────────────────
|
|
128
266
|
|
|
129
|
-
let liveProviderState = $state<
|
|
267
|
+
let liveProviderState = $state<LiveProvider | undefined>(undefined);
|
|
130
268
|
|
|
131
|
-
export function setLiveProvider(provider:
|
|
269
|
+
export function setLiveProvider(provider: LiveProvider): void {
|
|
132
270
|
liveProviderState = provider;
|
|
133
271
|
}
|
|
134
272
|
|
|
135
|
-
export function getLiveProvider():
|
|
136
|
-
return
|
|
273
|
+
export function getLiveProvider(): LiveProvider | undefined {
|
|
274
|
+
return captureAdminContext().liveProvider;
|
|
137
275
|
}
|
|
138
276
|
|
|
139
277
|
// ─── Task Provider ──────────────────────────────────────────────
|
|
@@ -147,10 +285,11 @@ export function setTaskProvider(provider: TaskProvider | undefined): void {
|
|
|
147
285
|
export function getTaskProvider(): TaskProvider;
|
|
148
286
|
export function getTaskProvider(options: { optional: true }): TaskProvider | undefined;
|
|
149
287
|
export function getTaskProvider(options?: { optional?: boolean }): TaskProvider | undefined {
|
|
150
|
-
|
|
288
|
+
const activeTaskProvider = captureAdminContext().taskProvider;
|
|
289
|
+
if (!activeTaskProvider && !options?.optional) {
|
|
151
290
|
throw new Error('TaskProvider not found. Did you call setTaskProvider in App.svelte?');
|
|
152
291
|
}
|
|
153
|
-
return
|
|
292
|
+
return activeTaskProvider;
|
|
154
293
|
}
|
|
155
294
|
|
|
156
295
|
// ─── Reset — for testing / HMR ─────────────────────────────────
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { captureAdminContext } from './context.svelte';
|
|
2
2
|
import { useParsed } from './useParsed.svelte';
|
|
3
3
|
import type { Sort, Filter, BaseRecord } from './types';
|
|
4
4
|
import { downloadData } from './export-format';
|
|
5
5
|
import type { ExportFormat } from './export-format';
|
|
6
|
+
import { parseCSV } from './helpers-pure';
|
|
6
7
|
export { downloadData, toCsv, toJson, toXlsx, escapeCsvField } from './export-format';
|
|
7
8
|
export type { ExportFormat } from './export-format';
|
|
8
9
|
|
|
@@ -26,6 +27,7 @@ export interface UseExportOptions<TData extends BaseRecord = BaseRecord> {
|
|
|
26
27
|
* Supports mapData, sorters, filters, maxItemCount, pageSize
|
|
27
28
|
*/
|
|
28
29
|
export function useExport<TData extends BaseRecord = BaseRecord>(options: UseExportOptions<TData> = {}) {
|
|
30
|
+
const adminContext = captureAdminContext();
|
|
29
31
|
const parsed = useParsed();
|
|
30
32
|
const resource = $derived(options.resource ?? parsed.resource ?? '');
|
|
31
33
|
let isLoading = $state(false);
|
|
@@ -35,7 +37,7 @@ export function useExport<TData extends BaseRecord = BaseRecord>(options: UseExp
|
|
|
35
37
|
isLoading = true;
|
|
36
38
|
|
|
37
39
|
try {
|
|
38
|
-
const provider = getDataProviderForResource(exportResource, options.dataProviderName);
|
|
40
|
+
const provider = adminContext.getDataProviderForResource(exportResource, options.dataProviderName);
|
|
39
41
|
const batchSize = options.pageSize ?? 20;
|
|
40
42
|
const maxItems = options.maxItemCount ?? Infinity;
|
|
41
43
|
let allRecords: TData[] = [];
|
|
@@ -68,7 +70,9 @@ export function useExport<TData extends BaseRecord = BaseRecord>(options: UseExp
|
|
|
68
70
|
|
|
69
71
|
return allRecords;
|
|
70
72
|
} catch (error) {
|
|
71
|
-
|
|
73
|
+
const exportError = error instanceof Error ? error : new Error(String(error));
|
|
74
|
+
if (!options.onError) throw exportError;
|
|
75
|
+
options.onError(exportError);
|
|
72
76
|
return [];
|
|
73
77
|
} finally {
|
|
74
78
|
isLoading = false;
|
|
@@ -98,6 +102,7 @@ export interface UseImportOptions<TData = Record<string, unknown>> {
|
|
|
98
102
|
* Supports mapData, batchSize, onFinish, onProgress, meta
|
|
99
103
|
*/
|
|
100
104
|
export function useImport<TData = Record<string, unknown>>(options: UseImportOptions<TData> = {}) {
|
|
105
|
+
const adminContext = captureAdminContext();
|
|
101
106
|
const parsed = useParsed();
|
|
102
107
|
const resource = $derived(options.resource ?? parsed.resource ?? '');
|
|
103
108
|
let isLoading = $state(false);
|
|
@@ -110,7 +115,7 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
|
|
|
110
115
|
const errored: { request: unknown; error: unknown }[] = [];
|
|
111
116
|
|
|
112
117
|
try {
|
|
113
|
-
const provider = getDataProviderForResource(importResource, options.dataProviderName);
|
|
118
|
+
const provider = adminContext.getDataProviderForResource(importResource, options.dataProviderName);
|
|
114
119
|
let text = await info.file.text();
|
|
115
120
|
// Strip BOM
|
|
116
121
|
if (text.charCodeAt(0) === 0xFEFF) {
|
|
@@ -209,61 +214,3 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
|
|
|
209
214
|
get mutationResult() { return mutationResult; },
|
|
210
215
|
};
|
|
211
216
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// ─── 解析工具 ────────────────────────────────────────────────────
|
|
215
|
-
|
|
216
|
-
/** Parse full CSV text respecting quoted multi-line fields */
|
|
217
|
-
function parseCSV(text: string): string[][] {
|
|
218
|
-
const rows: string[][] = [];
|
|
219
|
-
let currentRow: string[] = [];
|
|
220
|
-
let currentVal = '';
|
|
221
|
-
let inQuotes = false;
|
|
222
|
-
let wasQuoted = false;
|
|
223
|
-
|
|
224
|
-
for (let i = 0; i < text.length; i++) {
|
|
225
|
-
const ch = text[i];
|
|
226
|
-
if (inQuotes) {
|
|
227
|
-
if (ch === '"') {
|
|
228
|
-
if (i + 1 < text.length && text[i + 1] === '"') {
|
|
229
|
-
currentVal += '"';
|
|
230
|
-
i++;
|
|
231
|
-
} else {
|
|
232
|
-
inQuotes = false;
|
|
233
|
-
}
|
|
234
|
-
} else {
|
|
235
|
-
currentVal += ch;
|
|
236
|
-
}
|
|
237
|
-
} else {
|
|
238
|
-
if (ch === '"') {
|
|
239
|
-
inQuotes = true;
|
|
240
|
-
wasQuoted = true;
|
|
241
|
-
} else if (ch === ',') {
|
|
242
|
-
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
243
|
-
currentVal = '';
|
|
244
|
-
wasQuoted = false;
|
|
245
|
-
} else if (ch === '\n' || ch === '\r') {
|
|
246
|
-
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
247
|
-
if (currentRow.some(v => v !== '')) {
|
|
248
|
-
rows.push(currentRow);
|
|
249
|
-
}
|
|
250
|
-
currentRow = [];
|
|
251
|
-
currentVal = '';
|
|
252
|
-
wasQuoted = false;
|
|
253
|
-
if (ch === '\r' && i + 1 < text.length && text[i + 1] === '\n') {
|
|
254
|
-
i++;
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
currentVal += ch;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (currentVal || currentRow.length > 0) {
|
|
263
|
-
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
264
|
-
if (currentRow.some(v => v !== '')) {
|
|
265
|
-
rows.push(currentRow);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return rows;
|
|
269
|
-
}
|