@svadmin/core 0.22.1 → 0.23.1
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 +34 -24
- package/src/chatProvider.svelte.ts +8 -21
- package/src/context.svelte.ts +20 -0
- package/src/data-transfer.svelte.ts +13 -10
- package/src/form-hooks.svelte.ts +103 -43
- 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 +100 -17
- package/src/i18n.svelte.ts +23 -17
- package/src/index.ts +11 -4
- 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 +70 -20
- package/src/router-provider.ts +3 -1
- package/src/router.ts +23 -2
- package/src/routing-hooks.svelte.ts +37 -12
- package/src/table-hooks.svelte.ts +61 -15
- package/src/table-state.svelte.ts +2 -1
- package/src/theme.svelte.ts +30 -24
- package/src/toast.svelte.ts +22 -11
- package/src/types.ts +3 -1
- package/src/url-sync.ts +28 -16
- package/src/utility-hooks.svelte.ts +14 -4
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Pure utility functions — no Svelte runtime dependency
|
|
2
|
+
// Extracted to enable unit testing with bun test
|
|
3
|
+
|
|
4
|
+
import type { Filter, Sort, CrudOperator } from './types';
|
|
5
|
+
|
|
6
|
+
// ─── Table Helpers ────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the current filter value for a field from a filter array.
|
|
10
|
+
*/
|
|
11
|
+
export function getDefaultFilter(
|
|
12
|
+
columnName: string,
|
|
13
|
+
filters?: Filter[],
|
|
14
|
+
operatorType: CrudOperator = 'eq',
|
|
15
|
+
): unknown | undefined {
|
|
16
|
+
if (!filters) return undefined;
|
|
17
|
+
const filter = filters.find((f) => {
|
|
18
|
+
if ('field' in f) {
|
|
19
|
+
return f.field === columnName && f.operator === operatorType;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
});
|
|
23
|
+
return filter && 'value' in filter ? filter.value : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the current sort order for a field from a sorter array.
|
|
28
|
+
*/
|
|
29
|
+
export function getDefaultSortOrder(
|
|
30
|
+
columnName: string,
|
|
31
|
+
sorters?: Sort[],
|
|
32
|
+
): 'asc' | 'desc' | undefined {
|
|
33
|
+
if (!sorters) return undefined;
|
|
34
|
+
const sortItem = sorters.find((item) => item.field === columnName);
|
|
35
|
+
return sortItem?.order as 'asc' | 'desc' | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Merge two filter arrays, deduplicating by field+operator.
|
|
40
|
+
*/
|
|
41
|
+
export function unionFilters(
|
|
42
|
+
permanentFilter: Filter[],
|
|
43
|
+
newFilters: Filter[],
|
|
44
|
+
prevFilters: Filter[] = [],
|
|
45
|
+
): Filter[] {
|
|
46
|
+
const compareFilters = (a: Filter, b: Filter): boolean => {
|
|
47
|
+
if ('field' in a && 'field' in b) {
|
|
48
|
+
return a.field === b.field && a.operator === b.operator;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let result = [...prevFilters];
|
|
54
|
+
|
|
55
|
+
for (const newFilter of newFilters) {
|
|
56
|
+
const idx = result.findIndex((f) => compareFilters(f, newFilter));
|
|
57
|
+
if (idx >= 0) {
|
|
58
|
+
result[idx] = newFilter;
|
|
59
|
+
} else {
|
|
60
|
+
result.push(newFilter);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const perm of permanentFilter) {
|
|
65
|
+
const idx = result.findIndex((f) => compareFilters(f, perm));
|
|
66
|
+
if (idx >= 0) {
|
|
67
|
+
result[idx] = perm;
|
|
68
|
+
} else {
|
|
69
|
+
result.push(perm);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result.filter((f) => !('value' in f && f.value === undefined));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Merge two sorter arrays, deduplicating by field.
|
|
78
|
+
*/
|
|
79
|
+
export function unionSorters(
|
|
80
|
+
permanentSorters: Sort[],
|
|
81
|
+
newSorters: Sort[],
|
|
82
|
+
): Sort[] {
|
|
83
|
+
const result = [...newSorters];
|
|
84
|
+
|
|
85
|
+
for (const perm of permanentSorters) {
|
|
86
|
+
if (!result.some((s) => s.field === perm.field)) {
|
|
87
|
+
result.push(perm);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Convert a File to a base64 data URL string.
|
|
96
|
+
*/
|
|
97
|
+
export function file2Base64(file: File | { uid?: string } & Blob): Promise<string> {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const reader = new FileReader();
|
|
100
|
+
reader.readAsDataURL(file as Blob);
|
|
101
|
+
reader.addEventListener('load', () => {
|
|
102
|
+
resolve(reader.result as string);
|
|
103
|
+
});
|
|
104
|
+
reader.addEventListener('error', () => {
|
|
105
|
+
reject(reader.error);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Parse full CSV text respecting quoted multi-line fields */
|
|
111
|
+
export function parseCSV(text: string): string[][] {
|
|
112
|
+
const rows: string[][] = [];
|
|
113
|
+
let currentRow: string[] = [];
|
|
114
|
+
let currentVal = '';
|
|
115
|
+
let inQuotes = false;
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < text.length; i++) {
|
|
118
|
+
const ch = text[i];
|
|
119
|
+
if (inQuotes) {
|
|
120
|
+
if (ch === '"') {
|
|
121
|
+
if (i + 1 < text.length && text[i + 1] === '"') {
|
|
122
|
+
currentVal += '"';
|
|
123
|
+
i++; // skip escaped quote
|
|
124
|
+
} else {
|
|
125
|
+
inQuotes = false;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
currentVal += ch;
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
if (ch === '"') {
|
|
132
|
+
inQuotes = true;
|
|
133
|
+
} else if (ch === ',') {
|
|
134
|
+
currentRow.push(currentVal.trim());
|
|
135
|
+
currentVal = '';
|
|
136
|
+
} else if (ch === '\n' || ch === '\r') {
|
|
137
|
+
currentRow.push(currentVal.trim());
|
|
138
|
+
if (currentRow.some(v => v !== '')) {
|
|
139
|
+
rows.push(currentRow);
|
|
140
|
+
}
|
|
141
|
+
currentRow = [];
|
|
142
|
+
currentVal = '';
|
|
143
|
+
if (ch === '\r' && i + 1 < text.length && text[i + 1] === '\n') {
|
|
144
|
+
i++; // consume \n of \r\n
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
currentVal += ch;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (currentVal || currentRow.length > 0) {
|
|
153
|
+
currentRow.push(currentVal.trim());
|
|
154
|
+
if (currentRow.some(v => v !== '')) {
|
|
155
|
+
rows.push(currentRow);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return rows;
|
|
159
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Unit tests for helpers-pure.ts — pure functions, no Svelte runtime
|
|
2
|
+
import { describe, test, expect } from 'bun:test';
|
|
3
|
+
import {
|
|
4
|
+
getDefaultFilter, getDefaultSortOrder,
|
|
5
|
+
unionFilters, unionSorters,
|
|
6
|
+
parseCSV,
|
|
7
|
+
} from './helpers-pure';
|
|
8
|
+
import type { Filter, Sort } from './types';
|
|
9
|
+
|
|
10
|
+
// ─── getDefaultFilter ─────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
describe('getDefaultFilter', () => {
|
|
13
|
+
const filters: Filter[] = [
|
|
14
|
+
{ field: 'status', operator: 'eq', value: 'published' },
|
|
15
|
+
{ field: 'price', operator: 'gte', value: 100 },
|
|
16
|
+
{ field: 'title', operator: 'contains', value: 'hello' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
test('returns value for matching field+operator', () => {
|
|
20
|
+
expect(getDefaultFilter('status', filters)).toBe('published');
|
|
21
|
+
expect(getDefaultFilter('price', filters, 'gte')).toBe(100);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('returns undefined for non-matching field', () => {
|
|
25
|
+
expect(getDefaultFilter('nonexistent', filters)).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('returns undefined for wrong operator', () => {
|
|
29
|
+
expect(getDefaultFilter('status', filters, 'contains')).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('returns undefined for empty/undefined filters', () => {
|
|
33
|
+
expect(getDefaultFilter('status', [])).toBeUndefined();
|
|
34
|
+
expect(getDefaultFilter('status', undefined)).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ─── getDefaultSortOrder ──────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
describe('getDefaultSortOrder', () => {
|
|
41
|
+
const sorters: Sort[] = [
|
|
42
|
+
{ field: 'name', order: 'asc' },
|
|
43
|
+
{ field: 'createdAt', order: 'desc' },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
test('returns order for matching field', () => {
|
|
47
|
+
expect(getDefaultSortOrder('name', sorters)).toBe('asc');
|
|
48
|
+
expect(getDefaultSortOrder('createdAt', sorters)).toBe('desc');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('returns undefined for non-matching field', () => {
|
|
52
|
+
expect(getDefaultSortOrder('unknown', sorters)).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('returns undefined for empty/undefined sorters', () => {
|
|
56
|
+
expect(getDefaultSortOrder('name', undefined)).toBeUndefined();
|
|
57
|
+
expect(getDefaultSortOrder('name', [])).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ─── unionFilters ─────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe('unionFilters', () => {
|
|
64
|
+
test('merges new filters over previous', () => {
|
|
65
|
+
const prev: Filter[] = [
|
|
66
|
+
{ field: 'status', operator: 'eq', value: 'draft' },
|
|
67
|
+
];
|
|
68
|
+
const next: Filter[] = [
|
|
69
|
+
{ field: 'status', operator: 'eq', value: 'published' },
|
|
70
|
+
];
|
|
71
|
+
const result = unionFilters([], next, prev);
|
|
72
|
+
expect(result).toHaveLength(1);
|
|
73
|
+
expect(result[0].value).toBe('published');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('permanent filters override everything', () => {
|
|
77
|
+
const permanent: Filter[] = [
|
|
78
|
+
{ field: 'org', operator: 'eq', value: 'acme' },
|
|
79
|
+
];
|
|
80
|
+
const next: Filter[] = [
|
|
81
|
+
{ field: 'org', operator: 'eq', value: 'other' },
|
|
82
|
+
];
|
|
83
|
+
const result = unionFilters(permanent, next, []);
|
|
84
|
+
expect(result).toHaveLength(1);
|
|
85
|
+
expect(result[0].value).toBe('acme');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('removes filters with undefined value', () => {
|
|
89
|
+
const prev: Filter[] = [
|
|
90
|
+
{ field: 'status', operator: 'eq', value: 'draft' },
|
|
91
|
+
];
|
|
92
|
+
const next: Filter[] = [
|
|
93
|
+
{ field: 'status', operator: 'eq', value: undefined },
|
|
94
|
+
];
|
|
95
|
+
const result = unionFilters([], next, prev);
|
|
96
|
+
expect(result).toHaveLength(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('adds new filters not in previous', () => {
|
|
100
|
+
const prev: Filter[] = [
|
|
101
|
+
{ field: 'status', operator: 'eq', value: 'draft' },
|
|
102
|
+
];
|
|
103
|
+
const next: Filter[] = [
|
|
104
|
+
{ field: 'category', operator: 'eq', value: 'tech' },
|
|
105
|
+
];
|
|
106
|
+
const result = unionFilters([], next, prev);
|
|
107
|
+
expect(result).toHaveLength(2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('handles empty arrays', () => {
|
|
111
|
+
expect(unionFilters([], [], [])).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ─── unionSorters ─────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
describe('unionSorters', () => {
|
|
118
|
+
test('new sorters take priority', () => {
|
|
119
|
+
const permanent: Sort[] = [{ field: 'id', order: 'asc' }];
|
|
120
|
+
const newSorters: Sort[] = [{ field: 'name', order: 'desc' }];
|
|
121
|
+
const result = unionSorters(permanent, newSorters);
|
|
122
|
+
expect(result).toHaveLength(2);
|
|
123
|
+
expect(result[0].field).toBe('name');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('permanent sorters are not duplicated', () => {
|
|
127
|
+
const permanent: Sort[] = [{ field: 'name', order: 'asc' }];
|
|
128
|
+
const newSorters: Sort[] = [{ field: 'name', order: 'desc' }];
|
|
129
|
+
const result = unionSorters(permanent, newSorters);
|
|
130
|
+
expect(result).toHaveLength(1);
|
|
131
|
+
expect(result[0].order).toBe('desc');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('empty arrays', () => {
|
|
135
|
+
expect(unionSorters([], [])).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ─── parseCSV ─────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
describe('parseCSV', () => {
|
|
142
|
+
test('parses simple CSV', () => {
|
|
143
|
+
const rows = parseCSV('name,age\nAlice,30\nBob,25');
|
|
144
|
+
expect(rows).toHaveLength(3);
|
|
145
|
+
expect(rows[0]).toEqual(['name', 'age']);
|
|
146
|
+
expect(rows[1]).toEqual(['Alice', '30']);
|
|
147
|
+
expect(rows[2]).toEqual(['Bob', '25']);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('handles quoted fields', () => {
|
|
151
|
+
const rows = parseCSV('name,bio\nAlice,"Hello, World"');
|
|
152
|
+
expect(rows).toHaveLength(2);
|
|
153
|
+
expect(rows[1][1]).toBe('Hello, World');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('handles escaped quotes', () => {
|
|
157
|
+
const rows = parseCSV('val\n"He said ""hello"""');
|
|
158
|
+
expect(rows).toHaveLength(2);
|
|
159
|
+
expect(rows[1][0]).toBe('He said "hello"');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('handles CRLF line endings', () => {
|
|
163
|
+
const rows = parseCSV('a,b\r\n1,2\r\n3,4');
|
|
164
|
+
expect(rows).toHaveLength(3);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('skips empty rows', () => {
|
|
168
|
+
const rows = parseCSV('a\n\nb\n');
|
|
169
|
+
expect(rows).toHaveLength(2);
|
|
170
|
+
expect(rows[0]).toEqual(['a']);
|
|
171
|
+
expect(rows[1]).toEqual(['b']);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('handles empty input', () => {
|
|
175
|
+
expect(parseCSV('')).toEqual([]);
|
|
176
|
+
});
|
|
177
|
+
});
|
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
|
}
|