@svadmin/core 0.34.2 → 0.35.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 +1 -1
- package/src/audit.ts +11 -3
- package/src/auth-hooks.svelte.ts +83 -13
- package/src/context.svelte.ts +180 -23
- package/src/form-hooks.svelte.ts +63 -15
- package/src/hook-utils.svelte.ts +31 -19
- package/src/hooks.svelte.ts +175 -28
- package/src/index.ts +33 -3
- package/src/live.svelte.ts +5 -1
- package/src/mutation-hooks.svelte.ts +116 -19
- package/src/notification.svelte.ts +14 -4
- package/src/provider-bundle.ts +200 -0
- package/src/query-hooks.svelte.ts +54 -9
- package/src/task-hooks.svelte.ts +66 -19
- package/src/types.ts +25 -0
- package/src/useCan.ts +18 -4
- package/src/utility-hooks.svelte.ts +31 -2
package/src/task-hooks.svelte.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-qu
|
|
|
2
2
|
import { getAdminOptions } from './options.svelte';
|
|
3
3
|
import { captureAdminContext } from './context.svelte';
|
|
4
4
|
import type { AdminContextAccessor } from './context.svelte';
|
|
5
|
+
import {
|
|
6
|
+
appendTenantCacheKey,
|
|
7
|
+
queryKeyMatchesTenant,
|
|
8
|
+
resolveTenantProviderMeta,
|
|
9
|
+
} from './provider-bundle';
|
|
5
10
|
import { checkError, createOvertimeTracker, fireErrorNotification, fireSuccessNotification } from './hook-utils.svelte';
|
|
6
11
|
import type { NotificationConfig, OvertimeOptions } from './hook-utils.svelte';
|
|
7
12
|
import { useTranslation } from './i18n.svelte';
|
|
@@ -54,27 +59,41 @@ export function useSubmitTask<
|
|
|
54
59
|
const mutation = createMutation<TaskHandle<TTask>, TError, UseSubmitTaskMutateParams<TTask>>(() => ({
|
|
55
60
|
mutationFn: async (params) => {
|
|
56
61
|
const provider = normalizeTaskProvider<TTask>(params.taskProvider, adminContext);
|
|
57
|
-
|
|
62
|
+
const providerMeta = adminContext.tenant
|
|
63
|
+
? {
|
|
64
|
+
...(params.options?.meta ?? {}),
|
|
65
|
+
...resolveTenantProviderMeta(adminContext.tenant, adminContext.tenantAdapter),
|
|
66
|
+
}
|
|
67
|
+
: params.options?.meta;
|
|
68
|
+
const taskOptions = providerMeta === params.options?.meta
|
|
69
|
+
? params.options
|
|
70
|
+
: { ...params.options, meta: providerMeta };
|
|
71
|
+
return provider.submit(params.taskName, taskOptions);
|
|
58
72
|
},
|
|
59
73
|
onSuccess: (data, params, context) => {
|
|
60
|
-
fireSuccessNotification(
|
|
61
|
-
params.successNotification,
|
|
62
|
-
i18n.t('task.submitSuccess'),
|
|
74
|
+
fireSuccessNotification({
|
|
75
|
+
config: params.successNotification,
|
|
76
|
+
defaultMessage: i18n.t('task.submitSuccess'),
|
|
63
77
|
data,
|
|
64
|
-
params.options,
|
|
65
|
-
params.taskName,
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
values: params.options,
|
|
79
|
+
resource: params.taskName,
|
|
80
|
+
provider: adminContext.notificationProvider,
|
|
81
|
+
});
|
|
82
|
+
void queryClient.invalidateQueries({
|
|
83
|
+
predicate: (query) => query.queryKey[0] === 'taskList'
|
|
84
|
+
&& queryKeyMatchesTenant(query.queryKey, adminContext.tenantCacheKey),
|
|
85
|
+
});
|
|
68
86
|
options.mutationOptions?.onSuccess?.(data, params, context);
|
|
69
87
|
},
|
|
70
88
|
onError: (error, params, context) => {
|
|
71
89
|
checkError(error, adminContext);
|
|
72
|
-
fireErrorNotification(
|
|
73
|
-
params.errorNotification,
|
|
74
|
-
i18n.t('task.submitFailed'),
|
|
90
|
+
fireErrorNotification({
|
|
91
|
+
config: params.errorNotification,
|
|
92
|
+
defaultMessage: i18n.t('task.submitFailed'),
|
|
75
93
|
error,
|
|
76
|
-
params.taskName,
|
|
77
|
-
|
|
94
|
+
resource: params.taskName,
|
|
95
|
+
provider: adminContext.notificationProvider,
|
|
96
|
+
});
|
|
78
97
|
options.mutationOptions?.onError?.(error, params, context);
|
|
79
98
|
},
|
|
80
99
|
}));
|
|
@@ -109,7 +128,7 @@ export function useTask<TTask extends TaskRecord = TaskRecord, TError = HttpErro
|
|
|
109
128
|
const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
|
|
110
129
|
const queryOptions = options.queryOptions;
|
|
111
130
|
return {
|
|
112
|
-
queryKey: ['task', options.taskId],
|
|
131
|
+
queryKey: appendTenantCacheKey(['task', options.taskId], adminContext.tenantCacheKey),
|
|
113
132
|
queryFn: async () => {
|
|
114
133
|
if (!options.taskId) throw new Error('useTask requires a taskId');
|
|
115
134
|
return provider.get(options.taskId);
|
|
@@ -131,12 +150,24 @@ export function useTask<TTask extends TaskRecord = TaskRecord, TError = HttpErro
|
|
|
131
150
|
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
|
|
132
151
|
lastSuccessAt = query.dataUpdatedAt;
|
|
133
152
|
if (options.successNotification) {
|
|
134
|
-
fireSuccessNotification(
|
|
153
|
+
fireSuccessNotification({
|
|
154
|
+
config: options.successNotification,
|
|
155
|
+
defaultMessage: '',
|
|
156
|
+
data: query.data,
|
|
157
|
+
resource: options.taskId,
|
|
158
|
+
provider: adminContext.notificationProvider,
|
|
159
|
+
});
|
|
135
160
|
}
|
|
136
161
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
137
162
|
lastErrorAt = query.errorUpdatedAt;
|
|
138
163
|
checkError(query.error, adminContext);
|
|
139
|
-
fireErrorNotification(
|
|
164
|
+
fireErrorNotification({
|
|
165
|
+
config: options.errorNotification,
|
|
166
|
+
defaultMessage: i18n.t('task.fetchFailed'),
|
|
167
|
+
error: query.error,
|
|
168
|
+
resource: options.taskId,
|
|
169
|
+
provider: adminContext.notificationProvider,
|
|
170
|
+
});
|
|
140
171
|
}
|
|
141
172
|
});
|
|
142
173
|
|
|
@@ -176,7 +207,10 @@ export function useTaskList<TTask extends TaskRecord = TaskRecord, TError = Http
|
|
|
176
207
|
const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
|
|
177
208
|
const queryOptions = options.queryOptions;
|
|
178
209
|
return {
|
|
179
|
-
queryKey:
|
|
210
|
+
queryKey: appendTenantCacheKey(
|
|
211
|
+
['taskList', options.dlq ? 'dlq' : 'default', options.params],
|
|
212
|
+
adminContext.tenantCacheKey,
|
|
213
|
+
),
|
|
180
214
|
queryFn: async () => {
|
|
181
215
|
if (options.dlq) {
|
|
182
216
|
if (!provider.listDlq) throw new Error('TaskProvider does not implement listDlq');
|
|
@@ -202,12 +236,25 @@ export function useTaskList<TTask extends TaskRecord = TaskRecord, TError = Http
|
|
|
202
236
|
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
|
|
203
237
|
lastSuccessAt = query.dataUpdatedAt;
|
|
204
238
|
if (options.successNotification) {
|
|
205
|
-
fireSuccessNotification(
|
|
239
|
+
fireSuccessNotification({
|
|
240
|
+
config: options.successNotification,
|
|
241
|
+
defaultMessage: '',
|
|
242
|
+
data: query.data,
|
|
243
|
+
values: options.params,
|
|
244
|
+
resource: options.dlq ? 'taskDlq' : 'tasks',
|
|
245
|
+
provider: adminContext.notificationProvider,
|
|
246
|
+
});
|
|
206
247
|
}
|
|
207
248
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
208
249
|
lastErrorAt = query.errorUpdatedAt;
|
|
209
250
|
checkError(query.error, adminContext);
|
|
210
|
-
fireErrorNotification(
|
|
251
|
+
fireErrorNotification({
|
|
252
|
+
config: options.errorNotification,
|
|
253
|
+
defaultMessage: i18n.t('task.fetchListFailed'),
|
|
254
|
+
error: query.error,
|
|
255
|
+
resource: options.dlq ? 'taskDlq' : 'tasks',
|
|
256
|
+
provider: adminContext.notificationProvider,
|
|
257
|
+
});
|
|
211
258
|
}
|
|
212
259
|
});
|
|
213
260
|
|
package/src/types.ts
CHANGED
|
@@ -382,6 +382,28 @@ export type MutationMode = 'pessimistic' | 'optimistic' | 'undoable';
|
|
|
382
382
|
|
|
383
383
|
// ─── ResourceDefinition ───────────────────────────────────────
|
|
384
384
|
|
|
385
|
+
/** 声明资源使用的传输协议;具体 URL/序列化行为由对应 provider 实现。 */
|
|
386
|
+
export interface ResourceTransportConfig {
|
|
387
|
+
readonly type: string;
|
|
388
|
+
readonly endpoint?: string;
|
|
389
|
+
readonly options?: Readonly<Record<string, unknown>>;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** 声明资源使用的后端 adapter,不在 Core 内绑定具体运行时实现。 */
|
|
393
|
+
export interface ResourceAdapterConfig {
|
|
394
|
+
readonly name: string;
|
|
395
|
+
readonly version?: string;
|
|
396
|
+
readonly options?: Readonly<Record<string, unknown>>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** 每资源的 provider 选择与声明性 transport/adapter 元数据。 */
|
|
400
|
+
export interface ResourceProviderConfig {
|
|
401
|
+
readonly dataProviderName?: string;
|
|
402
|
+
readonly transport?: string | ResourceTransportConfig;
|
|
403
|
+
readonly adapter?: string | ResourceAdapterConfig;
|
|
404
|
+
readonly meta?: Readonly<Record<string, unknown>>;
|
|
405
|
+
}
|
|
406
|
+
|
|
385
407
|
export interface ResourceDefinition {
|
|
386
408
|
name: string;
|
|
387
409
|
/** Unique identifier — use when multiple resources share the same `name` but target different DataProviders */
|
|
@@ -401,6 +423,9 @@ export interface ResourceDefinition {
|
|
|
401
423
|
menuOrder?: number;
|
|
402
424
|
/** Navigation group name — resources with the same group are displayed in a collapsible section */
|
|
403
425
|
group?: string;
|
|
426
|
+
/** DataProvider 选择及声明性 transport/adapter 元数据。 */
|
|
427
|
+
provider?: ResourceProviderConfig;
|
|
428
|
+
/** @deprecated 新代码优先使用 `provider.dataProviderName` 选择 DataProvider。 */
|
|
404
429
|
meta?: Record<string, unknown> & { dataProviderName?: string; parent?: string };
|
|
405
430
|
}
|
|
406
431
|
|
package/src/useCan.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// useCan — reactive permission check hook with TanStack Query integration
|
|
2
2
|
|
|
3
3
|
import { createQuery } from '@tanstack/svelte-query';
|
|
4
|
-
import {
|
|
4
|
+
import { captureAdminContext } from './context.svelte';
|
|
5
|
+
import { appendTenantCacheKey } from './provider-bundle';
|
|
5
6
|
import type { Action, CanResult } from './permissions.svelte';
|
|
6
7
|
|
|
7
8
|
export interface UseCanOptions {
|
|
@@ -32,11 +33,24 @@ export interface UseCanResult {
|
|
|
32
33
|
* ```
|
|
33
34
|
*/
|
|
34
35
|
export function useCan(options: () => UseCanOptions): UseCanResult {
|
|
36
|
+
const adminContext = captureAdminContext();
|
|
35
37
|
const query = createQuery<CanResult>(() => {
|
|
36
38
|
const opts = options();
|
|
39
|
+
const provider = adminContext.accessControlProvider;
|
|
37
40
|
return {
|
|
38
|
-
queryKey: [
|
|
39
|
-
|
|
41
|
+
queryKey: appendTenantCacheKey([
|
|
42
|
+
'useCan', opts.resource, opts.action, opts.params, opts.meta,
|
|
43
|
+
], adminContext.tenantCacheKey),
|
|
44
|
+
queryFn: async () => {
|
|
45
|
+
if (!provider) return { can: true };
|
|
46
|
+
const result = await provider.can({
|
|
47
|
+
resource: opts.resource,
|
|
48
|
+
action: opts.action,
|
|
49
|
+
params: opts.params,
|
|
50
|
+
meta: adminContext.getProviderMeta(opts.resource, opts.meta),
|
|
51
|
+
});
|
|
52
|
+
return Array.isArray(result) ? (result[0] ?? { can: false }) : result;
|
|
53
|
+
},
|
|
40
54
|
enabled: opts.queryOptions?.enabled ?? true,
|
|
41
55
|
staleTime: opts.queryOptions?.staleTime ?? 5 * 60 * 1000,
|
|
42
56
|
};
|
|
@@ -44,7 +58,7 @@ export function useCan(options: () => UseCanOptions): UseCanResult {
|
|
|
44
58
|
|
|
45
59
|
return {
|
|
46
60
|
get allowed() {
|
|
47
|
-
const p =
|
|
61
|
+
const p = adminContext.accessControlProvider;
|
|
48
62
|
if (!p) return true;
|
|
49
63
|
return (query.data as CanResult | undefined)?.can ?? false;
|
|
50
64
|
},
|
|
@@ -2,6 +2,7 @@ import { useForm, type UseFormOptions } from './form-hooks.svelte';
|
|
|
2
2
|
import { useQueryClient } from '@tanstack/svelte-query';
|
|
3
3
|
import { useParsed } from './useParsed.svelte';
|
|
4
4
|
import { captureAdminContext } from './context.svelte';
|
|
5
|
+
import { queryKeyMatchesTenant } from './provider-bundle';
|
|
5
6
|
import { getAdminOptions } from './options.svelte';
|
|
6
7
|
import { t } from './i18n.svelte';
|
|
7
8
|
import type { BaseRecord, HttpError, Filter, KnownResources, ResourceDefinition } from './types';
|
|
@@ -39,6 +40,7 @@ export function useModalForm<
|
|
|
39
40
|
TError = HttpError,
|
|
40
41
|
>(options: UseModalFormOptions<TVariables, TData, TError> = {} as UseModalFormOptions<TVariables, TData, TError>) {
|
|
41
42
|
const queryClient = useQueryClient();
|
|
43
|
+
const adminContext = captureAdminContext();
|
|
42
44
|
const formState = useForm<TVariables, TData, TError>(options);
|
|
43
45
|
let visible = $state(options.defaultVisible ?? false);
|
|
44
46
|
|
|
@@ -58,7 +60,8 @@ export function useModalForm<
|
|
|
58
60
|
formState.reset();
|
|
59
61
|
if (options.autoSave?.invalidateOnClose) {
|
|
60
62
|
const dpN = options.dataProviderName;
|
|
61
|
-
const dp = (q: { queryKey: readonly unknown[] }) =>
|
|
63
|
+
const dp = (q: { queryKey: readonly unknown[] }) =>
|
|
64
|
+
queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey) && q.queryKey[0] === dpN;
|
|
62
65
|
queryClient.invalidateQueries({ predicate: (q) => dp(q) && q.queryKey[1] === formState.resource && (q.queryKey[2] === 'list' || q.queryKey[2] === 'infiniteList' || q.queryKey[2] === 'select') });
|
|
63
66
|
queryClient.invalidateQueries({ predicate: (q) => dp(q) && q.queryKey[1] === formState.resource && q.queryKey[2] === 'many' });
|
|
64
67
|
queryClient.invalidateQueries({ predicate: (q) => dp(q) && q.queryKey[1] === formState.resource && q.queryKey[2] === 'one' });
|
|
@@ -182,7 +185,33 @@ export function useRelation<TData extends BaseRecord = BaseRecord, TError = Http
|
|
|
182
185
|
|
|
183
186
|
// ─── useNotification ────────────────────────────────────────
|
|
184
187
|
export function useNotification() {
|
|
185
|
-
|
|
188
|
+
const adminContext = captureAdminContext();
|
|
189
|
+
|
|
190
|
+
function sendToast(
|
|
191
|
+
type: 'success' | 'error' | 'warning' | 'info',
|
|
192
|
+
message: string,
|
|
193
|
+
duration?: number,
|
|
194
|
+
options?: { key?: string },
|
|
195
|
+
): void {
|
|
196
|
+
const provider = adminContext.notificationProvider;
|
|
197
|
+
if (provider) {
|
|
198
|
+
provider.open({ type, message, key: options?.key });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
toast[type](message, duration, options);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
open: (message: string, duration?: number, options?: { key?: string }) =>
|
|
206
|
+
sendToast('info', message, duration, options),
|
|
207
|
+
success: (message: string, duration?: number, options?: { key?: string }) =>
|
|
208
|
+
sendToast('success', message, duration, options),
|
|
209
|
+
error: (message: string, duration?: number, options?: { key?: string }) =>
|
|
210
|
+
sendToast('error', message, duration, options),
|
|
211
|
+
warning: (message: string, duration?: number, options?: { key?: string }) =>
|
|
212
|
+
sendToast('warning', message, duration, options),
|
|
213
|
+
close: (key: string) => adminContext.notificationProvider?.close(key),
|
|
214
|
+
};
|
|
186
215
|
}
|
|
187
216
|
|
|
188
217
|
// ─── Field-specific hooks (refine-compatible) ─────────────────────
|