@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/core",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "author": "zuohuadong",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,32 +4,42 @@
4
4
 
5
5
  import { getAuthProvider } from './context.svelte';
6
6
  import { navigate } from './router';
7
- import { toast } from './toast.svelte';
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
- // ─── useLogin ─────────────────────────────────────────────────
11
+ // ─── Mutate Factory ─────────────────────────────────────────────
12
12
 
13
- export function useLogin() {
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: Record<string, unknown>): Promise<AuthActionResult> {
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 provider.login(params);
31
+ const result = await fn.call(provider, params);
22
32
  if (result.success) {
23
- toast.success(t('common.operationSuccess'));
24
- if (result.redirectTo) navigate(result.redirectTo);
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.loginFailed');
27
- toast.error(msg);
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.loginFailed');
32
- toast.error(msg);
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
- // ─── useLogout ────────────────────────────────────────────────
55
+ // ─── useLogin ─────────────────────────────────────────────────
46
56
 
47
- export function useLogout() {
48
- const provider = getAuthProvider();
49
- let isLoading = $state(false);
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
- async function mutate(params?: Record<string, unknown>): Promise<AuthActionResult> {
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
- return {
70
- mutate,
71
- get isLoading() { return isLoading; },
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
- const provider = getAuthProvider();
79
- let isLoading = $state(false);
80
-
81
- async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
82
- if (!provider?.register) throw new Error('AuthProvider.register not implemented');
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
- const provider = getAuthProvider();
113
- let isLoading = $state(false);
114
-
115
- async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
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
- const provider = getAuthProvider();
146
- let isLoading = $state(false);
147
-
148
- async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
149
- if (!provider?.updatePassword) throw new Error('AuthProvider.updatePassword not implemented');
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 ──────────────────────────────────────────
@@ -86,8 +86,7 @@ export function setResources(newResources: ResourceDefinition[]): void {
86
86
  }
87
87
 
88
88
  export function getResources(): ResourceDefinition[] {
89
- if (!resources || resources.length === 0) throw new Error('Resources not found. Did you call setResources in App.svelte?');
90
- return resources;
89
+ return resources ?? [];
91
90
  }
92
91
 
93
92
  export function getResource(nameOrIdentifier: string): ResourceDefinition {
@@ -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 { toast } from './toast.svelte';
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) toast.error(errorNotification || error.message || t('common.operationFailed'));
248
+ if (errorNotification !== false) notify({ type: 'error', message: typeof errorNotification === 'string' ? errorNotification : (error.message || t('common.operationFailed')) });
249
249
  } else {
250
- if (errorNotification !== false) toast.error(errorNotification || t('common.operationFailed') + ': ' + error.message);
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) toast.success(successNotification || t('common.createSuccess'));
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) toast.success(successNotification || t('common.updateSuccess'));
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()) { toast.warning(t('validation.required')); return; }
327
+ if (!runValidation()) { notify({ type: 'warning', message: t('validation.required') }); return; }
328
328
  redirectOverride = overrides?.redirect;
329
- if (action === 'create' || action === 'clone') await createMut.mutateAsync(values);
330
- else await updateMut.mutateAsync(values);
331
- // Untaint on success
332
- tainted = {};
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 ───────────────────────────────────────────────────
@@ -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; },
@@ -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;
@@ -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(mode);
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(colorTheme);
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(message)) {
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(message)) {
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 || '#');