@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/package.json
CHANGED
package/src/audit.ts
CHANGED
|
@@ -45,6 +45,11 @@ export function getAuditLogProvider(): AuditLogProvider | null {
|
|
|
45
45
|
return auditLogProvider;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export function resetAuditLogProvider(): void {
|
|
49
|
+
auditLogProvider = null;
|
|
50
|
+
handler = (entry) => { console.info('[audit]', entry.action, entry.resource, entry.recordId); };
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
export function audit(entry: Omit<AuditEntry, 'timestamp'>): void {
|
|
49
54
|
const fullEntry: AuditEntry = { ...entry, timestamp: new Date().toISOString() };
|
|
50
55
|
try {
|
package/src/auth-hooks.svelte.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { AuthActionResult, CheckResult, Identity, AuthProvider } from './ty
|
|
|
13
13
|
interface CreateAuthMutationOptions {
|
|
14
14
|
method: keyof AuthProvider;
|
|
15
15
|
successMessage?: string | null;
|
|
16
|
-
errorMessage?: string;
|
|
16
|
+
errorMessage?: string | false;
|
|
17
17
|
onSuccess?: (result: AuthActionResult) => void;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -33,13 +33,13 @@ function createAuthMutation(options: CreateAuthMutationOptions) {
|
|
|
33
33
|
if (options.successMessage) notify({ type: 'success', message: options.successMessage });
|
|
34
34
|
if (options.onSuccess) options.onSuccess(result);
|
|
35
35
|
} else {
|
|
36
|
-
const msg = result.error?.message ?? options.errorMessage
|
|
37
|
-
notify({ type: 'error', message: msg });
|
|
36
|
+
const msg = result.error?.message ?? (typeof options.errorMessage === 'string' ? options.errorMessage : t('common.operationFailed'));
|
|
37
|
+
if (options.errorMessage !== false) notify({ type: 'error', message: msg });
|
|
38
38
|
}
|
|
39
39
|
return result;
|
|
40
40
|
} catch (err) {
|
|
41
|
-
const msg = err instanceof Error ? err.message : (options.errorMessage
|
|
42
|
-
notify({ type: 'error', message: msg });
|
|
41
|
+
const msg = err instanceof Error ? err.message : (typeof options.errorMessage === 'string' ? options.errorMessage : t('common.operationFailed'));
|
|
42
|
+
if (options.errorMessage !== false) notify({ type: 'error', message: msg });
|
|
43
43
|
return { success: false, error: { message: msg } };
|
|
44
44
|
} finally {
|
|
45
45
|
isLoading = false;
|
|
@@ -54,11 +54,11 @@ function createAuthMutation(options: CreateAuthMutationOptions) {
|
|
|
54
54
|
|
|
55
55
|
// ─── useLogin ─────────────────────────────────────────────────
|
|
56
56
|
|
|
57
|
-
export function useLogin() {
|
|
57
|
+
export function useLogin(opts?: { errorMessage?: string | false }) {
|
|
58
58
|
return createAuthMutation({
|
|
59
59
|
method: 'login',
|
|
60
60
|
successMessage: t('common.operationSuccess'),
|
|
61
|
-
errorMessage: t('common.loginFailed'),
|
|
61
|
+
errorMessage: opts?.errorMessage ?? t('common.loginFailed'),
|
|
62
62
|
onSuccess: (result) => { if (result.redirectTo) navigate(result.redirectTo); }
|
|
63
63
|
});
|
|
64
64
|
}
|
|
@@ -105,7 +105,7 @@ export function useUpdatePassword() {
|
|
|
105
105
|
// ─── useGetIdentity ──────────────────────────────────────────
|
|
106
106
|
|
|
107
107
|
export function useGetIdentity() {
|
|
108
|
-
const provider = getAuthProvider();
|
|
108
|
+
const provider = getAuthProvider({ optional: true });
|
|
109
109
|
let data = $state<Identity | null>(null);
|
|
110
110
|
let isLoading = $state(true);
|
|
111
111
|
let error = $state<Error | null>(null);
|
|
@@ -138,33 +138,43 @@ export function useGetIdentity() {
|
|
|
138
138
|
// ─── useIsAuthenticated ──────────────────────────────────────
|
|
139
139
|
|
|
140
140
|
export function useIsAuthenticated() {
|
|
141
|
-
const provider = getAuthProvider();
|
|
141
|
+
const provider = getAuthProvider({ optional: true });
|
|
142
142
|
let isAuthenticated = $state(false);
|
|
143
143
|
let isLoading = $state(true);
|
|
144
|
+
let checkResult = $state<CheckResult | null>(null);
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
isLoading = false;
|
|
153
|
-
});
|
|
154
|
-
} else {
|
|
146
|
+
function check() {
|
|
147
|
+
if (!provider) {
|
|
148
|
+
isAuthenticated = true;
|
|
149
|
+
isLoading = false;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (typeof window === 'undefined') {
|
|
155
153
|
// In SSR we assume false to prevent hydration mismatch before check
|
|
156
154
|
isAuthenticated = false;
|
|
157
155
|
isLoading = false;
|
|
156
|
+
return;
|
|
158
157
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
isLoading = true;
|
|
159
|
+
provider.check().then((result: CheckResult) => {
|
|
160
|
+
isAuthenticated = result.authenticated;
|
|
161
|
+
checkResult = result;
|
|
162
|
+
isLoading = false;
|
|
163
|
+
}).catch(() => {
|
|
164
|
+
isAuthenticated = false;
|
|
165
|
+
checkResult = { authenticated: false };
|
|
166
|
+
isLoading = false;
|
|
167
|
+
});
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
// Initial check
|
|
171
|
+
check();
|
|
172
|
+
|
|
165
173
|
return {
|
|
166
174
|
get isAuthenticated() { return isAuthenticated; },
|
|
167
175
|
get isLoading() { return isLoading; },
|
|
176
|
+
get data() { return checkResult; },
|
|
177
|
+
refetch: check,
|
|
168
178
|
};
|
|
169
179
|
}
|
|
170
180
|
|
|
@@ -176,7 +186,7 @@ export function useIsAuthenticated() {
|
|
|
176
186
|
* If it returns { redirectTo }, navigates there.
|
|
177
187
|
*/
|
|
178
188
|
export function useOnError() {
|
|
179
|
-
const provider = getAuthProvider();
|
|
189
|
+
const provider = getAuthProvider({ optional: true });
|
|
180
190
|
|
|
181
191
|
async function mutate(error: unknown) {
|
|
182
192
|
if (!provider?.onError) {
|
|
@@ -242,6 +252,14 @@ export function usePermissions<T = unknown>() {
|
|
|
242
252
|
fetch();
|
|
243
253
|
}
|
|
244
254
|
|
|
255
|
+
const hasFn = (perm: string): boolean => {
|
|
256
|
+
if (!permissions) return false;
|
|
257
|
+
if (Array.isArray(permissions)) return permissions.includes(perm);
|
|
258
|
+
if (permissions instanceof Set) return (permissions as Set<string>).has(perm);
|
|
259
|
+
if (typeof permissions === 'object') return !!(permissions as Record<string, boolean>)[perm];
|
|
260
|
+
return false;
|
|
261
|
+
};
|
|
262
|
+
|
|
245
263
|
return {
|
|
246
264
|
get isLoading() { return isLoading; },
|
|
247
265
|
get error() { return error; },
|
|
@@ -250,18 +268,10 @@ export function usePermissions<T = unknown>() {
|
|
|
250
268
|
refetch: fetch,
|
|
251
269
|
|
|
252
270
|
/** Check if a specific permission string exists */
|
|
253
|
-
has
|
|
254
|
-
if (!permissions) return false;
|
|
255
|
-
if (Array.isArray(permissions)) return permissions.includes(perm);
|
|
256
|
-
if (permissions instanceof Set) return (permissions as Set<string>).has(perm);
|
|
257
|
-
if (typeof permissions === 'object') return !!(permissions as Record<string, boolean>)[perm];
|
|
258
|
-
return false;
|
|
259
|
-
},
|
|
271
|
+
has: hasFn,
|
|
260
272
|
|
|
261
273
|
/** Check resource:action style permission */
|
|
262
|
-
can(resource: string, action: string): boolean {
|
|
263
|
-
return this.has(`${resource}:${action}`);
|
|
264
|
-
},
|
|
274
|
+
can: (resource: string, action: string): boolean => hasFn(`${resource}:${action}`),
|
|
265
275
|
};
|
|
266
276
|
}
|
|
267
277
|
|
|
@@ -186,36 +186,23 @@ export function getAgentProvider(): AgentProvider | null {
|
|
|
186
186
|
interface ApprovalCallback {
|
|
187
187
|
(approved: boolean): void;
|
|
188
188
|
}
|
|
189
|
-
|
|
189
|
+
interface ApprovalEntry { id: string; callback: ApprovalCallback }
|
|
190
|
+
let pendingApprovals = $state<ApprovalEntry[]>([]);
|
|
190
191
|
|
|
191
|
-
/**
|
|
192
|
-
* Register a pending approval request. Called internally by the framework
|
|
193
|
-
* when an `approval_request` event is received from the agent.
|
|
194
|
-
*/
|
|
195
192
|
export function registerApproval(id: string, callback: ApprovalCallback): void {
|
|
196
|
-
pendingApprovals
|
|
193
|
+
pendingApprovals = [...pendingApprovals, { id, callback }];
|
|
197
194
|
}
|
|
198
195
|
|
|
199
|
-
/**
|
|
200
|
-
* Approve or reject a pending tool call.
|
|
201
|
-
*
|
|
202
|
-
* @example
|
|
203
|
-
* ```svelte
|
|
204
|
-
* <button onclick={() => resolveApproval('req-1', true)}>Approve</button>
|
|
205
|
-
* <button onclick={() => resolveApproval('req-1', false)}>Reject</button>
|
|
206
|
-
* ```
|
|
207
|
-
*/
|
|
208
196
|
export function resolveApproval(id: string, approved: boolean): boolean {
|
|
209
|
-
const
|
|
210
|
-
if (!
|
|
211
|
-
|
|
212
|
-
pendingApprovals.
|
|
197
|
+
const entry = pendingApprovals.find(e => e.id === id);
|
|
198
|
+
if (!entry) return false;
|
|
199
|
+
entry.callback(approved);
|
|
200
|
+
pendingApprovals = pendingApprovals.filter(e => e.id !== id);
|
|
213
201
|
return true;
|
|
214
202
|
}
|
|
215
203
|
|
|
216
|
-
/** Check if there are any pending approval requests. */
|
|
217
204
|
export function hasPendingApprovals(): boolean {
|
|
218
|
-
return pendingApprovals.
|
|
205
|
+
return pendingApprovals.length > 0;
|
|
219
206
|
}
|
|
220
207
|
|
|
221
208
|
// ─── Chat Context singleton ────────────────────────────────────
|
package/src/context.svelte.ts
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import type { DataProvider, AuthProvider, ResourceDefinition } from './types';
|
|
5
5
|
import type { RouterProvider } from './router-provider';
|
|
6
|
+
import { resetNotificationProvider } from './notification.svelte';
|
|
7
|
+
import { resetAuditLogProvider } from './audit';
|
|
8
|
+
import { resetAccessControlProvider } from './permissions';
|
|
9
|
+
import { resetAdminOptions } from './options.svelte';
|
|
10
|
+
import { resetI18n } from './i18n.svelte';
|
|
6
11
|
|
|
7
12
|
// ─── DataProvider (supports single or multiple) ─────────────────
|
|
8
13
|
|
|
@@ -68,8 +73,8 @@ function isDataProvider(value: unknown): value is DataProvider {
|
|
|
68
73
|
|
|
69
74
|
let authProvider = $state<AuthProvider | null>(null);
|
|
70
75
|
|
|
71
|
-
export function setAuthProvider(provider: AuthProvider): void {
|
|
72
|
-
authProvider = provider;
|
|
76
|
+
export function setAuthProvider(provider: AuthProvider | null | undefined): void {
|
|
77
|
+
authProvider = provider || null;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
export function getAuthProvider(): AuthProvider;
|
|
@@ -122,3 +127,18 @@ export function setLiveProvider(provider: import('./live.svelte').LiveProvider):
|
|
|
122
127
|
export function getLiveProvider(): import('./live.svelte').LiveProvider | undefined {
|
|
123
128
|
return liveProviderState;
|
|
124
129
|
}
|
|
130
|
+
|
|
131
|
+
// ─── Reset — for testing / HMR ─────────────────────────────────
|
|
132
|
+
|
|
133
|
+
export function resetContext(): void {
|
|
134
|
+
providers = null;
|
|
135
|
+
authProvider = null;
|
|
136
|
+
resources = [];
|
|
137
|
+
routerProvider = undefined;
|
|
138
|
+
liveProviderState = undefined;
|
|
139
|
+
resetNotificationProvider();
|
|
140
|
+
resetAuditLogProvider();
|
|
141
|
+
resetAccessControlProvider();
|
|
142
|
+
resetAdminOptions();
|
|
143
|
+
resetI18n();
|
|
144
|
+
}
|
|
@@ -57,14 +57,13 @@ export function useExport<TData extends BaseRecord = BaseRecord>(options: UseExp
|
|
|
57
57
|
|
|
58
58
|
if (options.download !== false && typeof document !== 'undefined') {
|
|
59
59
|
const fields = Object.keys(mapped[0]);
|
|
60
|
-
const
|
|
60
|
+
const escapeCsvField = (s: string) => s.includes(',') || s.includes('"') || s.includes('\n') || s.includes('\r') ? `"${s.replace(/"/g, '""')}"` : s;
|
|
61
|
+
const header = fields.map(escapeCsvField).join(',');
|
|
61
62
|
const rows = mapped.map(record =>
|
|
62
63
|
fields.map(f => {
|
|
63
64
|
const val = record[f];
|
|
64
|
-
const str = val === null || val === undefined ? '' : String(val);
|
|
65
|
-
return
|
|
66
|
-
? `"${str.replace(/"/g, '""')}"`
|
|
67
|
-
: str;
|
|
65
|
+
const str = val === null || val === undefined ? '' : typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
66
|
+
return escapeCsvField(str);
|
|
68
67
|
}).join(',')
|
|
69
68
|
);
|
|
70
69
|
|
|
@@ -120,15 +119,20 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
|
|
|
120
119
|
const errored: { request: unknown; error: unknown }[] = [];
|
|
121
120
|
|
|
122
121
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
122
|
+
let text = await info.file.text();
|
|
123
|
+
// Strip BOM
|
|
124
|
+
if (text.charCodeAt(0) === 0xFEFF) {
|
|
125
|
+
text = text.slice(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const rows = parseCSV(text);
|
|
129
|
+
if (rows.length < 2) return;
|
|
126
130
|
|
|
127
|
-
const headers =
|
|
131
|
+
const headers = rows[0];
|
|
128
132
|
const records: Record<string, unknown>[] = [];
|
|
129
133
|
|
|
130
|
-
for (let i = 1; i <
|
|
131
|
-
const values =
|
|
134
|
+
for (let i = 1; i < rows.length; i++) {
|
|
135
|
+
const values = rows[i];
|
|
132
136
|
const record: Record<string, unknown> = {};
|
|
133
137
|
headers.forEach((h, idx) => { record[h] = values[idx] ?? ''; });
|
|
134
138
|
records.push(options.mapData ? (options.mapData(record) as unknown as Record<string, unknown>) : record);
|
|
@@ -151,18 +155,23 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
|
|
|
151
155
|
} else {
|
|
152
156
|
for (let i = 0; i < records.length; i += batchSize) {
|
|
153
157
|
const batch = records.slice(i, i + batchSize);
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
if (provider.createMany) {
|
|
159
|
+
try {
|
|
156
160
|
const res = await provider.createMany({ resource, variables: batch, meta: options.meta });
|
|
157
161
|
succeeded.push(res);
|
|
158
|
-
}
|
|
159
|
-
|
|
162
|
+
} catch (error) {
|
|
163
|
+
errored.push({ request: batch, error });
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// Fallback: loop internal items inside batch securely instead of breaking whole batch
|
|
167
|
+
for (const record of batch) {
|
|
168
|
+
try {
|
|
160
169
|
const res = await provider.create({ resource, variables: record, meta: options.meta });
|
|
161
170
|
succeeded.push(res);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
errored.push({ request: record, error });
|
|
162
173
|
}
|
|
163
174
|
}
|
|
164
|
-
} catch (error) {
|
|
165
|
-
errored.push({ request: batch, error });
|
|
166
175
|
}
|
|
167
176
|
processed += batch.length;
|
|
168
177
|
options.onProgress?.({ totalAmount: records.length, processedAmount: processed });
|
|
@@ -184,34 +193,57 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
|
|
|
184
193
|
};
|
|
185
194
|
}
|
|
186
195
|
|
|
187
|
-
/** Parse
|
|
188
|
-
function
|
|
189
|
-
const
|
|
190
|
-
let
|
|
196
|
+
/** Parse full CSV text respecting quoted multi-line fields */
|
|
197
|
+
function parseCSV(text: string): string[][] {
|
|
198
|
+
const rows: string[][] = [];
|
|
199
|
+
let currentRow: string[] = [];
|
|
200
|
+
let currentVal = '';
|
|
191
201
|
let inQuotes = false;
|
|
202
|
+
let wasQuoted = false;
|
|
192
203
|
|
|
193
|
-
for (let i = 0; i <
|
|
194
|
-
const ch =
|
|
204
|
+
for (let i = 0; i < text.length; i++) {
|
|
205
|
+
const ch = text[i];
|
|
195
206
|
if (inQuotes) {
|
|
196
|
-
if (ch === '"'
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
207
|
+
if (ch === '"') {
|
|
208
|
+
if (i + 1 < text.length && text[i + 1] === '"') {
|
|
209
|
+
currentVal += '"';
|
|
210
|
+
i++;
|
|
211
|
+
} else {
|
|
212
|
+
inQuotes = false;
|
|
213
|
+
}
|
|
201
214
|
} else {
|
|
202
|
-
|
|
215
|
+
currentVal += ch;
|
|
203
216
|
}
|
|
204
217
|
} else {
|
|
205
218
|
if (ch === '"') {
|
|
206
219
|
inQuotes = true;
|
|
220
|
+
wasQuoted = true;
|
|
207
221
|
} else if (ch === ',') {
|
|
208
|
-
|
|
209
|
-
|
|
222
|
+
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
223
|
+
currentVal = '';
|
|
224
|
+
wasQuoted = false;
|
|
225
|
+
} else if (ch === '\n' || ch === '\r') {
|
|
226
|
+
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
227
|
+
if (currentRow.some(v => v !== '')) {
|
|
228
|
+
rows.push(currentRow);
|
|
229
|
+
}
|
|
230
|
+
currentRow = [];
|
|
231
|
+
currentVal = '';
|
|
232
|
+
wasQuoted = false;
|
|
233
|
+
if (ch === '\r' && i + 1 < text.length && text[i + 1] === '\n') {
|
|
234
|
+
i++;
|
|
235
|
+
}
|
|
210
236
|
} else {
|
|
211
|
-
|
|
237
|
+
currentVal += ch;
|
|
212
238
|
}
|
|
213
239
|
}
|
|
214
240
|
}
|
|
215
|
-
|
|
216
|
-
|
|
241
|
+
|
|
242
|
+
if (currentVal || currentRow.length > 0) {
|
|
243
|
+
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
244
|
+
if (currentRow.some(v => v !== '')) {
|
|
245
|
+
rows.push(currentRow);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return rows;
|
|
217
249
|
}
|