@svadmin/core 0.53.0 → 0.55.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.
@@ -1,7 +1,9 @@
1
1
  import { definedOptions } from './defined-options';
2
+ import { onDestroy } from 'svelte';
2
3
  import { createMutation,useQueryClient } from '@tanstack/svelte-query';
3
4
  import { createSessionQuery } from './session-query.svelte';
4
5
  import { supersededQuerySession } from './query-session.svelte';
6
+ import { captureAuthLiveScope } from './auth-hooks.svelte';
5
7
  import { getAdminOptions } from './options.svelte';
6
8
  import { captureAdminContext } from './context.svelte';
7
9
  import type { AdminContextAccessor } from './context.svelte';
@@ -85,29 +87,74 @@ export function useSubmitTask(options: UseSubmitTaskOptions={}) {
85
87
  const adminOptions=getAdminOptions();
86
88
  const queryClient=useQueryClient();
87
89
  const overtime=createOvertimeTracker(() => mutation.isPending,options.overtimeOptions??adminOptions.overtime);
88
- const submissionScopes = new WeakMap<TaskHandle, ReturnType<AdminContextAccessor['queryKeyMatcher']>>();
90
+ let disposed = false;
91
+ onDestroy(() => { disposed = true; });
92
+ interface SubmissionScope {
93
+ queryScope: ReturnType<AdminContextAccessor['queryKeyMatcher']>;
94
+ auth: ReturnType<typeof captureAuthLiveScope>;
95
+ notification: AdminContextAccessor['notificationProvider'];
96
+ current: () => boolean;
97
+ }
98
+ const submissionScopes = new WeakMap<TaskHandle, SubmissionScope>();
99
+ const failureScopes = new WeakMap<Error, SubmissionScope>();
89
100
 
90
101
  const mutation=createMutation<TaskHandle,Error,UseSubmitTaskMutateParams>(() => ({
91
102
  mutationFn: async (params) => {
92
- const scope = adminContext.queryKeyMatcher();
93
- const provider=normalizeTaskProvider(params.taskProvider,adminContext);
94
- const providerMeta=adminContext.tenant
95
- ? {
96
- ...(params.options?.meta??{}),
97
- ...resolveTenantProviderMeta(adminContext.tenant,adminContext.tenantAdapter),
98
- }
99
- :params.options?.meta;
100
- const taskOptions=providerMeta===params.options?.meta
101
- ? params.options
102
- :definedOptions({
103
- ...params.options,
104
- meta: providerMeta,
105
- });
106
- const handle = await provider.submit(params.taskName,taskOptions);
107
- submissionScopes.set(handle, scope);
108
- return handle;
103
+ const authProvider = adminContext.authProvider;
104
+ const auth = captureAuthLiveScope(authProvider);
105
+ const source = params.taskProvider ?? adminContext.taskProvider;
106
+ const tenant = adminContext.tenantCacheKey?.__svadminTenant;
107
+ const router = adminContext.routerProvider;
108
+ const access = adminContext.accessControlProvider;
109
+ const notification = adminContext.notificationProvider;
110
+ const scope: SubmissionScope = {
111
+ queryScope: adminContext.queryKeyMatcher(),
112
+ auth,
113
+ notification,
114
+ current: () => !disposed && auth.isCurrent() && adminContext.authProvider === authProvider
115
+ && adminContext.tenantCacheKey?.__svadminTenant === tenant
116
+ && adminContext.routerProvider === router && adminContext.accessControlProvider === access
117
+ && adminContext.notificationProvider === notification
118
+ && (params.taskProvider ?? adminContext.taskProvider) === source,
119
+ };
120
+ if (!scope.current()) throw supersededQuerySession();
121
+ try {
122
+ const provider=normalizeTaskProvider(source,adminContext);
123
+ const providerMeta=adminContext.tenant
124
+ ? {
125
+ ...(params.options?.meta??{}),
126
+ ...resolveTenantProviderMeta(adminContext.tenant,adminContext.tenantAdapter),
127
+ }
128
+ :params.options?.meta;
129
+ const taskOptions=providerMeta===params.options?.meta
130
+ ? params.options
131
+ :definedOptions({
132
+ ...params.options,
133
+ meta: providerMeta,
134
+ });
135
+ const handle = await provider.submit(params.taskName,taskOptions);
136
+ submissionScopes.set(handle, scope);
137
+ return handle;
138
+ } catch (error) {
139
+ const failure = error instanceof TaskError ? error : new TaskError('TASK_PROVIDER_FAILED');
140
+ failureScopes.set(failure, scope);
141
+ throw failure;
142
+ }
109
143
  },
110
144
  onSuccess: (data,params,context) => {
145
+ const submission = submissionScopes.get(data);
146
+ if (submission) submissionScopes.delete(data);
147
+ if (!submission) return;
148
+ if (submission.auth.isCurrent()) {
149
+ void queryClient.invalidateQueries({
150
+ predicate: (query) => queryKeyMatches(query.queryKey,{
151
+ ...submission.queryScope,
152
+ kind: 'task',
153
+ action: 'list',
154
+ }),
155
+ });
156
+ }
157
+ if (!submission.current()) return;
111
158
  fireSuccessNotification({
112
159
  config: params.successNotification,
113
160
  defaultMessage: i18n.t('task.submitSuccess'),
@@ -115,34 +162,27 @@ export function useSubmitTask(options: UseSubmitTaskOptions={}) {
115
162
  values: params.options,
116
163
  resource: params.taskName,
117
164
  ...definedOptions({
118
- provider: adminContext.notificationProvider
165
+ provider: submission.notification
119
166
  })
120
167
  });
121
- const scope = submissionScopes.get(data);
122
- if (scope) {
123
- submissionScopes.delete(data);
124
- void queryClient.invalidateQueries({
125
- predicate: (query) => queryKeyMatches(query.queryKey,{
126
- ...scope,
127
- kind: 'task',
128
- action: 'list',
129
- }),
130
- });
131
- }
132
- options.mutationOptions?.onSuccess?.(data,params,context);
168
+ if (submission.current()) options.mutationOptions?.onSuccess?.(data,params,context);
133
169
  },
134
170
  onError: (error,params,context) => {
171
+ const submission = failureScopes.get(error);
172
+ failureScopes.delete(error);
173
+ if (!submission?.current()) return;
135
174
  checkError(error,adminContext);
175
+ if (!submission.current()) return;
136
176
  fireErrorNotification({
137
177
  config: params.errorNotification,
138
178
  defaultMessage: i18n.t('task.submitFailed'),
139
179
  error,
140
180
  resource: params.taskName,
141
181
  ...definedOptions({
142
- provider: adminContext.notificationProvider
182
+ provider: submission.notification
143
183
  })
144
184
  });
145
- options.mutationOptions?.onError?.(error,params,context);
185
+ if (submission.current()) options.mutationOptions?.onError?.(error,params,context);
146
186
  },
147
187
  }));
148
188
 
@@ -235,6 +275,18 @@ export function useTaskList(
235
275
  const activeProvider=options.taskProvider??adminContext.taskProvider;
236
276
  const provider=activeProvider? withValidatedTaskProvider(activeProvider):undefined;
237
277
  const params=options.params===undefined? undefined:taskQueryParams(options.params);
278
+ const callerMeta = params?.['meta'];
279
+ if (callerMeta !== undefined
280
+ && (typeof callerMeta !== 'object' || callerMeta === null || Array.isArray(callerMeta))) {
281
+ throw new TaskError('INVALID_TASK_INPUT');
282
+ }
283
+ const tenantMeta = adminContext.tenant
284
+ ? resolveTenantProviderMeta(adminContext.tenant, adminContext.tenantAdapter)
285
+ : undefined;
286
+ const providerParams = params === undefined && tenantMeta === undefined ? undefined : {
287
+ ...(params ?? {}),
288
+ ...(tenantMeta ? { meta: { ...(callerMeta ?? {}), ...tenantMeta } } : {}),
289
+ };
238
290
  const dlq=options.dlq??false;
239
291
  const queryOptions=options.queryOptions;
240
292
  const resource = dlq ? 'taskDlq' : 'tasks';
@@ -249,16 +301,16 @@ export function useTaskList(
249
301
  decode: decodeTaskList,
250
302
  queryKey: scopedTaskKey(adminContext.queryKeys().task.list(definedOptions({
251
303
  list: dlq? 'dlq':'default',
252
- params,
304
+ params: providerParams,
253
305
  })),provider),
254
306
  queryFn: async () => {
255
307
  if(!provider) throw new Error('TaskProvider not found.');
256
308
  if(dlq) {
257
309
  if(!provider.listDlq) throw new Error('TaskProvider does not implement listDlq');
258
- return provider.listDlq(params);
310
+ return provider.listDlq(providerParams);
259
311
  }
260
312
  if(!provider.list) throw new Error('TaskProvider does not implement list');
261
- return provider.list(params);
313
+ return provider.list(providerParams);
262
314
  },
263
315
  enabled: queryOptions?.enabled??true,
264
316
  ...definedOptions({
@@ -294,19 +346,36 @@ export function useTaskSubscription(
294
346
  const enabled=options.enabled??true;
295
347
  if(!enabled) return;
296
348
 
297
- const provider=normalizeTaskProvider(options.taskProvider,adminContext);
349
+ const authProvider = adminContext.authProvider;
350
+ const auth = captureAuthLiveScope(authProvider);
351
+ if (!auth.available) return;
352
+ const source = options.taskProvider ?? adminContext.taskProvider;
353
+ const tenant = adminContext.tenantCacheKey?.__svadminTenant;
354
+ const router = adminContext.routerProvider;
355
+ const access = adminContext.accessControlProvider;
356
+ const taskId = options.taskId;
357
+ const onTask = options.onTask;
358
+ const onError = options.onError;
359
+ let active = true;
360
+ const current = () => active && (options.enabled ?? true) && options.taskId === taskId
361
+ && (options.taskProvider ?? adminContext.taskProvider) === source
362
+ && adminContext.authProvider === authProvider && auth.isCurrent()
363
+ && adminContext.tenantCacheKey?.__svadminTenant === tenant
364
+ && adminContext.routerProvider === router && adminContext.accessControlProvider === access;
365
+ const provider=normalizeTaskProvider(source,adminContext);
298
366
  if(!provider.subscribe) {
299
367
  throw new Error('TaskProvider does not implement subscribe');
300
368
  }
301
369
 
302
- const subscription=provider.subscribe(options.taskId,options.onTask,options.onError);
303
-
304
- if(typeof subscription==='function') {
305
- return subscription;
306
- }
307
-
308
- if(typeof subscription.unsubscribe==='function') {
309
- return () => subscription.unsubscribe();
310
- }
370
+ const subscription=provider.subscribe(taskId,
371
+ task => { if (current()) onTask(task); },
372
+ error => { if (current()) onError?.(error); },
373
+ );
374
+ return () => {
375
+ // 先失效,再退订;上游退订时同步发出的最后一条事件也不能进入新会话。
376
+ active = false;
377
+ if (typeof subscription === 'function') subscription();
378
+ else subscription.unsubscribe();
379
+ };
311
380
  });
312
381
  }
package/src/types.ts CHANGED
@@ -405,6 +405,22 @@ export interface MenuItem {
405
405
  children?: MenuItem[];
406
406
  }
407
407
 
408
+ export interface DateInputOptions {
409
+ min?: string;
410
+ max?: string;
411
+ step?: number;
412
+ /** 仅用于 daterange;其他日期字段由 field.type 决定输入模式。 */
413
+ rangeMode?: 'date' | 'time' | 'datetime';
414
+ disabledDate?: (date: string) => boolean;
415
+ presets?: { label: string; value: { start: string; end: string } }[];
416
+ /** datetime 字段的输出语义;默认保留本地 datetime-local 字符串。 */
417
+ valueMode?: 'civil' | 'instant';
418
+ /** valueMode=instant 时必填的 IANA 时区。 */
419
+ timeZone?: string;
420
+ /** 仅用于 datetime,重复本地时刻默认拒绝,允许明确选择较早或较晚的一次。 */
421
+ disambiguation?: 'reject' | 'earlier' | 'later';
422
+ }
423
+
408
424
  export interface FieldDefinition {
409
425
  key: string;
410
426
  label: string;
@@ -438,6 +454,8 @@ export interface FieldDefinition {
438
454
  currency?: string;
439
455
  locale?: string;
440
456
  scale?: '100' | '1';
457
+ // 日期输入策略与数值上下限分离,不隐式转换时区。
458
+ dateInput?: DateInputOptions;
441
459
  // File upload contract
442
460
  accept?: string;
443
461
  maxSize?: number;