@svadmin/core 0.22.0 → 0.23.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 +5 -0
- package/src/auth-hooks.svelte.ts +44 -34
- package/src/chatProvider.svelte.ts +8 -21
- package/src/context.svelte.ts +22 -2
- package/src/data-transfer.svelte.ts +66 -34
- package/src/form-hooks.svelte.ts +110 -42
- package/src/helpers-pure.ts +159 -0
- package/src/helpers.test.ts +177 -0
- package/src/helpers.ts +9 -118
- package/src/hook-utils.svelte.ts +27 -6
- package/src/hooks.svelte.ts +120 -18
- package/src/i18n.svelte.ts +41 -17
- package/src/index.ts +10 -3
- package/src/live-websocket.ts +20 -2
- package/src/live.svelte.ts +35 -19
- package/src/mutation-hooks.svelte.ts +162 -73
- package/src/notification.svelte.ts +9 -5
- package/src/options.svelte.ts +42 -19
- package/src/permissions.ts +4 -0
- package/src/query-hooks.svelte.ts +182 -100
- package/src/router-provider.ts +3 -1
- package/src/router.ts +23 -2
- package/src/routing-hooks.svelte.ts +42 -17
- package/src/table-hooks.svelte.ts +87 -34
- package/src/table-state.svelte.ts +2 -1
- package/src/theme.svelte.ts +27 -25
- package/src/toast.svelte.ts +22 -11
- package/src/types.ts +3 -1
- package/src/url-sync.ts +28 -16
- package/src/useCan.ts +6 -2
- package/src/useParsed.svelte.ts +4 -0
- package/src/useStepsForm.svelte.ts +31 -21
- package/src/utility-hooks.svelte.ts +14 -4
package/src/helpers.ts
CHANGED
|
@@ -4,122 +4,13 @@ import type { Filter, Sort, CrudOperator } from './types';
|
|
|
4
4
|
import { t } from './i18n.svelte';
|
|
5
5
|
import { getTextTransformers } from './options.svelte';
|
|
6
6
|
|
|
7
|
-
// ───
|
|
7
|
+
// ─── Re-export pure utilities (testable without Svelte runtime) ───
|
|
8
|
+
export {
|
|
9
|
+
getDefaultFilter, getDefaultSortOrder,
|
|
10
|
+
unionFilters, unionSorters,
|
|
11
|
+
file2Base64, parseCSV,
|
|
12
|
+
} from './helpers-pure';
|
|
8
13
|
|
|
9
|
-
/**
|
|
10
|
-
* Get the current filter value for a field from a filter array.
|
|
11
|
-
* Useful for setting default values in filter inputs.
|
|
12
|
-
*/
|
|
13
|
-
export function getDefaultFilter(
|
|
14
|
-
columnName: string,
|
|
15
|
-
filters?: Filter[],
|
|
16
|
-
operatorType: CrudOperator = 'eq',
|
|
17
|
-
): unknown | undefined {
|
|
18
|
-
if (!filters) return undefined;
|
|
19
|
-
const filter = filters.find((f) => {
|
|
20
|
-
if ('field' in f) {
|
|
21
|
-
return f.field === columnName && f.operator === operatorType;
|
|
22
|
-
}
|
|
23
|
-
return false;
|
|
24
|
-
});
|
|
25
|
-
return filter && 'value' in filter ? filter.value : undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Get the current sort order for a field from a sorter array.
|
|
30
|
-
* Returns 'asc', 'desc', or undefined.
|
|
31
|
-
*/
|
|
32
|
-
export function getDefaultSortOrder(
|
|
33
|
-
columnName: string,
|
|
34
|
-
sorters?: Sort[],
|
|
35
|
-
): 'asc' | 'desc' | undefined {
|
|
36
|
-
if (!sorters) return undefined;
|
|
37
|
-
const sortItem = sorters.find((item) => item.field === columnName);
|
|
38
|
-
return sortItem?.order as 'asc' | 'desc' | undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Merge two filter arrays, deduplicating by field+operator.
|
|
43
|
-
* Filters in `newFilters` take priority over `prevFilters`.
|
|
44
|
-
* `permanentFilter` items are always preserved.
|
|
45
|
-
* Filters with undefined values are removed from the result.
|
|
46
|
-
*/
|
|
47
|
-
export function unionFilters(
|
|
48
|
-
permanentFilter: Filter[],
|
|
49
|
-
newFilters: Filter[],
|
|
50
|
-
prevFilters: Filter[] = [],
|
|
51
|
-
): Filter[] {
|
|
52
|
-
const compareFilters = (a: Filter, b: Filter): boolean => {
|
|
53
|
-
if ('field' in a && 'field' in b) {
|
|
54
|
-
return a.field === b.field && a.operator === b.operator;
|
|
55
|
-
}
|
|
56
|
-
return false;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// Start with previous filters
|
|
60
|
-
let result = [...prevFilters];
|
|
61
|
-
|
|
62
|
-
// Apply new filters (override previous by field+operator)
|
|
63
|
-
for (const newFilter of newFilters) {
|
|
64
|
-
const idx = result.findIndex((f) => compareFilters(f, newFilter));
|
|
65
|
-
if (idx >= 0) {
|
|
66
|
-
result[idx] = newFilter;
|
|
67
|
-
} else {
|
|
68
|
-
result.push(newFilter);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Ensure permanent filters are always present
|
|
73
|
-
for (const perm of permanentFilter) {
|
|
74
|
-
const idx = result.findIndex((f) => compareFilters(f, perm));
|
|
75
|
-
if (idx >= 0) {
|
|
76
|
-
result[idx] = perm;
|
|
77
|
-
} else {
|
|
78
|
-
result.push(perm);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Remove filters with undefined values
|
|
83
|
-
return result.filter((f) => !('value' in f && f.value === undefined));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Merge two sorter arrays, deduplicating by field.
|
|
88
|
-
* Sorters in `newSorters` take priority.
|
|
89
|
-
*/
|
|
90
|
-
export function unionSorters(
|
|
91
|
-
permanentSorters: Sort[],
|
|
92
|
-
newSorters: Sort[],
|
|
93
|
-
): Sort[] {
|
|
94
|
-
const result = [...newSorters];
|
|
95
|
-
|
|
96
|
-
for (const perm of permanentSorters) {
|
|
97
|
-
if (!result.some((s) => s.field === perm.field)) {
|
|
98
|
-
result.push(perm);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return result;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ─── Upload Helpers ───────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Convert a File (or file-like object) to a base64 data URL string.
|
|
109
|
-
* Useful for image uploads and previews.
|
|
110
|
-
*/
|
|
111
|
-
export function file2Base64(file: File | { uid?: string } & Blob): Promise<string> {
|
|
112
|
-
return new Promise((resolve, reject) => {
|
|
113
|
-
const reader = new FileReader();
|
|
114
|
-
reader.readAsDataURL(file as Blob);
|
|
115
|
-
reader.addEventListener('load', () => {
|
|
116
|
-
resolve(reader.result as string);
|
|
117
|
-
});
|
|
118
|
-
reader.addEventListener('error', () => {
|
|
119
|
-
reject(reader.error);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
14
|
|
|
124
15
|
// ─── Document Title Helper ────────────────────────────────────
|
|
125
16
|
|
|
@@ -219,7 +110,7 @@ export function deriveValidator(
|
|
|
219
110
|
case 'email': {
|
|
220
111
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
221
112
|
if (typeof value === 'string' && !emailRegex.test(value)) {
|
|
222
|
-
errors[field.key] = t('validation.
|
|
113
|
+
errors[field.key] = t('validation.invalidEmail');
|
|
223
114
|
}
|
|
224
115
|
break;
|
|
225
116
|
}
|
|
@@ -227,13 +118,13 @@ export function deriveValidator(
|
|
|
227
118
|
try {
|
|
228
119
|
new URL(value as string);
|
|
229
120
|
} catch {
|
|
230
|
-
errors[field.key] = t('validation.
|
|
121
|
+
errors[field.key] = t('validation.invalidUrl');
|
|
231
122
|
}
|
|
232
123
|
break;
|
|
233
124
|
}
|
|
234
125
|
case 'number': {
|
|
235
126
|
if (typeof value === 'string' && isNaN(Number(value))) {
|
|
236
|
-
errors[field.key] = t('validation.
|
|
127
|
+
errors[field.key] = t('validation.invalidNumber');
|
|
237
128
|
}
|
|
238
129
|
break;
|
|
239
130
|
}
|
package/src/hook-utils.svelte.ts
CHANGED
|
@@ -3,7 +3,27 @@
|
|
|
3
3
|
|
|
4
4
|
import { useQueryClient } from '@tanstack/svelte-query';
|
|
5
5
|
import type { LiveProvider, LiveEvent, LiveMode } from './live.svelte';
|
|
6
|
-
import {
|
|
6
|
+
import { getAuthProvider } from './context.svelte';
|
|
7
|
+
import { notify } from './notification.svelte';
|
|
8
|
+
|
|
9
|
+
// ─── Auth Error Delegate ────────────────────────────────────────
|
|
10
|
+
// Delegate auth errors (401/403) to authProvider.onError() — refine pattern
|
|
11
|
+
|
|
12
|
+
export async function checkError(error: unknown): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
const authProvider = getAuthProvider({ optional: true });
|
|
15
|
+
if (!authProvider?.onError) return;
|
|
16
|
+
const result = await authProvider.onError(error);
|
|
17
|
+
if (result.logout) {
|
|
18
|
+
await authProvider.logout?.();
|
|
19
|
+
const { navigate } = await import('./router');
|
|
20
|
+
navigate(result.redirectTo ?? '/login');
|
|
21
|
+
} else if (result.redirectTo) {
|
|
22
|
+
const { navigate } = await import('./router');
|
|
23
|
+
navigate(result.redirectTo);
|
|
24
|
+
}
|
|
25
|
+
} catch { /* auth check failed silently */ }
|
|
26
|
+
}
|
|
7
27
|
|
|
8
28
|
// ─── Overtime Tracker ───────────────────────────────────────────
|
|
9
29
|
// Tracks elapsed time during loading, auto-cleans via $effect
|
|
@@ -71,7 +91,7 @@ export function createLiveSubscription(paramsFn: () => LiveSubscriptionParams):
|
|
|
71
91
|
callback: (event: LiveEvent) => {
|
|
72
92
|
params.onLiveEvent?.(event);
|
|
73
93
|
if (liveMode === 'auto') {
|
|
74
|
-
queryClient.invalidateQueries({
|
|
94
|
+
queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === params.resource });
|
|
75
95
|
}
|
|
76
96
|
},
|
|
77
97
|
});
|
|
@@ -98,10 +118,10 @@ export function fireSuccessNotification(
|
|
|
98
118
|
if (!config && !defaultMessage) return;
|
|
99
119
|
if (typeof config === 'function') {
|
|
100
120
|
const result = config(data, values, resource);
|
|
101
|
-
|
|
121
|
+
notify({ type: 'success', message: result.message, description: result.description });
|
|
102
122
|
return;
|
|
103
123
|
}
|
|
104
|
-
|
|
124
|
+
notify({ type: 'success', message: config || defaultMessage });
|
|
105
125
|
}
|
|
106
126
|
|
|
107
127
|
export function fireErrorNotification(
|
|
@@ -110,11 +130,12 @@ export function fireErrorNotification(
|
|
|
110
130
|
error?: unknown,
|
|
111
131
|
): void {
|
|
112
132
|
if (config === false) return;
|
|
133
|
+
if (!config && !defaultMessage) return;
|
|
113
134
|
if (typeof config === 'function') {
|
|
114
135
|
const result = config(error);
|
|
115
|
-
|
|
136
|
+
notify({ type: 'error', message: result.message, description: result.description });
|
|
116
137
|
return;
|
|
117
138
|
}
|
|
118
139
|
const errMsg = error instanceof Error ? error.message : String(error ?? '');
|
|
119
|
-
|
|
140
|
+
notify({ type: 'error', message: config || `${defaultMessage}${errMsg ? ': ' + errMsg : ''}` });
|
|
120
141
|
}
|
package/src/hooks.svelte.ts
CHANGED
|
@@ -23,10 +23,11 @@ export type { OvertimeResult, OvertimeOptions, NotificationConfig } from './hook
|
|
|
23
23
|
|
|
24
24
|
import { createQuery, createInfiniteQuery, createMutation, useQueryClient } from '@tanstack/svelte-query';
|
|
25
25
|
import { getAdminOptions } from './options.svelte';
|
|
26
|
-
import { getDataProviderForResource, getDataProvider } from './context.svelte';
|
|
26
|
+
import { getDataProviderForResource, getDataProvider, getLiveProvider } from './context.svelte';
|
|
27
27
|
import { useParsed } from './useParsed.svelte';
|
|
28
|
-
import { createOvertimeTracker, fireSuccessNotification, fireErrorNotification } from './hook-utils.svelte';
|
|
29
|
-
import
|
|
28
|
+
import { createOvertimeTracker, createLiveSubscription, fireSuccessNotification, fireErrorNotification, checkError } from './hook-utils.svelte';
|
|
29
|
+
import { invalidateByScopes } from './mutation-hooks.svelte';
|
|
30
|
+
import type { NotificationConfig, OvertimeOptions, LiveSubscriptionParams } from './hook-utils.svelte';
|
|
30
31
|
import type { BaseRecord, HttpError, Pagination, Sort, Filter, DataProvider, KnownResources } from './types';
|
|
31
32
|
import type { LiveMode, LiveEvent } from './live.svelte';
|
|
32
33
|
import { useList } from './query-hooks.svelte';
|
|
@@ -52,13 +53,13 @@ export interface UseInfiniteListOptions<TData extends BaseRecord = BaseRecord, T
|
|
|
52
53
|
|
|
53
54
|
export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseInfiniteListOptions<TData, TError> = {}) {
|
|
54
55
|
const parsed = useParsed();
|
|
55
|
-
const resource = options.resource ?? parsed.resource ?? '';
|
|
56
56
|
const adminOptions = getAdminOptions();
|
|
57
57
|
|
|
58
58
|
const query = createInfiniteQuery<{ data: TData[]; total: number }, TError>(() => {
|
|
59
|
+
const resource = options.resource ?? parsed.resource ?? '';
|
|
59
60
|
const provider = getDataProviderForResource(resource, options.dataProviderName);
|
|
60
61
|
return {
|
|
61
|
-
queryKey: [resource, 'infiniteList', options.sorters, options.filters, options.meta],
|
|
62
|
+
queryKey: [options.dataProviderName, resource, 'infiniteList', options.pagination?.pageSize, options.sorters, options.filters, options.meta],
|
|
62
63
|
queryFn: async ({ pageParam = 1 }) => {
|
|
63
64
|
const result = await provider.getList<TData>({
|
|
64
65
|
resource,
|
|
@@ -82,6 +83,30 @@ export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError =
|
|
|
82
83
|
|
|
83
84
|
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
84
85
|
|
|
86
|
+
createLiveSubscription((): LiveSubscriptionParams => ({
|
|
87
|
+
resource: options.resource ?? parsed.resource ?? '',
|
|
88
|
+
liveProvider: getLiveProvider(),
|
|
89
|
+
liveMode: options.liveMode ?? adminOptions.liveMode,
|
|
90
|
+
onLiveEvent: (e: LiveEvent) => {
|
|
91
|
+
options.onLiveEvent?.(e);
|
|
92
|
+
adminOptions.onLiveEvent?.(e);
|
|
93
|
+
},
|
|
94
|
+
enabled: options.queryOptions?.enabled ?? true,
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
let lastSuccessAt = 0;
|
|
98
|
+
let lastErrorAt = 0;
|
|
99
|
+
$effect(() => {
|
|
100
|
+
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
|
|
101
|
+
lastSuccessAt = query.dataUpdatedAt;
|
|
102
|
+
fireSuccessNotification(options.successNotification, '', query.data, undefined, options.resource ?? parsed.resource ?? '');
|
|
103
|
+
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
104
|
+
lastErrorAt = query.errorUpdatedAt;
|
|
105
|
+
checkError(query.error);
|
|
106
|
+
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
85
110
|
return { query, get overtime() { return overtime; } };
|
|
86
111
|
}
|
|
87
112
|
|
|
@@ -126,7 +151,7 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
|
|
|
126
151
|
const query = createQuery<{ data: TData[]; total: number }>(() => {
|
|
127
152
|
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
128
153
|
return {
|
|
129
|
-
queryKey: [resource, 'select', allFilters, sorters, pagination, meta],
|
|
154
|
+
queryKey: [dataProviderName, resource, 'select', allFilters, sorters, pagination, meta],
|
|
130
155
|
queryFn: () => provider.getList<TData>({ resource, sorters, filters: allFilters, pagination: { current: 1, pageSize: effectivePageSize }, meta }),
|
|
131
156
|
enabled: options.queryOptions?.enabled ?? true,
|
|
132
157
|
staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
@@ -139,7 +164,7 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
|
|
|
139
164
|
? createQuery<{ data: TData[] }>(() => {
|
|
140
165
|
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
141
166
|
return {
|
|
142
|
-
queryKey: [resource, 'select-defaults', defaultValueIds],
|
|
167
|
+
queryKey: [dataProviderName, resource, 'select-defaults', defaultValueIds],
|
|
143
168
|
queryFn: async () => {
|
|
144
169
|
if (provider.getMany) return provider.getMany<TData>({ resource, ids: defaultValueIds, meta });
|
|
145
170
|
const results = await Promise.all(defaultValueIds.map(id => provider.getOne<TData>({ resource, id, meta })));
|
|
@@ -172,6 +197,19 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
|
|
|
172
197
|
|
|
173
198
|
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
174
199
|
|
|
200
|
+
let lastSuccessAt = 0;
|
|
201
|
+
let lastErrorAt = 0;
|
|
202
|
+
$effect(() => {
|
|
203
|
+
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
|
|
204
|
+
lastSuccessAt = query.dataUpdatedAt;
|
|
205
|
+
fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
|
|
206
|
+
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
207
|
+
lastErrorAt = query.errorUpdatedAt;
|
|
208
|
+
checkError(query.error);
|
|
209
|
+
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
175
213
|
function onSearchChange(value: string) {
|
|
176
214
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
177
215
|
debounceTimer = setTimeout(() => { searchText = value; }, debounceMs);
|
|
@@ -210,7 +248,7 @@ export function useCustom<TData = unknown, TError = HttpError>(options: UseCusto
|
|
|
210
248
|
const query = createQuery<{ data: TData }, TError>(() => {
|
|
211
249
|
const provider = getDataProvider(options.dataProviderName);
|
|
212
250
|
return {
|
|
213
|
-
queryKey: ['custom', options.url, options.method, options.config, options.meta],
|
|
251
|
+
queryKey: [options.dataProviderName, 'custom', options.url, options.method, options.config, options.meta],
|
|
214
252
|
queryFn: async () => {
|
|
215
253
|
if (!provider.custom) throw new Error('DataProvider does not support custom method');
|
|
216
254
|
return provider.custom<TData>({
|
|
@@ -231,6 +269,19 @@ export function useCustom<TData = unknown, TError = HttpError>(options: UseCusto
|
|
|
231
269
|
|
|
232
270
|
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
233
271
|
|
|
272
|
+
let lastSuccessAt = 0;
|
|
273
|
+
let lastErrorAt = 0;
|
|
274
|
+
$effect(() => {
|
|
275
|
+
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
|
|
276
|
+
lastSuccessAt = query.dataUpdatedAt;
|
|
277
|
+
fireSuccessNotification(options.successNotification, '', query.data);
|
|
278
|
+
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
279
|
+
lastErrorAt = query.errorUpdatedAt;
|
|
280
|
+
checkError(query.error);
|
|
281
|
+
fireErrorNotification(options.errorNotification, 'Custom request failed', query.error);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
234
285
|
return { query, get overtime() { return overtime; } };
|
|
235
286
|
}
|
|
236
287
|
|
|
@@ -239,11 +290,19 @@ export function useCustom<TData = unknown, TError = HttpError>(options: UseCusto
|
|
|
239
290
|
export function useCustomMutation<TData = unknown, TError = HttpError, TVariables = unknown>(dataProviderName?: string) {
|
|
240
291
|
const queryClient = useQueryClient();
|
|
241
292
|
|
|
242
|
-
const mutation = createMutation<{ data: TData }, TError, { url: string; method: 'get' | 'post' | 'put' | 'patch' | 'delete'; values?: TVariables; meta?: Record<string, unknown
|
|
293
|
+
const mutation = createMutation<{ data: TData }, TError, { url: string; method: 'get' | 'post' | 'put' | 'patch' | 'delete'; values?: TVariables; query?: Record<string, unknown>; headers?: Record<string, string>; sorters?: Sort[]; filters?: Filter[]; meta?: Record<string, unknown>; invalidates?: string[] | false; resource?: string }>(() => ({
|
|
243
294
|
mutationFn: async (params) => {
|
|
244
295
|
const provider = getDataProvider(dataProviderName);
|
|
245
296
|
if (!provider.custom) throw new Error('DataProvider does not support custom method');
|
|
246
|
-
return provider.custom<TData>({ url: params.url, method: params.method, payload: params.values, meta: params.meta });
|
|
297
|
+
return provider.custom<TData>({ url: params.url, method: params.method, payload: params.values, query: params.query, headers: params.headers, sorters: params.sorters, filters: params.filters, meta: params.meta });
|
|
298
|
+
},
|
|
299
|
+
onSuccess: (data, params) => {
|
|
300
|
+
if (params.resource && params.invalidates !== false) {
|
|
301
|
+
invalidateByScopes(queryClient, params.resource, params.invalidates, ['list', 'many']);
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
onError: (error) => {
|
|
305
|
+
checkError(error);
|
|
247
306
|
},
|
|
248
307
|
}));
|
|
249
308
|
|
|
@@ -271,7 +330,19 @@ export function useCreateMany<TData extends BaseRecord = BaseRecord, TError = Ht
|
|
|
271
330
|
return { data: results.map(r => r.data) };
|
|
272
331
|
} finally { isMutating = false; }
|
|
273
332
|
},
|
|
274
|
-
onSuccess: (
|
|
333
|
+
onSuccess: (data, params) => {
|
|
334
|
+
const resName = params.resource ?? resource;
|
|
335
|
+
fireSuccessNotification(undefined, 'Created successfully', data.data, params.variables, resName);
|
|
336
|
+
audit({ action: 'create', resource: resName });
|
|
337
|
+
},
|
|
338
|
+
onError: (error, params) => {
|
|
339
|
+
checkError(error);
|
|
340
|
+
fireErrorNotification(undefined, 'Create many failed', error);
|
|
341
|
+
},
|
|
342
|
+
onSettled: (_d, _e, params) => {
|
|
343
|
+
const resName = params.resource ?? resource;
|
|
344
|
+
invalidateByScopes(queryClient, resName, undefined, ['list', 'many']);
|
|
345
|
+
},
|
|
275
346
|
}));
|
|
276
347
|
|
|
277
348
|
return { mutation, get overtime() { return overtime; } };
|
|
@@ -296,7 +367,22 @@ export function useUpdateMany<TData extends BaseRecord = BaseRecord, TError = Ht
|
|
|
296
367
|
return { data: results.map(r => r.data) };
|
|
297
368
|
} finally { isMutating = false; }
|
|
298
369
|
},
|
|
299
|
-
onSuccess: (
|
|
370
|
+
onSuccess: (data, params) => {
|
|
371
|
+
const resName = params.resource ?? resource;
|
|
372
|
+
fireSuccessNotification(undefined, 'Updated successfully', data.data, params.variables, resName);
|
|
373
|
+
audit({ action: 'update', resource: resName });
|
|
374
|
+
},
|
|
375
|
+
onError: (error, params) => {
|
|
376
|
+
checkError(error);
|
|
377
|
+
fireErrorNotification(undefined, 'Update many failed', error);
|
|
378
|
+
},
|
|
379
|
+
onSettled: (_d, _e, params) => {
|
|
380
|
+
const resName = params.resource ?? resource;
|
|
381
|
+
invalidateByScopes(queryClient, resName, undefined, ['list', 'many', 'detail'], undefined);
|
|
382
|
+
for (const id of params.ids) {
|
|
383
|
+
queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === id });
|
|
384
|
+
}
|
|
385
|
+
},
|
|
300
386
|
}));
|
|
301
387
|
|
|
302
388
|
return { mutation, get overtime() { return overtime; } };
|
|
@@ -321,7 +407,22 @@ export function useDeleteMany<TData extends BaseRecord = BaseRecord, TError = Ht
|
|
|
321
407
|
return { data: results.map(r => r.data) };
|
|
322
408
|
} finally { isMutating = false; }
|
|
323
409
|
},
|
|
324
|
-
onSuccess: (
|
|
410
|
+
onSuccess: (data, params) => {
|
|
411
|
+
const resName = params.resource ?? resource;
|
|
412
|
+
fireSuccessNotification(undefined, 'Deleted successfully', data.data, undefined, resName);
|
|
413
|
+
audit({ action: 'delete', resource: resName });
|
|
414
|
+
},
|
|
415
|
+
onError: (error, params) => {
|
|
416
|
+
checkError(error);
|
|
417
|
+
fireErrorNotification(undefined, 'Delete many failed', error);
|
|
418
|
+
},
|
|
419
|
+
onSettled: (_d, _e, params) => {
|
|
420
|
+
const resName = params.resource ?? resource;
|
|
421
|
+
invalidateByScopes(queryClient, resName, undefined, ['list', 'many']);
|
|
422
|
+
for (const id of params.ids) {
|
|
423
|
+
queryClient.removeQueries({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === id });
|
|
424
|
+
}
|
|
425
|
+
},
|
|
325
426
|
}));
|
|
326
427
|
|
|
327
428
|
return { mutation, get overtime() { return overtime; } };
|
|
@@ -338,12 +439,13 @@ export function useInvalidate() {
|
|
|
338
439
|
return;
|
|
339
440
|
}
|
|
340
441
|
const scopes = params.invalidates || ['resourceAll'];
|
|
442
|
+
const res = params.resource;
|
|
341
443
|
for (const scope of scopes) {
|
|
342
|
-
if (scope === 'resourceAll') queryClient.invalidateQueries({
|
|
343
|
-
else if ((scope === 'detail' || scope === 'one') && params.id) queryClient.invalidateQueries({
|
|
344
|
-
else if (scope === 'one') queryClient.invalidateQueries({
|
|
345
|
-
else if (scope === 'list') queryClient.invalidateQueries({
|
|
346
|
-
else if (scope === 'many') queryClient.invalidateQueries({
|
|
444
|
+
if (scope === 'resourceAll') queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === res });
|
|
445
|
+
else if ((scope === 'detail' || scope === 'one') && params.id) queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === res && q.queryKey[2] === 'one' && q.queryKey[3] === params.id });
|
|
446
|
+
else if (scope === 'one') queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === res && q.queryKey[2] === 'one' });
|
|
447
|
+
else if (scope === 'list') queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === res && (q.queryKey[2] === 'list' || q.queryKey[2] === 'infiniteList' || q.queryKey[2] === 'select') });
|
|
448
|
+
else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === res && q.queryKey[2] === 'many' });
|
|
347
449
|
}
|
|
348
450
|
};
|
|
349
451
|
}
|
package/src/i18n.svelte.ts
CHANGED
|
@@ -2,14 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
let locales = $state<Record<string, Record<string, string>>>({
|
|
6
6
|
'zh-CN': {
|
|
7
|
+
'common.list': '列表',
|
|
8
|
+
'common.prev': '上一页',
|
|
9
|
+
'common.pageOf': '第 {page} 页,共 {total} 页',
|
|
10
|
+
'common.areYouSure': '确认操作?',
|
|
11
|
+
'common.backToList': '返回列表',
|
|
12
|
+
'common.refresh': '刷新',
|
|
13
|
+
'common.importData': '导入数据',
|
|
14
|
+
'common.upload': '上传',
|
|
15
|
+
'common.returnHome': '返回首页',
|
|
7
16
|
// Common
|
|
8
17
|
'common.save': '保存',
|
|
9
18
|
'common.cancel': '取消',
|
|
10
19
|
'common.create': '新建',
|
|
11
20
|
'common.edit': '编辑',
|
|
12
21
|
'common.delete': '删除',
|
|
22
|
+
'common.batchDeletePartialFail': '批量删除部分失败:{failed}/{total} 条未成功',
|
|
23
|
+
'common.batchDeleteSuccess': '成功删除 {count} 条记录',
|
|
13
24
|
'common.search': '搜索...',
|
|
14
25
|
'common.confirm': '确定',
|
|
15
26
|
'common.loading': '加载中...',
|
|
@@ -142,6 +153,8 @@ const locales: Record<string, Record<string, string>> = {
|
|
|
142
153
|
'validation.minLength': '最少 {min} 个字符',
|
|
143
154
|
'validation.maxLength': '最多 {max} 个字符',
|
|
144
155
|
'validation.invalidEmail': '请输入有效的邮箱地址',
|
|
156
|
+
'validation.invalidUrl': '请输入有效的 URL 地址',
|
|
157
|
+
'validation.invalidNumber': '请输入有效的数字',
|
|
145
158
|
'validation.invalidFormat': '格式不正确',
|
|
146
159
|
// Settings
|
|
147
160
|
'settings.title': '设置',
|
|
@@ -242,12 +255,23 @@ const locales: Record<string, Record<string, string>> = {
|
|
|
242
255
|
'profile.updatePassword': '更新密码',
|
|
243
256
|
},
|
|
244
257
|
'en': {
|
|
258
|
+
'common.list': 'List',
|
|
259
|
+
'common.prev': 'Prev',
|
|
260
|
+
'common.pageOf': 'Page {page} of {total}',
|
|
261
|
+
'common.areYouSure': 'Are you sure?',
|
|
262
|
+
'common.backToList': 'Back to List',
|
|
263
|
+
'common.refresh': 'Refresh',
|
|
264
|
+
'common.importData': 'Import data',
|
|
265
|
+
'common.upload': 'Upload',
|
|
266
|
+
'common.returnHome': 'Return to Home',
|
|
245
267
|
// Common
|
|
246
268
|
'common.save': 'Save',
|
|
247
269
|
'common.cancel': 'Cancel',
|
|
248
270
|
'common.create': 'Create',
|
|
249
271
|
'common.edit': 'Edit',
|
|
250
272
|
'common.delete': 'Delete',
|
|
273
|
+
'common.batchDeletePartialFail': 'Batch delete partially failed: {failed}/{total} unsuccessful',
|
|
274
|
+
'common.batchDeleteSuccess': 'Successfully deleted {count} records',
|
|
251
275
|
'common.search': 'Search...',
|
|
252
276
|
'common.confirm': 'Confirm',
|
|
253
277
|
'common.loading': 'Loading...',
|
|
@@ -380,6 +404,8 @@ const locales: Record<string, Record<string, string>> = {
|
|
|
380
404
|
'validation.minLength': 'Minimum {min} characters',
|
|
381
405
|
'validation.maxLength': 'Maximum {max} characters',
|
|
382
406
|
'validation.invalidEmail': 'Please enter a valid email address',
|
|
407
|
+
'validation.invalidUrl': 'Please enter a valid URL',
|
|
408
|
+
'validation.invalidNumber': 'Please enter a valid number',
|
|
383
409
|
'validation.invalidFormat': 'Invalid format',
|
|
384
410
|
// Settings
|
|
385
411
|
'settings.title': 'Settings',
|
|
@@ -479,19 +505,18 @@ const locales: Record<string, Record<string, string>> = {
|
|
|
479
505
|
'profile.passwordChanged': 'Password updated successfully',
|
|
480
506
|
'profile.updatePassword': 'Update Password',
|
|
481
507
|
},
|
|
482
|
-
};
|
|
508
|
+
});
|
|
483
509
|
|
|
484
510
|
/** Detect best locale from browser language */
|
|
485
511
|
function detectLocale(): string {
|
|
486
|
-
if (typeof navigator === 'undefined') return 'en';
|
|
512
|
+
if (typeof navigator === 'undefined') return 'en';
|
|
487
513
|
const browserLang = navigator.language || navigator.languages?.[0] || 'zh-CN';
|
|
488
|
-
// Exact match
|
|
489
514
|
if (locales[browserLang]) return browserLang;
|
|
490
|
-
// Prefix match (e.g., 'zh' → 'zh-CN', 'en-US' → 'en')
|
|
491
515
|
const prefix = browserLang.split('-')[0];
|
|
492
516
|
for (const key of Object.keys(locales)) {
|
|
493
|
-
if (key.startsWith(prefix)) return key;
|
|
517
|
+
if (key === browserLang || key.startsWith(prefix + '-')) return key;
|
|
494
518
|
}
|
|
519
|
+
if (locales[prefix]) return prefix;
|
|
495
520
|
return 'en';
|
|
496
521
|
}
|
|
497
522
|
|
|
@@ -510,8 +535,8 @@ export function getAvailableLocales(): string[] {
|
|
|
510
535
|
}
|
|
511
536
|
|
|
512
537
|
export function t(key: string, params?: Record<string, string | number>): string {
|
|
513
|
-
const locale = locales[currentLocale]
|
|
514
|
-
let text = locale[key] ?? key;
|
|
538
|
+
const locale = locales[currentLocale];
|
|
539
|
+
let text = locale?.[key] ?? locales['en']?.[key] ?? key;
|
|
515
540
|
|
|
516
541
|
if (params) {
|
|
517
542
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -522,7 +547,11 @@ export function t(key: string, params?: Record<string, string | number>): string
|
|
|
522
547
|
}
|
|
523
548
|
|
|
524
549
|
export function addTranslations(locale: string, translations: Record<string, string>): void {
|
|
525
|
-
locales[locale]
|
|
550
|
+
locales = { ...locales, [locale]: { ...locales[locale], ...translations } };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function resetI18n(): void {
|
|
554
|
+
currentLocale = detectLocale();
|
|
526
555
|
}
|
|
527
556
|
|
|
528
557
|
/**
|
|
@@ -530,14 +559,9 @@ export function addTranslations(locale: string, translations: Record<string, str
|
|
|
530
559
|
* Tracks locale changes natively.
|
|
531
560
|
*/
|
|
532
561
|
export function useTranslation() {
|
|
533
|
-
const translate =
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
return (key: string, params?: Record<string, string | number>): string => {
|
|
537
|
-
// Intentionally passing the key to the main t() function which uses currentLocale
|
|
538
|
-
return t(key, params);
|
|
539
|
-
};
|
|
540
|
-
});
|
|
562
|
+
const translate = (key: string, params?: Record<string, string | number>): string => {
|
|
563
|
+
return t(key, params);
|
|
564
|
+
};
|
|
541
565
|
|
|
542
566
|
return {
|
|
543
567
|
get t() { return translate; },
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export {
|
|
|
6
6
|
setResources, getResources, getResource,
|
|
7
7
|
setRouterProvider, getRouterProvider,
|
|
8
8
|
setLiveProvider, getLiveProvider,
|
|
9
|
+
resetContext,
|
|
9
10
|
} from './context.svelte';
|
|
10
11
|
export type { DataProviderInput } from './context.svelte';
|
|
11
12
|
export {
|
|
@@ -26,7 +27,8 @@ export {
|
|
|
26
27
|
useNotification, useDataProvider,
|
|
27
28
|
useMenu, useBreadcrumb, useThemedLayoutContext,
|
|
28
29
|
} from './hooks.svelte';
|
|
29
|
-
export { matchRoute, navigate, currentPath, setActiveRouterProvider } from './router';
|
|
30
|
+
export { matchRoute, navigate, currentPath, setActiveRouterProvider, beforeEach, afterEach } from './router';
|
|
31
|
+
export type { RouteGuard } from './router';
|
|
30
32
|
export { readURLState, writeURLState } from './url-sync';
|
|
31
33
|
export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, canAccessAsync } from './permissions';
|
|
32
34
|
export { useLive, useSubscription, usePublish } from './live.svelte';
|
|
@@ -42,12 +44,14 @@ export type { ThemeMode, ColorTheme, ThemeStrategy, ThemeConfig, ColorPreset } f
|
|
|
42
44
|
export { setUnsavedChanges, getUnsavedChanges, initUnsavedChangesNotifier } from './unsaved-changes.svelte';
|
|
43
45
|
export { setAdminOptions, getAdminOptions, getTextTransformers } from './options.svelte';
|
|
44
46
|
export type { AdminOptions, TextTransformers, OvertimeConfig } from './options.svelte';
|
|
47
|
+
export { checkError } from './hook-utils.svelte';
|
|
48
|
+
export type { NotificationConfig, OvertimeOptions, OvertimeResult, LiveSubscriptionParams } from './hook-utils.svelte';
|
|
45
49
|
|
|
46
50
|
export * from './query-keys';
|
|
47
|
-
export { HttpError } from './types';
|
|
51
|
+
export { HttpError, UndoError } from './types';
|
|
48
52
|
export type {
|
|
49
53
|
DataProvider, AuthProvider, NotificationProvider, MutationMode,
|
|
50
|
-
ValidationErrors, CrudOperator, LogicalFilter,
|
|
54
|
+
ValidationErrors, CrudOperator, LogicalFilter, FieldFilter,
|
|
51
55
|
CustomParams, CustomResult,
|
|
52
56
|
GetListParams, GetListResult,
|
|
53
57
|
GetOneParams, GetOneResult,
|
|
@@ -101,3 +105,6 @@ export {
|
|
|
101
105
|
} from './helpers';
|
|
102
106
|
export { TableState } from './table-state.svelte';
|
|
103
107
|
export type { TableStateOptions } from './table-state.svelte';
|
|
108
|
+
export type { UseInfiniteListOptions } from './hooks.svelte';
|
|
109
|
+
export type { UseSelectOptions } from './hooks.svelte';
|
|
110
|
+
export type { UseFormReturn } from './form-hooks.svelte';
|