@svadmin/core 0.19.0 → 0.19.2
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 +1 -1
- package/src/auth-hooks.svelte.ts +52 -123
- package/src/context.svelte.ts +1 -2
- package/src/form-hooks.svelte.ts +15 -10
- package/src/hooks.svelte.ts +5 -0
- package/src/i18n.svelte.ts +1 -0
- package/src/theme.svelte.ts +6 -6
- package/src/unsaved-changes.svelte.ts +2 -4
package/package.json
CHANGED
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,12 +324,17 @@ 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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
329
|
+
try {
|
|
330
|
+
if (action === 'create' || action === 'clone') await createMut.mutateAsync(values);
|
|
331
|
+
else await updateMut.mutateAsync(values);
|
|
332
|
+
// Untaint on success
|
|
333
|
+
tainted = {};
|
|
334
|
+
} catch (error) {
|
|
335
|
+
// Errors are already handled by the mutation's onError callback.
|
|
336
|
+
// We catch here to prevent unhandled promise rejections.
|
|
337
|
+
}
|
|
333
338
|
}
|
|
334
339
|
|
|
335
340
|
// ─── AutoSave ───────────────────────────────────────────────────
|
package/src/hooks.svelte.ts
CHANGED
|
@@ -170,6 +170,11 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
|
|
|
170
170
|
debounceTimer = setTimeout(() => { searchText = value; }, debounceMs);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
// Cleanup debounce timer on unmount
|
|
174
|
+
$effect(() => {
|
|
175
|
+
return () => { if (debounceTimer) clearTimeout(debounceTimer); };
|
|
176
|
+
});
|
|
177
|
+
|
|
173
178
|
return {
|
|
174
179
|
query,
|
|
175
180
|
get options() { return selectOptions; },
|
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/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
|
|
|
@@ -336,7 +336,7 @@ function applyTheme(m: ThemeMode): void {
|
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
// Apply on init
|
|
339
|
-
applyTheme(
|
|
339
|
+
applyTheme(getStoredTheme());
|
|
340
340
|
|
|
341
341
|
// Listen for system preference changes
|
|
342
342
|
if (typeof window !== 'undefined') {
|
|
@@ -383,7 +383,7 @@ function applyColorTheme(ct: ColorTheme): void {
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
// Apply on init
|
|
386
|
-
applyColorTheme(
|
|
386
|
+
applyColorTheme(getStoredColorTheme());
|
|
387
387
|
|
|
388
388
|
export function getColorTheme(): ColorTheme {
|
|
389
389
|
return colorTheme;
|
|
@@ -71,8 +71,6 @@ export function initUnsavedChangesNotifier(options?: {
|
|
|
71
71
|
}) {
|
|
72
72
|
if (typeof window === 'undefined') return;
|
|
73
73
|
|
|
74
|
-
const message = t('common.unsavedChanges');
|
|
75
|
-
|
|
76
74
|
// 1. Browser tab close / refresh
|
|
77
75
|
const onBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
78
76
|
if (!dirty) return;
|
|
@@ -85,7 +83,7 @@ export function initUnsavedChangesNotifier(options?: {
|
|
|
85
83
|
if (options?.beforeNavigate) {
|
|
86
84
|
options.beforeNavigate(({ cancel }) => {
|
|
87
85
|
if (dirty) {
|
|
88
|
-
if (!confirm(
|
|
86
|
+
if (!confirm(t('common.unsavedChanges'))) {
|
|
89
87
|
cancel();
|
|
90
88
|
} else {
|
|
91
89
|
dirty = false;
|
|
@@ -97,7 +95,7 @@ export function initUnsavedChangesNotifier(options?: {
|
|
|
97
95
|
// 3. Hash-based fallback for SPAs without a framework router
|
|
98
96
|
const onHashChange = (e: HashChangeEvent) => {
|
|
99
97
|
if (!dirty) return;
|
|
100
|
-
if (!confirm(
|
|
98
|
+
if (!confirm(t('common.unsavedChanges'))) {
|
|
101
99
|
e.preventDefault();
|
|
102
100
|
if (e.oldURL) {
|
|
103
101
|
history.replaceState(null, '', new URL(e.oldURL).hash || '#');
|