@svadmin/core 0.23.1 → 0.23.3
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 +20 -3
- package/src/adapters/casbin.ts +1 -1
- package/src/adapters/casl.ts +1 -1
- package/src/audit.ts +12 -11
- package/src/auth-hooks.svelte.ts +25 -2
- package/src/chatProvider.svelte.ts +20 -3
- package/src/context.svelte.ts +17 -1
- package/src/data-transfer.svelte.ts +23 -11
- package/src/form-hooks.svelte.ts +71 -27
- package/src/form-hooks.test.svelte.ts +83 -0
- package/src/helpers-pure.ts +13 -6
- package/src/helpers.test.ts +2 -2
- package/src/helpers.ts +4 -0
- package/src/hook-utils.svelte.ts +40 -20
- package/src/hooks.svelte.ts +43 -50
- package/src/i18n.svelte.ts +3 -0
- package/src/index.ts +10 -8
- package/src/inferencer-openapi.ts +5 -3
- package/src/inferencer.ts +6 -4
- package/src/live-sse.ts +20 -17
- package/src/live-websocket.ts +1 -0
- package/src/live.svelte.ts +19 -11
- package/src/mutation-hooks.svelte.ts +39 -25
- package/src/mutation-hooks.test.svelte.ts +114 -0
- package/src/notification.svelte.ts +6 -6
- package/src/options.svelte.ts +1 -1
- package/src/{permissions.ts → permissions.svelte.ts} +1 -1
- package/src/permissions.test.ts +32 -80
- package/src/query-hooks.svelte.ts +18 -9
- package/src/query-hooks.test.svelte.ts +65 -0
- package/src/query-keys.ts +3 -4
- package/src/reactive-hooks.test.svelte.ts +26 -0
- package/src/router-provider.test.ts +3 -0
- package/src/router-provider.ts +13 -9
- package/src/router.ts +8 -0
- package/src/routing-hooks.svelte.ts +16 -13
- package/src/routing-hooks.test.svelte.ts +55 -0
- package/src/table-hooks.svelte.ts +46 -1
- package/src/table-hooks.test.svelte.ts +57 -0
- package/src/table-state.svelte.ts +17 -12
- package/src/theme.svelte.ts +25 -6
- package/src/toast.svelte.ts +8 -0
- package/src/unsaved-changes.svelte.ts +14 -6
- package/src/url-sync.ts +2 -1
- package/src/useCan.ts +2 -2
- package/src/useParsed.svelte.ts +12 -4
- package/src/utility-hooks.svelte.ts +5 -2
package/src/helpers-pure.ts
CHANGED
|
@@ -83,7 +83,10 @@ export function unionSorters(
|
|
|
83
83
|
const result = [...newSorters];
|
|
84
84
|
|
|
85
85
|
for (const perm of permanentSorters) {
|
|
86
|
-
|
|
86
|
+
const idx = result.findIndex((s) => s.field === perm.field);
|
|
87
|
+
if (idx >= 0) {
|
|
88
|
+
result[idx] = perm;
|
|
89
|
+
} else {
|
|
87
90
|
result.push(perm);
|
|
88
91
|
}
|
|
89
92
|
}
|
|
@@ -113,6 +116,7 @@ export function parseCSV(text: string): string[][] {
|
|
|
113
116
|
let currentRow: string[] = [];
|
|
114
117
|
let currentVal = '';
|
|
115
118
|
let inQuotes = false;
|
|
119
|
+
let wasQuoted = false;
|
|
116
120
|
|
|
117
121
|
for (let i = 0; i < text.length; i++) {
|
|
118
122
|
const ch = text[i];
|
|
@@ -120,7 +124,7 @@ export function parseCSV(text: string): string[][] {
|
|
|
120
124
|
if (ch === '"') {
|
|
121
125
|
if (i + 1 < text.length && text[i + 1] === '"') {
|
|
122
126
|
currentVal += '"';
|
|
123
|
-
i++;
|
|
127
|
+
i++;
|
|
124
128
|
} else {
|
|
125
129
|
inQuotes = false;
|
|
126
130
|
}
|
|
@@ -130,18 +134,21 @@ export function parseCSV(text: string): string[][] {
|
|
|
130
134
|
} else {
|
|
131
135
|
if (ch === '"') {
|
|
132
136
|
inQuotes = true;
|
|
137
|
+
wasQuoted = true;
|
|
133
138
|
} else if (ch === ',') {
|
|
134
|
-
currentRow.push(currentVal.trim());
|
|
139
|
+
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
135
140
|
currentVal = '';
|
|
141
|
+
wasQuoted = false;
|
|
136
142
|
} else if (ch === '\n' || ch === '\r') {
|
|
137
|
-
currentRow.push(currentVal.trim());
|
|
143
|
+
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
138
144
|
if (currentRow.some(v => v !== '')) {
|
|
139
145
|
rows.push(currentRow);
|
|
140
146
|
}
|
|
141
147
|
currentRow = [];
|
|
142
148
|
currentVal = '';
|
|
149
|
+
wasQuoted = false;
|
|
143
150
|
if (ch === '\r' && i + 1 < text.length && text[i + 1] === '\n') {
|
|
144
|
-
i++;
|
|
151
|
+
i++;
|
|
145
152
|
}
|
|
146
153
|
} else {
|
|
147
154
|
currentVal += ch;
|
|
@@ -150,7 +157,7 @@ export function parseCSV(text: string): string[][] {
|
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
if (currentVal || currentRow.length > 0) {
|
|
153
|
-
currentRow.push(currentVal.trim());
|
|
160
|
+
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
154
161
|
if (currentRow.some(v => v !== '')) {
|
|
155
162
|
rows.push(currentRow);
|
|
156
163
|
}
|
package/src/helpers.test.ts
CHANGED
|
@@ -123,12 +123,12 @@ describe('unionSorters', () => {
|
|
|
123
123
|
expect(result[0].field).toBe('name');
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
test('permanent sorters
|
|
126
|
+
test('permanent sorters override new sorters on same field', () => {
|
|
127
127
|
const permanent: Sort[] = [{ field: 'name', order: 'asc' }];
|
|
128
128
|
const newSorters: Sort[] = [{ field: 'name', order: 'desc' }];
|
|
129
129
|
const result = unionSorters(permanent, newSorters);
|
|
130
130
|
expect(result).toHaveLength(1);
|
|
131
|
-
expect(result[0].order).toBe('
|
|
131
|
+
expect(result[0].order).toBe('asc');
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
test('empty arrays', () => {
|
package/src/helpers.ts
CHANGED
|
@@ -125,6 +125,10 @@ export function deriveValidator(
|
|
|
125
125
|
case 'number': {
|
|
126
126
|
if (typeof value === 'string' && isNaN(Number(value))) {
|
|
127
127
|
errors[field.key] = t('validation.invalidNumber');
|
|
128
|
+
} else if (typeof value === 'number' && isNaN(value)) {
|
|
129
|
+
errors[field.key] = t('validation.invalidNumber');
|
|
130
|
+
} else if (typeof value !== 'string' && typeof value !== 'number') {
|
|
131
|
+
errors[field.key] = t('validation.invalidNumber');
|
|
128
132
|
}
|
|
129
133
|
break;
|
|
130
134
|
}
|
package/src/hook-utils.svelte.ts
CHANGED
|
@@ -12,15 +12,25 @@ import { notify } from './notification.svelte';
|
|
|
12
12
|
export async function checkError(error: unknown): Promise<void> {
|
|
13
13
|
try {
|
|
14
14
|
const authProvider = getAuthProvider({ optional: true });
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
if (authProvider?.onError) {
|
|
16
|
+
const result = await authProvider.onError(error);
|
|
17
|
+
if (!result) return;
|
|
18
|
+
if (result.logout) {
|
|
19
|
+
try { await authProvider.logout?.(); } catch { /* logout failure should not block redirect */ }
|
|
20
|
+
const { navigate } = await import('./router');
|
|
21
|
+
navigate(result.redirectTo ?? '/login');
|
|
22
|
+
} else if (result.redirectTo) {
|
|
23
|
+
const { navigate } = await import('./router');
|
|
24
|
+
navigate(result.redirectTo);
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (error && typeof error === 'object' && 'statusCode' in error) {
|
|
29
|
+
const status = (error as { statusCode: number }).statusCode;
|
|
30
|
+
if (status === 401 || status === 403) {
|
|
31
|
+
const { navigate } = await import('./router');
|
|
32
|
+
navigate('/login');
|
|
33
|
+
}
|
|
24
34
|
}
|
|
25
35
|
} catch { /* auth check failed silently */ }
|
|
26
36
|
}
|
|
@@ -72,6 +82,7 @@ export interface LiveSubscriptionParams {
|
|
|
72
82
|
onLiveEvent?: (event: LiveEvent) => void;
|
|
73
83
|
liveParams?: Record<string, unknown>;
|
|
74
84
|
enabled?: boolean;
|
|
85
|
+
dataProviderName?: string;
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
export function createLiveSubscription(paramsFn: () => LiveSubscriptionParams): void {
|
|
@@ -85,16 +96,24 @@ export function createLiveSubscription(paramsFn: () => LiveSubscriptionParams):
|
|
|
85
96
|
|
|
86
97
|
if (!liveProvider || liveMode === 'off' || !enabled) return;
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
params.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
let unsubscribe: (() => void) | undefined;
|
|
100
|
+
try {
|
|
101
|
+
unsubscribe = liveProvider.subscribe({
|
|
102
|
+
resource: params.resource,
|
|
103
|
+
liveParams: params.liveParams,
|
|
104
|
+
callback: (event: LiveEvent) => {
|
|
105
|
+
params.onLiveEvent?.(event);
|
|
106
|
+
if (liveMode === 'auto') {
|
|
107
|
+
const dpN = params.dataProviderName;
|
|
108
|
+
const dpMatch = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dpN;
|
|
109
|
+
queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === params.resource });
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.warn('[svadmin] LiveProvider.subscribe failed:', e);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
98
117
|
return unsubscribe;
|
|
99
118
|
});
|
|
100
119
|
}
|
|
@@ -128,11 +147,12 @@ export function fireErrorNotification(
|
|
|
128
147
|
config: NotificationConfig,
|
|
129
148
|
defaultMessage: string,
|
|
130
149
|
error?: unknown,
|
|
150
|
+
resource?: string,
|
|
131
151
|
): void {
|
|
132
152
|
if (config === false) return;
|
|
133
153
|
if (!config && !defaultMessage) return;
|
|
134
154
|
if (typeof config === 'function') {
|
|
135
|
-
const result = config(error);
|
|
155
|
+
const result = config(error, undefined, resource);
|
|
136
156
|
notify({ type: 'error', message: result.message, description: result.description });
|
|
137
157
|
return;
|
|
138
158
|
}
|
package/src/hooks.svelte.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { getAdminOptions } from './options.svelte';
|
|
|
26
26
|
import { getDataProviderForResource, getDataProvider, getLiveProvider } from './context.svelte';
|
|
27
27
|
import { useParsed } from './useParsed.svelte';
|
|
28
28
|
import { createOvertimeTracker, createLiveSubscription, fireSuccessNotification, fireErrorNotification, checkError } from './hook-utils.svelte';
|
|
29
|
-
import { invalidateByScopes } from './mutation-hooks.svelte';
|
|
29
|
+
import { invalidateByScopes, publishLiveEvent } from './mutation-hooks.svelte';
|
|
30
30
|
import type { NotificationConfig, OvertimeOptions, LiveSubscriptionParams } from './hook-utils.svelte';
|
|
31
31
|
import type { BaseRecord, HttpError, Pagination, Sort, Filter, DataProvider, KnownResources } from './types';
|
|
32
32
|
import type { LiveMode, LiveEvent } from './live.svelte';
|
|
@@ -92,6 +92,7 @@ export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError =
|
|
|
92
92
|
adminOptions.onLiveEvent?.(e);
|
|
93
93
|
},
|
|
94
94
|
enabled: options.queryOptions?.enabled ?? true,
|
|
95
|
+
dataProviderName: options.dataProviderName,
|
|
95
96
|
}));
|
|
96
97
|
|
|
97
98
|
let lastSuccessAt = 0;
|
|
@@ -103,7 +104,7 @@ export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError =
|
|
|
103
104
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
104
105
|
lastErrorAt = query.errorUpdatedAt;
|
|
105
106
|
checkError(query.error);
|
|
106
|
-
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
|
|
107
|
+
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error, options.resource ?? parsed.resource ?? '');
|
|
107
108
|
}
|
|
108
109
|
});
|
|
109
110
|
|
|
@@ -206,7 +207,7 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
|
|
|
206
207
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
207
208
|
lastErrorAt = query.errorUpdatedAt;
|
|
208
209
|
checkError(query.error);
|
|
209
|
-
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
|
|
210
|
+
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error, resource);
|
|
210
211
|
}
|
|
211
212
|
});
|
|
212
213
|
|
|
@@ -278,7 +279,7 @@ export function useCustom<TData = unknown, TError = HttpError>(options: UseCusto
|
|
|
278
279
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
279
280
|
lastErrorAt = query.errorUpdatedAt;
|
|
280
281
|
checkError(query.error);
|
|
281
|
-
fireErrorNotification(options.errorNotification, 'Custom request failed', query.error);
|
|
282
|
+
fireErrorNotification(options.errorNotification, 'Custom request failed', query.error, '');
|
|
282
283
|
}
|
|
283
284
|
});
|
|
284
285
|
|
|
@@ -298,7 +299,7 @@ export function useCustomMutation<TData = unknown, TError = HttpError, TVariable
|
|
|
298
299
|
},
|
|
299
300
|
onSuccess: (data, params) => {
|
|
300
301
|
if (params.resource && params.invalidates !== false) {
|
|
301
|
-
invalidateByScopes(queryClient, params.resource, params.invalidates, ['list', 'many']);
|
|
302
|
+
invalidateByScopes(queryClient, params.resource, params.invalidates, ['list', 'many'], undefined, dataProviderName);
|
|
302
303
|
}
|
|
303
304
|
},
|
|
304
305
|
onError: (error) => {
|
|
@@ -316,36 +317,32 @@ export function useCreateMany<TData extends BaseRecord = BaseRecord, TError = Ht
|
|
|
316
317
|
const resource = options.resource ?? parsed.resource ?? '';
|
|
317
318
|
const adminOptions = getAdminOptions();
|
|
318
319
|
const queryClient = useQueryClient();
|
|
319
|
-
let isMutating = $state(false);
|
|
320
|
-
const overtime = createOvertimeTracker(() => isMutating, options.overtimeOptions ?? adminOptions.overtime);
|
|
321
320
|
|
|
322
321
|
const mutation = createMutation<{ data: TData[] }, TError, { resource?: KnownResources; variables: TVariables[]; meta?: Record<string, unknown>; dataProviderName?: string }>(() => ({
|
|
323
322
|
mutationFn: async (params) => {
|
|
324
|
-
isMutating = true;
|
|
325
323
|
const resName = params.resource ?? resource;
|
|
326
324
|
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return { data: results.map(r => r.data) };
|
|
331
|
-
} finally { isMutating = false; }
|
|
325
|
+
if (provider.createMany) return await provider.createMany<TData, TVariables>({ resource: resName, variables: params.variables, meta: params.meta });
|
|
326
|
+
const results = await Promise.all(params.variables.map(v => provider.create<TData, TVariables>({ resource: resName, variables: v, meta: params.meta })));
|
|
327
|
+
return { data: results.map(r => r.data) };
|
|
332
328
|
},
|
|
333
329
|
onSuccess: (data, params) => {
|
|
334
330
|
const resName = params.resource ?? resource;
|
|
335
331
|
fireSuccessNotification(undefined, 'Created successfully', data.data, params.variables, resName);
|
|
336
332
|
audit({ action: 'create', resource: resName });
|
|
333
|
+
publishLiveEvent(resName, 'created');
|
|
337
334
|
},
|
|
338
335
|
onError: (error, params) => {
|
|
339
336
|
checkError(error);
|
|
340
|
-
fireErrorNotification(undefined, 'Create many failed', error);
|
|
337
|
+
fireErrorNotification(undefined, 'Create many failed', error, params.resource ?? resource);
|
|
341
338
|
},
|
|
342
339
|
onSettled: (_d, _e, params) => {
|
|
343
340
|
const resName = params.resource ?? resource;
|
|
344
|
-
invalidateByScopes(queryClient, resName, undefined, ['list', 'many']);
|
|
341
|
+
invalidateByScopes(queryClient, resName, undefined, ['list', 'many'], undefined, params.dataProviderName);
|
|
345
342
|
},
|
|
346
343
|
}));
|
|
347
344
|
|
|
348
|
-
return { mutation, get overtime() { return overtime; } };
|
|
345
|
+
return { mutation, get overtime() { return createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime); } };
|
|
349
346
|
}
|
|
350
347
|
|
|
351
348
|
export function useUpdateMany<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: { resource?: KnownResources; overtimeOptions?: OvertimeOptions } = {}) {
|
|
@@ -353,39 +350,35 @@ export function useUpdateMany<TData extends BaseRecord = BaseRecord, TError = Ht
|
|
|
353
350
|
const resource = options.resource ?? parsed.resource ?? '';
|
|
354
351
|
const adminOptions = getAdminOptions();
|
|
355
352
|
const queryClient = useQueryClient();
|
|
356
|
-
let isMutating = $state(false);
|
|
357
|
-
const overtime = createOvertimeTracker(() => isMutating, options.overtimeOptions ?? adminOptions.overtime);
|
|
358
353
|
|
|
359
354
|
const mutation = createMutation<{ data: TData[] }, TError, { resource?: KnownResources; ids: (string | number)[]; variables: TVariables; meta?: Record<string, unknown>; dataProviderName?: string }>(() => ({
|
|
360
355
|
mutationFn: async (params) => {
|
|
361
|
-
isMutating = true;
|
|
362
356
|
const resName = params.resource ?? resource;
|
|
363
357
|
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return { data: results.map(r => r.data) };
|
|
368
|
-
} finally { isMutating = false; }
|
|
358
|
+
if (provider.updateMany) return await provider.updateMany<TData, TVariables>({ resource: resName, ids: params.ids, variables: params.variables, meta: params.meta });
|
|
359
|
+
const results = await Promise.all(params.ids.map(id => provider.update<TData, TVariables>({ resource: resName, id, variables: params.variables, meta: params.meta })));
|
|
360
|
+
return { data: results.map(r => r.data) };
|
|
369
361
|
},
|
|
370
362
|
onSuccess: (data, params) => {
|
|
371
363
|
const resName = params.resource ?? resource;
|
|
372
364
|
fireSuccessNotification(undefined, 'Updated successfully', data.data, params.variables, resName);
|
|
373
365
|
audit({ action: 'update', resource: resName });
|
|
366
|
+
publishLiveEvent(resName, 'updated', params.ids);
|
|
374
367
|
},
|
|
375
368
|
onError: (error, params) => {
|
|
376
369
|
checkError(error);
|
|
377
|
-
fireErrorNotification(undefined, 'Update many failed', error);
|
|
370
|
+
fireErrorNotification(undefined, 'Update many failed', error, params.resource ?? resource);
|
|
378
371
|
},
|
|
379
372
|
onSettled: (_d, _e, params) => {
|
|
380
373
|
const resName = params.resource ?? resource;
|
|
381
|
-
invalidateByScopes(queryClient, resName, undefined, ['list', 'many', 'detail'], undefined);
|
|
374
|
+
invalidateByScopes(queryClient, resName, undefined, ['list', 'many', 'detail'], undefined, params.dataProviderName);
|
|
382
375
|
for (const id of params.ids) {
|
|
383
|
-
queryClient.invalidateQueries({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === id });
|
|
376
|
+
queryClient.invalidateQueries({ predicate: (q) => q.queryKey[0] === params.dataProviderName && q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === id });
|
|
384
377
|
}
|
|
385
378
|
},
|
|
386
379
|
}));
|
|
387
380
|
|
|
388
|
-
return { mutation, get overtime() { return overtime; } };
|
|
381
|
+
return { mutation, get overtime() { return createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime); } };
|
|
389
382
|
}
|
|
390
383
|
|
|
391
384
|
export function useDeleteMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: { resource?: KnownResources; overtimeOptions?: OvertimeOptions } = {}) {
|
|
@@ -393,39 +386,35 @@ export function useDeleteMany<TData extends BaseRecord = BaseRecord, TError = Ht
|
|
|
393
386
|
const resource = options.resource ?? parsed.resource ?? '';
|
|
394
387
|
const adminOptions = getAdminOptions();
|
|
395
388
|
const queryClient = useQueryClient();
|
|
396
|
-
let isMutating = $state(false);
|
|
397
|
-
const overtime = createOvertimeTracker(() => isMutating, options.overtimeOptions ?? adminOptions.overtime);
|
|
398
389
|
|
|
399
390
|
const mutation = createMutation<{ data: TData[] }, TError, { resource?: KnownResources; ids: (string | number)[]; meta?: Record<string, unknown>; dataProviderName?: string }>(() => ({
|
|
400
391
|
mutationFn: async (params) => {
|
|
401
|
-
isMutating = true;
|
|
402
392
|
const resName = params.resource ?? resource;
|
|
403
393
|
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return { data: results.map(r => r.data) };
|
|
408
|
-
} finally { isMutating = false; }
|
|
394
|
+
if (provider.deleteMany) return await provider.deleteMany<TData>({ resource: resName, ids: params.ids, meta: params.meta });
|
|
395
|
+
const results = await Promise.all(params.ids.map(id => provider.deleteOne<TData>({ resource: resName, id, meta: params.meta })));
|
|
396
|
+
return { data: results.map(r => r.data) };
|
|
409
397
|
},
|
|
410
398
|
onSuccess: (data, params) => {
|
|
411
399
|
const resName = params.resource ?? resource;
|
|
412
400
|
fireSuccessNotification(undefined, 'Deleted successfully', data.data, undefined, resName);
|
|
413
401
|
audit({ action: 'delete', resource: resName });
|
|
402
|
+
publishLiveEvent(resName, 'deleted', params.ids);
|
|
414
403
|
},
|
|
415
404
|
onError: (error, params) => {
|
|
416
405
|
checkError(error);
|
|
417
|
-
fireErrorNotification(undefined, 'Delete many failed', error);
|
|
406
|
+
fireErrorNotification(undefined, 'Delete many failed', error, params.resource ?? resource);
|
|
418
407
|
},
|
|
419
408
|
onSettled: (_d, _e, params) => {
|
|
420
409
|
const resName = params.resource ?? resource;
|
|
421
|
-
invalidateByScopes(queryClient, resName, undefined, ['list', 'many']);
|
|
410
|
+
invalidateByScopes(queryClient, resName, undefined, ['list', 'many'], undefined, params.dataProviderName);
|
|
422
411
|
for (const id of params.ids) {
|
|
423
|
-
queryClient.removeQueries({ predicate: (q) => q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === id });
|
|
412
|
+
queryClient.removeQueries({ predicate: (q) => q.queryKey[0] === params.dataProviderName && q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === id });
|
|
424
413
|
}
|
|
425
414
|
},
|
|
426
415
|
}));
|
|
427
416
|
|
|
428
|
-
return { mutation, get overtime() { return overtime; } };
|
|
417
|
+
return { mutation, get overtime() { return createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime); } };
|
|
429
418
|
}
|
|
430
419
|
|
|
431
420
|
// ─── useInvalidate ──────────────────────────────────────────────────
|
|
@@ -435,17 +424,18 @@ export function useInvalidate() {
|
|
|
435
424
|
return (params: { resource?: string; invalidates?: string[] | 'all' | false; id?: string | number; dataProviderName?: string }) => {
|
|
436
425
|
if (params.invalidates === false) return;
|
|
437
426
|
if (params.invalidates === 'all' || !params.resource) {
|
|
438
|
-
queryClient.invalidateQueries();
|
|
427
|
+
queryClient.invalidateQueries({ predicate: (q) => q.queryKey[0] === params.dataProviderName });
|
|
439
428
|
return;
|
|
440
429
|
}
|
|
441
430
|
const scopes = params.invalidates || ['resourceAll'];
|
|
442
431
|
const res = params.resource;
|
|
432
|
+
const dpMatch = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === params.dataProviderName;
|
|
443
433
|
for (const scope of scopes) {
|
|
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' });
|
|
434
|
+
if (scope === 'resourceAll') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res });
|
|
435
|
+
else if ((scope === 'detail' || scope === 'one') && params.id) queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res && q.queryKey[2] === 'one' && q.queryKey[3] === params.id });
|
|
436
|
+
else if (scope === 'detail' || scope === 'one') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res && q.queryKey[2] === 'one' });
|
|
437
|
+
else if (scope === 'list') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res && (q.queryKey[2] === 'list' || q.queryKey[2] === 'infiniteList' || q.queryKey[2] === 'select') });
|
|
438
|
+
else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res && q.queryKey[2] === 'many' });
|
|
449
439
|
}
|
|
450
440
|
};
|
|
451
441
|
}
|
|
@@ -470,12 +460,15 @@ export function useDataProvider(): (dataProviderName?: string) => DataProvider {
|
|
|
470
460
|
|
|
471
461
|
// ─── useThemedLayoutContext ─────────────────────────────────────────
|
|
472
462
|
|
|
463
|
+
let _sidebarCollapsed = $state(false);
|
|
464
|
+
|
|
465
|
+
export function resetSidebarCollapsed() { _sidebarCollapsed = false; }
|
|
466
|
+
|
|
473
467
|
export function useThemedLayoutContext() {
|
|
474
|
-
let sidebarCollapsed = $state(false);
|
|
475
468
|
return {
|
|
476
|
-
get sidebarCollapsed() { return
|
|
477
|
-
setSidebarCollapsed(v: boolean) {
|
|
478
|
-
toggleSidebar() {
|
|
469
|
+
get sidebarCollapsed() { return _sidebarCollapsed; },
|
|
470
|
+
setSidebarCollapsed(v: boolean) { _sidebarCollapsed = v; },
|
|
471
|
+
toggleSidebar() { _sidebarCollapsed = !_sidebarCollapsed; },
|
|
479
472
|
};
|
|
480
473
|
}
|
|
481
474
|
|
package/src/i18n.svelte.ts
CHANGED
|
@@ -507,6 +507,8 @@ let locales = $state<Record<string, Record<string, string>>>({
|
|
|
507
507
|
},
|
|
508
508
|
});
|
|
509
509
|
|
|
510
|
+
const initialLocales = locales;
|
|
511
|
+
|
|
510
512
|
/** Detect best locale from browser language */
|
|
511
513
|
function detectLocale(): string {
|
|
512
514
|
if (typeof navigator === 'undefined') return 'en';
|
|
@@ -552,6 +554,7 @@ export function addTranslations(locale: string, translations: Record<string, str
|
|
|
552
554
|
|
|
553
555
|
export function resetI18n(): void {
|
|
554
556
|
currentLocale = detectLocale();
|
|
557
|
+
locales = initialLocales;
|
|
555
558
|
}
|
|
556
559
|
|
|
557
560
|
/**
|
package/src/index.ts
CHANGED
|
@@ -26,22 +26,23 @@ export {
|
|
|
26
26
|
useOvertime, useRelation,
|
|
27
27
|
useNotification, useDataProvider,
|
|
28
28
|
useMenu, useBreadcrumb, useThemedLayoutContext,
|
|
29
|
+
publishLiveEvent, resetSidebarCollapsed,
|
|
29
30
|
} from './hooks.svelte';
|
|
30
|
-
export { matchRoute, navigate, currentPath, setActiveRouterProvider, beforeEach, afterEach } from './router';
|
|
31
|
+
export { matchRoute, navigate, currentPath, setActiveRouterProvider, beforeEach, afterEach, resetRouter } from './router';
|
|
31
32
|
export type { RouteGuard } from './router';
|
|
32
33
|
export { readURLState, writeURLState } from './url-sync';
|
|
33
|
-
export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, canAccessAsync } from './permissions';
|
|
34
|
+
export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, canAccessAsync } from './permissions.svelte';
|
|
34
35
|
export { useLive, useSubscription, usePublish } from './live.svelte';
|
|
35
|
-
export { toast, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue } from './toast.svelte';
|
|
36
|
+
export { toast, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue, resetToast } from './toast.svelte';
|
|
36
37
|
export { notify, closeNotification, setNotificationProvider, getNotificationProvider } from './notification.svelte';
|
|
37
38
|
export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation } from './i18n.svelte';
|
|
38
39
|
export { audit, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
|
|
39
40
|
export type { AuditLogProvider } from './audit';
|
|
40
|
-
export { setChatProvider, getChatProvider, setChatContext, getChatContext, setAgentProvider, getAgentProvider, registerApproval, resolveApproval, hasPendingApprovals } from './chatProvider.svelte';
|
|
41
|
+
export { setChatProvider, getChatProvider, setChatContext, getChatContext, setAgentProvider, getAgentProvider, registerApproval, resolveApproval, hasPendingApprovals, resetChatProvider } from './chatProvider.svelte';
|
|
41
42
|
export type { ChatProvider, ChatMessage, ChatContext, ChatAction, AgentProvider, AgentEvent, AgentOptions, AdminTool, AdminToolParameter, ToolResult } from './chatProvider.svelte';
|
|
42
|
-
export { getTheme, setTheme, toggleTheme, getResolvedTheme, getColorTheme, setColorTheme, getColorThemes, configureTheme, getThemeConfig, clearCssOverrides, builtinPresets, registerColorPreset, getColorPresets } from './theme.svelte';
|
|
43
|
+
export { getTheme, setTheme, toggleTheme, getResolvedTheme, getColorTheme, setColorTheme, getColorThemes, configureTheme, getThemeConfig, clearCssOverrides, builtinPresets, registerColorPreset, getColorPresets, resetTheme } from './theme.svelte';
|
|
43
44
|
export type { ThemeMode, ColorTheme, ThemeStrategy, ThemeConfig, ColorPreset } from './theme.svelte';
|
|
44
|
-
export { setUnsavedChanges, getUnsavedChanges, initUnsavedChangesNotifier } from './unsaved-changes.svelte';
|
|
45
|
+
export { setUnsavedChanges, getUnsavedChanges, initUnsavedChangesNotifier, resetUnsavedChanges } from './unsaved-changes.svelte';
|
|
45
46
|
export { setAdminOptions, getAdminOptions, getTextTransformers } from './options.svelte';
|
|
46
47
|
export type { AdminOptions, TextTransformers, OvertimeConfig } from './options.svelte';
|
|
47
48
|
export { checkError } from './hook-utils.svelte';
|
|
@@ -70,7 +71,7 @@ export type {
|
|
|
70
71
|
} from './types';
|
|
71
72
|
export type { InvalidateScope } from './options.svelte';
|
|
72
73
|
export type { LiveProvider, LiveEvent, LiveMode } from './live.svelte';
|
|
73
|
-
export type { Action, CanParams, CanResult, AccessControlProvider } from './permissions';
|
|
74
|
+
export type { Action, CanParams, CanResult, AccessControlProvider } from './permissions.svelte';
|
|
74
75
|
export type { AuditEntry, AuditHandler } from './audit';
|
|
75
76
|
export { useCan } from './useCan';
|
|
76
77
|
export type { UseCanOptions, UseCanResult } from './useCan';
|
|
@@ -84,8 +85,9 @@ export {
|
|
|
84
85
|
useRegister, useForgotPassword, useUpdatePassword,
|
|
85
86
|
useGetIdentity, useIsAuthenticated,
|
|
86
87
|
useOnError, usePermissions,
|
|
88
|
+
getLogoutVersion, resetLogoutVersion,
|
|
87
89
|
} from './auth-hooks.svelte';
|
|
88
|
-
export { useParsed } from './useParsed.svelte';
|
|
90
|
+
export { useParsed, resetGlobalPath, syncGlobalPath } from './useParsed.svelte';
|
|
89
91
|
export * from './useStepsForm.svelte';
|
|
90
92
|
export { createHashRouterProvider, createHistoryRouterProvider } from './router-provider';
|
|
91
93
|
export type { RouterProvider } from './router-provider';
|
|
@@ -229,11 +229,13 @@ export function inferFromOpenAPI(
|
|
|
229
229
|
});
|
|
230
230
|
|
|
231
231
|
// Check path-based CRUD capabilities
|
|
232
|
-
const
|
|
233
|
-
|
|
232
|
+
const baseName = schemaName.toLowerCase();
|
|
233
|
+
const resourceName = baseName.endsWith('s') ? baseName : baseName + 's';
|
|
234
|
+
const pathInfo = resourcePaths.get(baseName) ??
|
|
235
|
+
resourcePaths.get(resourceName);
|
|
234
236
|
|
|
235
237
|
resources.push({
|
|
236
|
-
name:
|
|
238
|
+
name: resourceName,
|
|
237
239
|
label: schemaName,
|
|
238
240
|
primaryKey,
|
|
239
241
|
fields,
|
package/src/inferencer.ts
CHANGED
|
@@ -12,7 +12,7 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
12
12
|
const URL_RE = /^https?:\/\//;
|
|
13
13
|
const IMAGE_RE = /\.(png|jpe?g|gif|svg|webp|avif|ico)(\?.*)?$/i;
|
|
14
14
|
const PHONE_RE = /^\+?[\d\s\-()]{7,}$/;
|
|
15
|
-
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2})
|
|
15
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2})?/;
|
|
16
16
|
const COLOR_RE = /^#([0-9a-fA-F]{3,8})$/;
|
|
17
17
|
|
|
18
18
|
type InferredType = FieldDefinition['type'];
|
|
@@ -31,7 +31,8 @@ export function inferFieldType(key: string, value: unknown): InferredType {
|
|
|
31
31
|
if (value.every(v => typeof v === 'string' && IMAGE_RE.test(v))) return 'images';
|
|
32
32
|
return 'tags';
|
|
33
33
|
}
|
|
34
|
-
return 'tags';
|
|
34
|
+
if (value.length > 0 && typeof value[0] === 'number') return 'tags';
|
|
35
|
+
return 'json';
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
if (typeof value === 'object') return 'json';
|
|
@@ -53,7 +54,7 @@ export function inferFieldType(key: string, value: unknown): InferredType {
|
|
|
53
54
|
if (lk.includes('avatar') || lk.includes('image') || lk.includes('photo') || lk.includes('thumbnail') || lk.includes('logo')) return 'image';
|
|
54
55
|
if (lk.includes('color') || lk.includes('colour')) return 'color';
|
|
55
56
|
if (lk.includes('description') || lk.includes('content') || lk.includes('body') || lk.includes('bio') || lk.includes('summary')) return 'textarea';
|
|
56
|
-
if (lk === 'created_at' || lk === 'updated_at' || lk.endsWith('_at') || lk.endsWith('_date') || lk
|
|
57
|
+
if (lk === 'created_at' || lk === 'updated_at' || lk.endsWith('_at') || lk.endsWith('_date') || /\bdate\b/.test(lk)) return 'date';
|
|
57
58
|
|
|
58
59
|
return 'text';
|
|
59
60
|
}
|
|
@@ -196,9 +197,10 @@ export function inferResource(
|
|
|
196
197
|
// ─── Code Generation ─────────────────────────────────────────
|
|
197
198
|
|
|
198
199
|
function generateCode(resource: ResourceDefinition): string {
|
|
200
|
+
const esc = (s: string) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
199
201
|
const fieldsCode = resource.fields.map(f => {
|
|
200
202
|
const lines = [
|
|
201
|
-
` { key: '${f.key}', label: '${f.label}', type: '${f.type}'`,
|
|
203
|
+
` { key: '${esc(f.key)}', label: '${esc(f.label)}', type: '${f.type}'`,
|
|
202
204
|
];
|
|
203
205
|
if (f.sortable) lines.push(` sortable: true`);
|
|
204
206
|
if (f.searchable) lines.push(` searchable: true`);
|