@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.
- 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 +5 -3
- 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
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
import { navigate } from './router';
|
|
3
|
-
import { getResource, getResources, getRouterProvider } from './context.svelte';
|
|
1
|
+
import { captureAdminContext } from './context.svelte';
|
|
4
2
|
import { useParsed } from './useParsed.svelte';
|
|
5
3
|
|
|
6
4
|
// ─── routing-hooks.svelte.ts ───────────────────────────────────
|
|
7
5
|
|
|
8
6
|
export function useGetToPath() {
|
|
9
|
-
const
|
|
7
|
+
const adminContext = captureAdminContext();
|
|
10
8
|
const parsed = useParsed();
|
|
11
9
|
|
|
12
10
|
return (options: { resource?: string; action?: 'list' | 'create' | 'edit' | 'show' | 'clone'; id?: string | number; meta?: Record<string, unknown> }) => {
|
|
@@ -31,7 +29,7 @@ export function useGetToPath() {
|
|
|
31
29
|
else if (action === 'clone' && id) path += `/clone/${id}`;
|
|
32
30
|
|
|
33
31
|
// Apply router mapping if the router provider defines custom path transformers
|
|
34
|
-
if (routerProvider?.go) {
|
|
32
|
+
if (adminContext.routerProvider?.go) {
|
|
35
33
|
// SvelteKit path generation can be injected here.
|
|
36
34
|
}
|
|
37
35
|
return path;
|
|
@@ -39,6 +37,7 @@ export function useGetToPath() {
|
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
export function useGo() {
|
|
40
|
+
const adminContext = captureAdminContext();
|
|
42
41
|
const getToPath = useGetToPath();
|
|
43
42
|
|
|
44
43
|
return (options: { to?: string; query?: Record<string, unknown>; type?: 'push' | 'replace'; resource?: string; action?: 'list' | 'create' | 'edit' | 'show' | 'clone'; id?: string | number }) => {
|
|
@@ -55,19 +54,13 @@ export function useGo() {
|
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
navigate(targetUrl, options.type === 'replace' ? { replaceState: true } : undefined);
|
|
57
|
+
void adminContext.navigate(targetUrl, options.type === 'replace' ? { replaceState: true } : undefined);
|
|
59
58
|
};
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
export function useBack() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (routerProvider?.back) {
|
|
66
|
-
routerProvider.back();
|
|
67
|
-
} else if (typeof window !== 'undefined') {
|
|
68
|
-
window.history.back();
|
|
69
|
-
}
|
|
70
|
-
};
|
|
62
|
+
const adminContext = captureAdminContext();
|
|
63
|
+
return () => adminContext.back();
|
|
71
64
|
}
|
|
72
65
|
|
|
73
66
|
/**
|
|
@@ -75,13 +68,15 @@ export function useBack() {
|
|
|
75
68
|
* (to avoid full page reloads in SSR apps)
|
|
76
69
|
*/
|
|
77
70
|
export function useLink() {
|
|
71
|
+
const adminContext = captureAdminContext();
|
|
78
72
|
return () => {
|
|
79
|
-
const routerProvider =
|
|
73
|
+
const routerProvider = adminContext.routerProvider as unknown as Record<string, unknown> | undefined;
|
|
80
74
|
return (routerProvider?.Link as string) ?? 'a';
|
|
81
75
|
};
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
export function useResource(resourceName?: string) {
|
|
79
|
+
const adminContext = captureAdminContext();
|
|
85
80
|
const parsed = useParsed();
|
|
86
81
|
|
|
87
82
|
// Derived resource — re-evaluates when parsed.resource or resourceName changes
|
|
@@ -89,7 +84,7 @@ export function useResource(resourceName?: string) {
|
|
|
89
84
|
const target = resourceName ?? parsed.resource;
|
|
90
85
|
if (!target) return undefined;
|
|
91
86
|
try {
|
|
92
|
-
return getResource(target);
|
|
87
|
+
return adminContext.getResource(target);
|
|
93
88
|
} catch {
|
|
94
89
|
return undefined;
|
|
95
90
|
}
|
|
@@ -97,7 +92,7 @@ export function useResource(resourceName?: string) {
|
|
|
97
92
|
|
|
98
93
|
return {
|
|
99
94
|
get resource() { return resolvedResource; },
|
|
100
|
-
get resources() { return
|
|
95
|
+
get resources() { return adminContext.resources; },
|
|
101
96
|
get identifier() { return resolvedResource?.identifier ?? resolvedResource?.name; },
|
|
102
97
|
/** 当前 URL 解析出的 parent 资源信息(嵌套资源路由) */
|
|
103
98
|
get parent() {
|
|
@@ -111,7 +106,7 @@ export function useResource(resourceName?: string) {
|
|
|
111
106
|
/** Resolve a resource by name — useful for dynamic lookups inside callbacks */
|
|
112
107
|
select: (name: string) => {
|
|
113
108
|
try {
|
|
114
|
-
const res = getResource(name);
|
|
109
|
+
const res = adminContext.getResource(name);
|
|
115
110
|
return { resource: res, identifier: res.identifier ?? res.name };
|
|
116
111
|
} catch {
|
|
117
112
|
return { resource: undefined, identifier: undefined };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useParsed } from './useParsed.svelte';
|
|
2
2
|
import { useList } from './query-hooks.svelte';
|
|
3
3
|
import { readURLState, writeURLState } from './url-sync';
|
|
4
|
+
import { captureAdminContext } from './context.svelte';
|
|
4
5
|
import { getAdminOptions } from './options.svelte';
|
|
5
6
|
import type { Pagination, Sort, Filter, BaseRecord, HttpError, KnownResources, GetListResult } from './types';
|
|
6
7
|
import type { UseListOptions, MaybeGetter } from './query-hooks.svelte';
|
|
@@ -29,6 +30,7 @@ export function useTable<
|
|
|
29
30
|
TError = HttpError,
|
|
30
31
|
TSearchVariables = Record<string, unknown>
|
|
31
32
|
>(optionsOrGetter: MaybeGetter<UseTableOptions<TQueryFnData, TError, TSearchVariables>> = {}) {
|
|
33
|
+
const adminContext = captureAdminContext();
|
|
32
34
|
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
33
35
|
const parsed = useParsed();
|
|
34
36
|
|
|
@@ -50,7 +52,7 @@ export function useTable<
|
|
|
50
52
|
let initFilters = initialOpts.filters?.initial ?? [];
|
|
51
53
|
|
|
52
54
|
if (syncWithLocation && typeof window !== 'undefined') {
|
|
53
|
-
const urlState = readURLState();
|
|
55
|
+
const urlState = readURLState(adminContext);
|
|
54
56
|
if (urlState.page || urlState.pageSize) {
|
|
55
57
|
initPagination = {
|
|
56
58
|
current: urlState.page ?? initPagination.current,
|
|
@@ -128,12 +130,12 @@ export function useTable<
|
|
|
128
130
|
sortField: currentSorters[0]?.field,
|
|
129
131
|
sortOrder: currentSorters[0]?.order,
|
|
130
132
|
filters: currentFilters,
|
|
131
|
-
});
|
|
133
|
+
}, adminContext);
|
|
132
134
|
});
|
|
133
135
|
|
|
134
136
|
$effect(() => {
|
|
135
137
|
const handler = () => {
|
|
136
|
-
const urlState = readURLState();
|
|
138
|
+
const urlState = readURLState(adminContext);
|
|
137
139
|
const newPage = urlState.page ?? 1;
|
|
138
140
|
if (newPage !== pagination.current) {
|
|
139
141
|
pagination = { ...pagination, current: newPage };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
/* eslint-disable svelte/prefer-svelte-reactivity */
|
|
2
1
|
// TableState — Svelte 5 Class State Pattern for table data management
|
|
3
2
|
// Reactive class that encapsulates all table state with fine-grained $state/$derived
|
|
4
3
|
|
|
5
4
|
import { useParsed } from './useParsed.svelte';
|
|
6
5
|
import { useList } from './query-hooks.svelte';
|
|
7
6
|
import { readURLState, writeURLState } from './url-sync';
|
|
7
|
+
import { captureAdminContext } from './context.svelte';
|
|
8
|
+
import type { AdminContextAccessor } from './context.svelte';
|
|
8
9
|
import type { Pagination, Sort, Filter, BaseRecord, HttpError, KnownResources, GetListResult } from './types';
|
|
9
10
|
import type { UseListOptions } from './query-hooks.svelte';
|
|
10
11
|
|
|
@@ -53,11 +54,13 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
53
54
|
private filterDefaultBehavior: FilterSetMode;
|
|
54
55
|
private syncWithLocation: boolean;
|
|
55
56
|
private paginationMode: string;
|
|
57
|
+
private adminContext: AdminContextAccessor;
|
|
56
58
|
|
|
57
59
|
// ─── Query result ─────────────────────────────────────────────
|
|
58
60
|
private _queryResult;
|
|
59
61
|
|
|
60
62
|
constructor(options: TableStateOptions<TData, TError> = {}) {
|
|
63
|
+
this.adminContext = captureAdminContext();
|
|
61
64
|
const parsed = useParsed();
|
|
62
65
|
const resource = options.resource ?? parsed.resource ?? '';
|
|
63
66
|
|
|
@@ -75,7 +78,7 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
75
78
|
let initFilters = options.filters?.initial ?? [];
|
|
76
79
|
|
|
77
80
|
if (this.syncWithLocation && typeof window !== 'undefined') {
|
|
78
|
-
const urlState = readURLState();
|
|
81
|
+
const urlState = readURLState(this.adminContext);
|
|
79
82
|
if (urlState.page || urlState.pageSize) {
|
|
80
83
|
initPagination = {
|
|
81
84
|
current: urlState.page ?? initPagination.current,
|
|
@@ -116,7 +119,7 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
116
119
|
sortField: this.currentSorters[0]?.field,
|
|
117
120
|
sortOrder: this.currentSorters[0]?.order,
|
|
118
121
|
filters: this.currentFilters,
|
|
119
|
-
});
|
|
122
|
+
}, this.adminContext);
|
|
120
123
|
});
|
|
121
124
|
}
|
|
122
125
|
}
|
package/src/task-hooks.svelte.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query';
|
|
2
2
|
import { getAdminOptions } from './options.svelte';
|
|
3
|
-
import {
|
|
3
|
+
import { captureAdminContext } from './context.svelte';
|
|
4
|
+
import type { AdminContextAccessor } from './context.svelte';
|
|
4
5
|
import { checkError, createOvertimeTracker, fireErrorNotification, fireSuccessNotification } from './hook-utils.svelte';
|
|
5
6
|
import type { NotificationConfig, OvertimeOptions } from './hook-utils.svelte';
|
|
6
|
-
import {
|
|
7
|
+
import { useTranslation } from './i18n.svelte';
|
|
7
8
|
import type {
|
|
8
9
|
HttpError,
|
|
9
10
|
SubmitTaskOptions,
|
|
@@ -15,8 +16,13 @@ import type {
|
|
|
15
16
|
|
|
16
17
|
function normalizeTaskProvider<TTask extends TaskRecord = TaskRecord>(
|
|
17
18
|
provider?: TaskProvider<TTask>,
|
|
19
|
+
adminContext: AdminContextAccessor = captureAdminContext(),
|
|
18
20
|
): TaskProvider<TTask> {
|
|
19
|
-
|
|
21
|
+
const activeProvider = provider ?? adminContext.taskProvider;
|
|
22
|
+
if (!activeProvider) {
|
|
23
|
+
throw new Error('TaskProvider not found. Did you call setTaskProvider in App.svelte?');
|
|
24
|
+
}
|
|
25
|
+
return activeProvider as TaskProvider<TTask>;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export interface UseSubmitTaskOptions {
|
|
@@ -39,19 +45,21 @@ export function useSubmitTask<
|
|
|
39
45
|
TTask extends TaskRecord = TaskRecord,
|
|
40
46
|
TError = HttpError,
|
|
41
47
|
>(options: UseSubmitTaskOptions = {}) {
|
|
48
|
+
const adminContext = captureAdminContext();
|
|
49
|
+
const i18n = useTranslation();
|
|
42
50
|
const adminOptions = getAdminOptions();
|
|
43
51
|
const queryClient = useQueryClient();
|
|
44
52
|
const overtime = createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime);
|
|
45
53
|
|
|
46
54
|
const mutation = createMutation<TaskHandle<TTask>, TError, UseSubmitTaskMutateParams<TTask>>(() => ({
|
|
47
55
|
mutationFn: async (params) => {
|
|
48
|
-
const provider = normalizeTaskProvider<TTask>(params.taskProvider);
|
|
56
|
+
const provider = normalizeTaskProvider<TTask>(params.taskProvider, adminContext);
|
|
49
57
|
return provider.submit(params.taskName, params.options);
|
|
50
58
|
},
|
|
51
59
|
onSuccess: (data, params, context) => {
|
|
52
60
|
fireSuccessNotification(
|
|
53
61
|
params.successNotification,
|
|
54
|
-
t('task.submitSuccess'),
|
|
62
|
+
i18n.t('task.submitSuccess'),
|
|
55
63
|
data,
|
|
56
64
|
params.options,
|
|
57
65
|
params.taskName,
|
|
@@ -60,10 +68,10 @@ export function useSubmitTask<
|
|
|
60
68
|
options.mutationOptions?.onSuccess?.(data, params, context);
|
|
61
69
|
},
|
|
62
70
|
onError: (error, params, context) => {
|
|
63
|
-
checkError(error);
|
|
71
|
+
checkError(error, adminContext);
|
|
64
72
|
fireErrorNotification(
|
|
65
73
|
params.errorNotification,
|
|
66
|
-
t('task.submitFailed'),
|
|
74
|
+
i18n.t('task.submitFailed'),
|
|
67
75
|
error,
|
|
68
76
|
params.taskName,
|
|
69
77
|
);
|
|
@@ -93,10 +101,12 @@ export interface UseTaskOptions<TTask extends TaskRecord = TaskRecord, _TError =
|
|
|
93
101
|
export function useTask<TTask extends TaskRecord = TaskRecord, TError = HttpError>(
|
|
94
102
|
options: UseTaskOptions<TTask, TError> = {},
|
|
95
103
|
) {
|
|
104
|
+
const adminContext = captureAdminContext();
|
|
105
|
+
const i18n = useTranslation();
|
|
96
106
|
const adminOptions = getAdminOptions();
|
|
97
107
|
|
|
98
108
|
const query = createQuery<TTask, TError>(() => {
|
|
99
|
-
const provider = normalizeTaskProvider<TTask>(options.taskProvider);
|
|
109
|
+
const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
|
|
100
110
|
const queryOptions = options.queryOptions;
|
|
101
111
|
return {
|
|
102
112
|
queryKey: ['task', options.taskId],
|
|
@@ -125,8 +135,8 @@ export function useTask<TTask extends TaskRecord = TaskRecord, TError = HttpErro
|
|
|
125
135
|
}
|
|
126
136
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
127
137
|
lastErrorAt = query.errorUpdatedAt;
|
|
128
|
-
checkError(query.error);
|
|
129
|
-
fireErrorNotification(options.errorNotification, t('task.fetchFailed'), query.error, options.taskId);
|
|
138
|
+
checkError(query.error, adminContext);
|
|
139
|
+
fireErrorNotification(options.errorNotification, i18n.t('task.fetchFailed'), query.error, options.taskId);
|
|
130
140
|
}
|
|
131
141
|
});
|
|
132
142
|
|
|
@@ -158,10 +168,12 @@ export interface UseTaskListOptions<TTask extends TaskRecord = TaskRecord, _TErr
|
|
|
158
168
|
export function useTaskList<TTask extends TaskRecord = TaskRecord, TError = HttpError>(
|
|
159
169
|
options: UseTaskListOptions<TTask, TError> = {},
|
|
160
170
|
) {
|
|
171
|
+
const adminContext = captureAdminContext();
|
|
172
|
+
const i18n = useTranslation();
|
|
161
173
|
const adminOptions = getAdminOptions();
|
|
162
174
|
|
|
163
175
|
const query = createQuery<TaskListResult<TTask>, TError>(() => {
|
|
164
|
-
const provider = normalizeTaskProvider<TTask>(options.taskProvider);
|
|
176
|
+
const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
|
|
165
177
|
const queryOptions = options.queryOptions;
|
|
166
178
|
return {
|
|
167
179
|
queryKey: ['taskList', options.dlq ? 'dlq' : 'default', options.params],
|
|
@@ -194,8 +206,8 @@ export function useTaskList<TTask extends TaskRecord = TaskRecord, TError = Http
|
|
|
194
206
|
}
|
|
195
207
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
196
208
|
lastErrorAt = query.errorUpdatedAt;
|
|
197
|
-
checkError(query.error);
|
|
198
|
-
fireErrorNotification(options.errorNotification, t('task.fetchListFailed'), query.error, options.dlq ? 'taskDlq' : 'tasks');
|
|
209
|
+
checkError(query.error, adminContext);
|
|
210
|
+
fireErrorNotification(options.errorNotification, i18n.t('task.fetchListFailed'), query.error, options.dlq ? 'taskDlq' : 'tasks');
|
|
199
211
|
}
|
|
200
212
|
});
|
|
201
213
|
|
|
@@ -217,11 +229,12 @@ export interface UseTaskSubscriptionOptions<TTask extends TaskRecord = TaskRecor
|
|
|
217
229
|
export function useTaskSubscription<TTask extends TaskRecord = TaskRecord>(
|
|
218
230
|
options: UseTaskSubscriptionOptions<TTask>,
|
|
219
231
|
): void {
|
|
232
|
+
const adminContext = captureAdminContext();
|
|
220
233
|
$effect(() => {
|
|
221
234
|
const enabled = options.enabled ?? true;
|
|
222
235
|
if (!enabled) return;
|
|
223
236
|
|
|
224
|
-
const provider = normalizeTaskProvider<TTask>(options.taskProvider);
|
|
237
|
+
const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
|
|
225
238
|
if (!provider.subscribe) {
|
|
226
239
|
throw new Error('TaskProvider does not implement subscribe');
|
|
227
240
|
}
|