@svadmin/core 0.19.1 → 0.19.3
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 +2 -4
- package/src/auth-hooks.svelte.ts +52 -123
- package/src/context.svelte.ts +1 -2
- package/src/form-hooks.svelte.ts +6 -7
- package/src/i18n.svelte.ts +1 -0
- package/src/index.ts +1 -1
- package/src/theme.svelte.ts +4 -4
- package/src/toast.svelte.ts +27 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/core",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.3",
|
|
4
4
|
"author": "zuohuadong",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -61,7 +61,5 @@
|
|
|
61
61
|
"sideEffects": false,
|
|
62
62
|
"type": "module",
|
|
63
63
|
"types": "src/index.ts",
|
|
64
|
-
"dependencies": {
|
|
65
|
-
"sonner-svelte": "^0.3.3"
|
|
66
|
-
}
|
|
64
|
+
"dependencies": {}
|
|
67
65
|
}
|
package/src/auth-hooks.svelte.ts
CHANGED
|
@@ -4,32 +4,42 @@
|
|
|
4
4
|
|
|
5
5
|
import { getAuthProvider } from './context.svelte';
|
|
6
6
|
import { navigate } from './router';
|
|
7
|
-
import {
|
|
7
|
+
import { notify } from './notification.svelte';
|
|
8
8
|
import { t } from './i18n.svelte';
|
|
9
|
-
import type { AuthActionResult, CheckResult, Identity } from './types';
|
|
9
|
+
import type { AuthActionResult, CheckResult, Identity, AuthProvider } from './types';
|
|
10
10
|
|
|
11
|
-
// ───
|
|
11
|
+
// ─── Mutate Factory ─────────────────────────────────────────────
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
interface CreateAuthMutationOptions {
|
|
14
|
+
method: keyof AuthProvider;
|
|
15
|
+
successMessage?: string | null;
|
|
16
|
+
errorMessage?: string;
|
|
17
|
+
onSuccess?: (result: AuthActionResult) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createAuthMutation(options: CreateAuthMutationOptions) {
|
|
14
21
|
const provider = getAuthProvider();
|
|
15
22
|
let isLoading = $state(false);
|
|
16
23
|
|
|
17
|
-
async function mutate(params
|
|
24
|
+
async function mutate(params?: Record<string, unknown>): Promise<AuthActionResult> {
|
|
18
25
|
if (!provider) throw new Error('AuthProvider not configured');
|
|
26
|
+
const fn = provider[options.method] as ((params?: Record<string, unknown>) => Promise<AuthActionResult>) | undefined;
|
|
27
|
+
if (!fn) throw new Error(`AuthProvider.${options.method} not implemented`);
|
|
28
|
+
|
|
19
29
|
isLoading = true;
|
|
20
30
|
try {
|
|
21
|
-
const result = await
|
|
31
|
+
const result = await fn.call(provider, params);
|
|
22
32
|
if (result.success) {
|
|
23
|
-
|
|
24
|
-
if (
|
|
33
|
+
if (options.successMessage) notify({ type: 'success', message: options.successMessage });
|
|
34
|
+
if (options.onSuccess) options.onSuccess(result);
|
|
25
35
|
} else {
|
|
26
|
-
const msg = result.error?.message ?? t('common.
|
|
27
|
-
|
|
36
|
+
const msg = result.error?.message ?? options.errorMessage ?? t('common.operationFailed');
|
|
37
|
+
notify({ type: 'error', message: msg });
|
|
28
38
|
}
|
|
29
39
|
return result;
|
|
30
40
|
} catch (err) {
|
|
31
|
-
const msg = err instanceof Error ? err.message : t('common.
|
|
32
|
-
|
|
41
|
+
const msg = err instanceof Error ? err.message : (options.errorMessage ?? t('common.operationFailed'));
|
|
42
|
+
notify({ type: 'error', message: msg });
|
|
33
43
|
return { success: false, error: { message: msg } };
|
|
34
44
|
} finally {
|
|
35
45
|
isLoading = false;
|
|
@@ -42,135 +52,54 @@ export function useLogin() {
|
|
|
42
52
|
};
|
|
43
53
|
}
|
|
44
54
|
|
|
45
|
-
// ───
|
|
55
|
+
// ─── useLogin ─────────────────────────────────────────────────
|
|
46
56
|
|
|
47
|
-
export function
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
export function useLogin() {
|
|
58
|
+
return createAuthMutation({
|
|
59
|
+
method: 'login',
|
|
60
|
+
successMessage: t('common.operationSuccess'),
|
|
61
|
+
errorMessage: t('common.loginFailed'),
|
|
62
|
+
onSuccess: (result) => { if (result.redirectTo) navigate(result.redirectTo); }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
50
65
|
|
|
51
|
-
|
|
52
|
-
if (!provider) throw new Error('AuthProvider not configured');
|
|
53
|
-
isLoading = true;
|
|
54
|
-
try {
|
|
55
|
-
const result = await provider.logout(params);
|
|
56
|
-
if (result.success) {
|
|
57
|
-
navigate(result.redirectTo ?? '/login');
|
|
58
|
-
}
|
|
59
|
-
return result;
|
|
60
|
-
} catch (err) {
|
|
61
|
-
const msg = err instanceof Error ? err.message : t('common.operationFailed');
|
|
62
|
-
toast.error(msg);
|
|
63
|
-
return { success: false, error: { message: msg } };
|
|
64
|
-
} finally {
|
|
65
|
-
isLoading = false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
66
|
+
// ─── useLogout ────────────────────────────────────────────────
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
export function useLogout() {
|
|
69
|
+
return createAuthMutation({
|
|
70
|
+
method: 'logout',
|
|
71
|
+
successMessage: null,
|
|
72
|
+
onSuccess: (result) => { navigate(result.redirectTo ?? '/login'); }
|
|
73
|
+
});
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
// ─── useRegister ──────────────────────────────────────────────
|
|
76
77
|
|
|
77
78
|
export function useRegister() {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
isLoading = true;
|
|
84
|
-
try {
|
|
85
|
-
const result = await provider.register(params);
|
|
86
|
-
if (result.success) {
|
|
87
|
-
toast.success(t('auth.registerSuccess'));
|
|
88
|
-
if (result.redirectTo) navigate(result.redirectTo);
|
|
89
|
-
} else {
|
|
90
|
-
const msg = result.error?.message ?? t('common.operationFailed');
|
|
91
|
-
toast.error(msg);
|
|
92
|
-
}
|
|
93
|
-
return result;
|
|
94
|
-
} catch (err) {
|
|
95
|
-
const msg = err instanceof Error ? err.message : t('common.operationFailed');
|
|
96
|
-
toast.error(msg);
|
|
97
|
-
return { success: false, error: { message: msg } };
|
|
98
|
-
} finally {
|
|
99
|
-
isLoading = false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
mutate,
|
|
105
|
-
get isLoading() { return isLoading; },
|
|
106
|
-
};
|
|
79
|
+
return createAuthMutation({
|
|
80
|
+
method: 'register',
|
|
81
|
+
successMessage: t('auth.registerSuccess'),
|
|
82
|
+
onSuccess: (result) => { if (result.redirectTo) navigate(result.redirectTo); }
|
|
83
|
+
});
|
|
107
84
|
}
|
|
108
85
|
|
|
109
86
|
// ─── useForgotPassword ───────────────────────────────────────
|
|
110
87
|
|
|
111
88
|
export function useForgotPassword() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (!provider?.forgotPassword) throw new Error('AuthProvider.forgotPassword not implemented');
|
|
117
|
-
isLoading = true;
|
|
118
|
-
try {
|
|
119
|
-
const result = await provider.forgotPassword(params);
|
|
120
|
-
if (result.success) {
|
|
121
|
-
toast.success(t('auth.resetLinkSent'));
|
|
122
|
-
} else {
|
|
123
|
-
const msg = result.error?.message ?? t('common.operationFailed');
|
|
124
|
-
toast.error(msg);
|
|
125
|
-
}
|
|
126
|
-
return result;
|
|
127
|
-
} catch (err) {
|
|
128
|
-
const msg = err instanceof Error ? err.message : t('common.operationFailed');
|
|
129
|
-
toast.error(msg);
|
|
130
|
-
return { success: false, error: { message: msg } };
|
|
131
|
-
} finally {
|
|
132
|
-
isLoading = false;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
mutate,
|
|
138
|
-
get isLoading() { return isLoading; },
|
|
139
|
-
};
|
|
89
|
+
return createAuthMutation({
|
|
90
|
+
method: 'forgotPassword',
|
|
91
|
+
successMessage: t('auth.resetLinkSent')
|
|
92
|
+
});
|
|
140
93
|
}
|
|
141
94
|
|
|
142
95
|
// ─── useUpdatePassword ───────────────────────────────────────
|
|
143
96
|
|
|
144
97
|
export function useUpdatePassword() {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
isLoading = true;
|
|
151
|
-
try {
|
|
152
|
-
const result = await provider.updatePassword(params);
|
|
153
|
-
if (result.success) {
|
|
154
|
-
toast.success(t('common.operationSuccess'));
|
|
155
|
-
if (result.redirectTo) navigate(result.redirectTo);
|
|
156
|
-
} else {
|
|
157
|
-
const msg = result.error?.message ?? t('common.operationFailed');
|
|
158
|
-
toast.error(msg);
|
|
159
|
-
}
|
|
160
|
-
return result;
|
|
161
|
-
} catch (err) {
|
|
162
|
-
const msg = err instanceof Error ? err.message : t('common.operationFailed');
|
|
163
|
-
toast.error(msg);
|
|
164
|
-
return { success: false, error: { message: msg } };
|
|
165
|
-
} finally {
|
|
166
|
-
isLoading = false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
mutate,
|
|
172
|
-
get isLoading() { return isLoading; },
|
|
173
|
-
};
|
|
98
|
+
return createAuthMutation({
|
|
99
|
+
method: 'updatePassword',
|
|
100
|
+
successMessage: t('common.operationSuccess'),
|
|
101
|
+
onSuccess: (result) => { if (result.redirectTo) navigate(result.redirectTo); }
|
|
102
|
+
});
|
|
174
103
|
}
|
|
175
104
|
|
|
176
105
|
// ─── useGetIdentity ──────────────────────────────────────────
|
package/src/context.svelte.ts
CHANGED
|
@@ -86,8 +86,7 @@ export function setResources(newResources: ResourceDefinition[]): void {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export function getResources(): ResourceDefinition[] {
|
|
89
|
-
|
|
90
|
-
return resources;
|
|
89
|
+
return resources ?? [];
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
export function getResource(nameOrIdentifier: string): ResourceDefinition {
|
package/src/form-hooks.svelte.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { useParsed } from './useParsed.svelte';
|
|
|
3
3
|
import { getAdminOptions } from './options.svelte';
|
|
4
4
|
import { getDataProviderForResource } from './context.svelte';
|
|
5
5
|
import { createQuery, createMutation } from '@tanstack/svelte-query';
|
|
6
|
-
import {
|
|
6
|
+
import { notify } from './notification.svelte';
|
|
7
7
|
import { t } from './i18n.svelte';
|
|
8
8
|
import { audit } from './audit';
|
|
9
9
|
import { navigate } from './router';
|
|
@@ -245,9 +245,9 @@ export function useForm<
|
|
|
245
245
|
const msg = Array.isArray(messages) ? messages[0] : messages;
|
|
246
246
|
setFieldError(field, msg);
|
|
247
247
|
}
|
|
248
|
-
if (errorNotification !== false)
|
|
248
|
+
if (errorNotification !== false) notify({ type: 'error', message: typeof errorNotification === 'string' ? errorNotification : (error.message || t('common.operationFailed')) });
|
|
249
249
|
} else {
|
|
250
|
-
if (errorNotification !== false)
|
|
250
|
+
if (errorNotification !== false) notify({ type: 'error', message: typeof errorNotification === 'string' ? errorNotification : (t('common.operationFailed') + ': ' + error.message) });
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
@@ -288,7 +288,7 @@ export function useForm<
|
|
|
288
288
|
mutationFn: (variables: TVariables) => provider.create<TData, TVariables>({ resource, variables, meta: mutationMeta }),
|
|
289
289
|
onSuccess: (data: { data: TData }) => {
|
|
290
290
|
if (invalidateScopes !== false) queryClient.invalidateQueries({ queryKey: [resource] });
|
|
291
|
-
if (successNotification !== false)
|
|
291
|
+
if (successNotification !== false) notify({ type: 'success', message: typeof successNotification === 'string' ? successNotification : t('common.createSuccess') });
|
|
292
292
|
audit({ action: 'create', resource, recordId: String((data.data as Record<string, unknown>).id) });
|
|
293
293
|
onMutationSuccess?.(data);
|
|
294
294
|
if (redirectOverride !== false) doRedirect(redirectOverride ?? redirectDefault);
|
|
@@ -301,7 +301,7 @@ export function useForm<
|
|
|
301
301
|
mutationFn: (variables: TVariables) => provider.update<TData, TVariables>({ resource, id: currentId!, variables, meta: mutationMeta }),
|
|
302
302
|
onSuccess: (data: { data: TData }) => {
|
|
303
303
|
if (invalidateScopes !== false) queryClient.invalidateQueries({ queryKey: [resource] });
|
|
304
|
-
if (successNotification !== false)
|
|
304
|
+
if (successNotification !== false) notify({ type: 'success', message: typeof successNotification === 'string' ? successNotification : t('common.updateSuccess') });
|
|
305
305
|
audit({ action: 'update', resource, recordId: String(currentId) });
|
|
306
306
|
onMutationSuccess?.(data);
|
|
307
307
|
if (redirectOverride !== false) doRedirect(redirectOverride ?? redirectDefault);
|
|
@@ -324,7 +324,7 @@ export function useForm<
|
|
|
324
324
|
if (result === false || cancelled) return;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
if (!runValidation()) {
|
|
327
|
+
if (!runValidation()) { notify({ type: 'warning', message: t('validation.required') }); return; }
|
|
328
328
|
redirectOverride = overrides?.redirect;
|
|
329
329
|
try {
|
|
330
330
|
if (action === 'create' || action === 'clone') await createMut.mutateAsync(values);
|
|
@@ -334,7 +334,6 @@ export function useForm<
|
|
|
334
334
|
} catch (error) {
|
|
335
335
|
// Errors are already handled by the mutation's onError callback.
|
|
336
336
|
// We catch here to prevent unhandled promise rejections.
|
|
337
|
-
handleHttpError(error instanceof Error ? error : new Error(String(error)));
|
|
338
337
|
}
|
|
339
338
|
}
|
|
340
339
|
|
package/src/i18n.svelte.ts
CHANGED
|
@@ -477,6 +477,7 @@ const locales: Record<string, Locale> = {
|
|
|
477
477
|
|
|
478
478
|
/** Detect best locale from browser language */
|
|
479
479
|
function detectLocale(): string {
|
|
480
|
+
if (typeof navigator === 'undefined') return 'en'; // Safe default for SSR
|
|
480
481
|
const browserLang = navigator.language || navigator.languages?.[0] || 'zh-CN';
|
|
481
482
|
// Exact match
|
|
482
483
|
if (locales[browserLang]) return browserLang;
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { matchRoute, navigate, currentPath, setActiveRouterProvider } from './ro
|
|
|
29
29
|
export { readURLState, writeURLState } from './url-sync';
|
|
30
30
|
export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, canAccessAsync } from './permissions';
|
|
31
31
|
export { useLive, useSubscription, usePublish } from './live.svelte';
|
|
32
|
-
export { toast } from './toast.svelte';
|
|
32
|
+
export { toast, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue } from './toast.svelte';
|
|
33
33
|
export { notify, closeNotification, setNotificationProvider, getNotificationProvider } from './notification.svelte';
|
|
34
34
|
export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation } from './i18n.svelte';
|
|
35
35
|
export { audit, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
|
package/src/theme.svelte.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface ColorPreset {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Built-in color presets — consumers can extend via registerColorPreset(). */
|
|
30
|
-
export const builtinPresets: Record<string, ColorPreset> = {
|
|
30
|
+
export const builtinPresets: Record<string, ColorPreset> = $state({
|
|
31
31
|
neutral: {
|
|
32
32
|
name: 'neutral',
|
|
33
33
|
label: 'Neutral',
|
|
@@ -189,7 +189,7 @@ export const builtinPresets: Record<string, ColorPreset> = {
|
|
|
189
189
|
'--chart-1': 'oklch(0.541 0.281 293.009)',
|
|
190
190
|
},
|
|
191
191
|
},
|
|
192
|
-
};
|
|
192
|
+
});
|
|
193
193
|
|
|
194
194
|
/** Register a custom color preset. Overwrites any built-in preset with the same name. */
|
|
195
195
|
export function registerColorPreset(preset: ColorPreset): void {
|
|
@@ -292,11 +292,11 @@ export function clearCssOverrides(keys?: string[]): void {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
// ── Color themes (display metadata, derived from presets) ──
|
|
295
|
-
export const colorThemes: { id: ColorTheme | string; label: string; color: string }[] = Object.values(builtinPresets).map(p => ({
|
|
295
|
+
export const colorThemes: { id: ColorTheme | string; label: string; color: string }[] = $state(Object.values(builtinPresets).map(p => ({
|
|
296
296
|
id: p.name as ColorTheme,
|
|
297
297
|
label: p.label,
|
|
298
298
|
color: p.color,
|
|
299
|
-
}));
|
|
299
|
+
})));
|
|
300
300
|
|
|
301
301
|
// ── Dark/Light mode ──────────────────────────────────────
|
|
302
302
|
|
package/src/toast.svelte.ts
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
// Toast notification system — wraps sonner-svelte for consistent API
|
|
2
|
-
// Maintains backward-compatible `toast.success()` / `toast.error()` etc.
|
|
3
|
-
|
|
4
|
-
import { toast as sonner } from 'sonner-svelte';
|
|
5
|
-
|
|
6
1
|
export type ToastType = 'success' | 'error' | 'info' | 'warning' | 'undoable';
|
|
7
2
|
|
|
8
|
-
export interface
|
|
3
|
+
export interface ToastItem {
|
|
9
4
|
id: number;
|
|
10
5
|
type: ToastType;
|
|
11
6
|
message: string;
|
|
12
|
-
duration
|
|
7
|
+
duration?: number;
|
|
13
8
|
onUndo?: () => void;
|
|
14
9
|
onTimeout?: () => void;
|
|
15
10
|
}
|
|
16
11
|
|
|
17
|
-
// Legacy compatibility — kept for existing consumers
|
|
18
12
|
let nextId = 0;
|
|
19
|
-
let toasts = $state<Toast[]>([]);
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
// Queue for simple toasts (consumed by UI Toast bridge)
|
|
15
|
+
let queue = $state<ToastItem[]>([]);
|
|
16
|
+
export function getToastQueue() { return queue; }
|
|
17
|
+
export function consumeToastQueue() { queue = []; }
|
|
18
|
+
|
|
19
|
+
// Promise Queue
|
|
20
|
+
let promiseQueue = $state<{ id: number; promise: Promise<any>; opts: { loading: string; success: string; error: string } }[]>([]);
|
|
21
|
+
export function getPromiseQueue() { return promiseQueue; }
|
|
22
|
+
export function consumePromiseQueue() { promiseQueue = []; }
|
|
23
|
+
|
|
24
|
+
// Legacy compatibility — kept for existing consumers (Undoable)
|
|
25
|
+
let toasts = $state<ToastItem[]>([]);
|
|
26
|
+
export function getToasts(): ToastItem[] { return toasts; }
|
|
27
|
+
|
|
28
|
+
export function addToast(
|
|
29
|
+
type: ToastType,
|
|
30
|
+
message: string,
|
|
31
|
+
duration = 3000,
|
|
32
|
+
options?: { onUndo?: () => void, onTimeout?: () => void }
|
|
33
|
+
): void {
|
|
27
34
|
if (type !== 'undoable') {
|
|
28
|
-
|
|
29
|
-
case 'success': sonner.success(message, { duration }); break;
|
|
30
|
-
case 'error': sonner.error(message, { duration }); break;
|
|
31
|
-
case 'warning': sonner.warning(message, { duration }); break;
|
|
32
|
-
case 'info': sonner.info(message, { duration }); break;
|
|
33
|
-
}
|
|
35
|
+
queue.push({ id: nextId++, type, message, duration });
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
// Undoable toasts still use custom system
|
|
38
38
|
const id = nextId++;
|
|
39
39
|
toasts = [...toasts, { id, type, message, duration, ...options }];
|
|
40
40
|
}
|
|
@@ -52,6 +52,8 @@ export const toast = {
|
|
|
52
52
|
undoable: (msg: string, duration: number, onUndo: () => void, onTimeout: () => void) =>
|
|
53
53
|
addToast('undoable', msg, duration, { onUndo, onTimeout }),
|
|
54
54
|
/** Promise toast — shows loading → success/error automatically */
|
|
55
|
-
promise: <T>(promise: Promise<T>, opts: { loading: string; success: string; error: string }) =>
|
|
56
|
-
|
|
55
|
+
promise: <T>(promise: Promise<T>, opts: { loading: string; success: string; error: string }) => {
|
|
56
|
+
promiseQueue.push({ id: nextId++, promise, opts });
|
|
57
|
+
return promise;
|
|
58
|
+
},
|
|
57
59
|
};
|