@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
|
@@ -3,6 +3,7 @@ import { getAdminOptions } from './options.svelte';
|
|
|
3
3
|
import { getDataProviderForResource, getDataProvider, getLiveProvider } from './context.svelte';
|
|
4
4
|
import { useParsed } from './useParsed.svelte';
|
|
5
5
|
import {
|
|
6
|
+
checkError,
|
|
6
7
|
createOvertimeTracker,
|
|
7
8
|
createLiveSubscription,
|
|
8
9
|
fireSuccessNotification,
|
|
@@ -22,19 +23,18 @@ import type { LiveMode, LiveEvent } from './live.svelte';
|
|
|
22
23
|
*/
|
|
23
24
|
function extendQuery<Q extends object, E extends Record<string, unknown>>(
|
|
24
25
|
query: Q,
|
|
25
|
-
extensions:
|
|
26
|
+
extensions: E,
|
|
26
27
|
): Q & E {
|
|
27
28
|
return new Proxy(query, {
|
|
28
29
|
get(target, prop) {
|
|
29
|
-
if (typeof prop === 'string') {
|
|
30
|
-
|
|
31
|
-
if (prop in ext) return ext[prop as keyof E];
|
|
30
|
+
if (typeof prop === 'string' && prop in extensions) {
|
|
31
|
+
return extensions[prop as keyof E];
|
|
32
32
|
}
|
|
33
|
-
return target
|
|
33
|
+
return Reflect.get(target, prop);
|
|
34
34
|
},
|
|
35
35
|
has(target, prop) {
|
|
36
|
-
if (typeof prop === 'string' && prop in extensions
|
|
37
|
-
return prop
|
|
36
|
+
if (typeof prop === 'string' && prop in extensions) return true;
|
|
37
|
+
return Reflect.has(target, prop);
|
|
38
38
|
},
|
|
39
39
|
}) as Q & E;
|
|
40
40
|
}
|
|
@@ -73,7 +73,7 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
73
73
|
const queryOptions = opts.queryOptions;
|
|
74
74
|
|
|
75
75
|
return {
|
|
76
|
-
queryKey: [resource, 'list', opts.pagination, opts.sorters, opts.filters, opts.meta],
|
|
76
|
+
queryKey: [opts.dataProviderName, resource, 'list', opts.pagination, opts.sorters, opts.filters, opts.meta],
|
|
77
77
|
queryFn: async () => provider.getList<TData>({
|
|
78
78
|
resource,
|
|
79
79
|
pagination: opts.pagination,
|
|
@@ -105,16 +105,22 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
105
105
|
};
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
// Track last notification timestamps to prevent duplicate firings on re-render
|
|
109
|
+
let lastSuccessAt = 0;
|
|
110
|
+
let lastErrorAt = 0;
|
|
108
111
|
$effect(() => {
|
|
109
112
|
const opts = getOptions();
|
|
110
|
-
if (query.isSuccess && opts.successNotification) {
|
|
113
|
+
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && opts.successNotification) {
|
|
114
|
+
lastSuccessAt = query.dataUpdatedAt;
|
|
111
115
|
fireSuccessNotification(opts.successNotification, '', query.data, undefined, getResource());
|
|
112
|
-
} else if (query.isError) {
|
|
116
|
+
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
117
|
+
lastErrorAt = query.errorUpdatedAt;
|
|
118
|
+
checkError(query.error);
|
|
113
119
|
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error);
|
|
114
120
|
}
|
|
115
121
|
});
|
|
116
122
|
|
|
117
|
-
return extendQuery(query,
|
|
123
|
+
return extendQuery(query, { overtime });
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
// ─── useOne ────────────────────────────────────────────────────
|
|
@@ -149,7 +155,7 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
149
155
|
const queryOptions = opts.queryOptions;
|
|
150
156
|
|
|
151
157
|
return {
|
|
152
|
-
queryKey: [resource, 'one', id, opts.meta],
|
|
158
|
+
queryKey: [opts.dataProviderName, resource, 'one', id, opts.meta],
|
|
153
159
|
queryFn: async () => {
|
|
154
160
|
if (id == null) throw new Error('useOne requires an id');
|
|
155
161
|
const result = await provider.getOne<TData>({
|
|
@@ -168,16 +174,37 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
168
174
|
|
|
169
175
|
const overtime = createOvertimeTracker(() => query.isLoading, typeof optionsOrGetter === 'function' ? optionsOrGetter().overtimeOptions : optionsOrGetter.overtimeOptions ?? adminOptions.overtime);
|
|
170
176
|
|
|
177
|
+
createLiveSubscription((): LiveSubscriptionParams => {
|
|
178
|
+
const opts = getOptions();
|
|
179
|
+
const resource = getResource();
|
|
180
|
+
return {
|
|
181
|
+
resource,
|
|
182
|
+
liveProvider: getLiveProvider(),
|
|
183
|
+
liveMode: opts.liveMode ?? adminOptions.liveMode,
|
|
184
|
+
onLiveEvent: (e: LiveEvent) => {
|
|
185
|
+
opts.onLiveEvent?.(e);
|
|
186
|
+
adminOptions.onLiveEvent?.(e);
|
|
187
|
+
},
|
|
188
|
+
liveParams: opts.liveParams,
|
|
189
|
+
enabled: (opts.queryOptions?.enabled ?? true) && getId() != null,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
let lastSuccessAt = 0;
|
|
194
|
+
let lastErrorAt = 0;
|
|
171
195
|
$effect(() => {
|
|
172
196
|
const opts = getOptions();
|
|
173
|
-
if (query.isSuccess && opts.successNotification) {
|
|
197
|
+
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && opts.successNotification) {
|
|
198
|
+
lastSuccessAt = query.dataUpdatedAt;
|
|
174
199
|
fireSuccessNotification(opts.successNotification, '', query.data, undefined, getResource());
|
|
175
|
-
} else if (query.isError) {
|
|
200
|
+
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
201
|
+
lastErrorAt = query.errorUpdatedAt;
|
|
202
|
+
checkError(query.error);
|
|
176
203
|
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error);
|
|
177
204
|
}
|
|
178
205
|
});
|
|
179
206
|
|
|
180
|
-
return extendQuery(query,
|
|
207
|
+
return extendQuery(query, { overtime });
|
|
181
208
|
}
|
|
182
209
|
|
|
183
210
|
// ─── useShow ──────────────────────────────────────────────────
|
|
@@ -200,7 +227,7 @@ export function useShow<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
200
227
|
id: showId,
|
|
201
228
|
}));
|
|
202
229
|
|
|
203
|
-
return extendQuery(result, ()
|
|
230
|
+
return extendQuery(result, { get showId() { return showId; }, setShowId });
|
|
204
231
|
}
|
|
205
232
|
|
|
206
233
|
// ─── useMany ───────────────────────────────────────────────────
|
|
@@ -229,7 +256,7 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
229
256
|
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
230
257
|
|
|
231
258
|
return {
|
|
232
|
-
queryKey: [resource, 'many', ids, meta],
|
|
259
|
+
queryKey: [dataProviderName, resource, 'many', ids, meta],
|
|
233
260
|
queryFn: async () => {
|
|
234
261
|
if (!ids.length) return { data: [] };
|
|
235
262
|
if (provider.getMany) {
|
|
@@ -247,13 +274,36 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
247
274
|
|
|
248
275
|
const overtime = createOvertimeTracker(() => query.isLoading, typeof optionsOrGetter === 'function' ? optionsOrGetter().overtimeOptions : optionsOrGetter.overtimeOptions ?? adminOptions.overtime);
|
|
249
276
|
|
|
277
|
+
createLiveSubscription((): LiveSubscriptionParams => {
|
|
278
|
+
const opts = getOptions();
|
|
279
|
+
return {
|
|
280
|
+
resource: opts.resource,
|
|
281
|
+
liveProvider: getLiveProvider(),
|
|
282
|
+
liveMode: opts.liveMode ?? adminOptions.liveMode,
|
|
283
|
+
onLiveEvent: (e: LiveEvent) => {
|
|
284
|
+
opts.onLiveEvent?.(e);
|
|
285
|
+
adminOptions.onLiveEvent?.(e);
|
|
286
|
+
},
|
|
287
|
+
liveParams: opts.liveParams,
|
|
288
|
+
enabled: (opts.queryOptions?.enabled ?? true) && opts.ids.length > 0,
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
let lastSuccessAt = 0;
|
|
293
|
+
let lastErrorAt = 0;
|
|
250
294
|
$effect(() => {
|
|
251
295
|
const opts = getOptions();
|
|
252
|
-
if (query.isSuccess &&
|
|
253
|
-
|
|
296
|
+
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && opts.successNotification) {
|
|
297
|
+
lastSuccessAt = query.dataUpdatedAt;
|
|
298
|
+
fireSuccessNotification(opts.successNotification, '', query.data, undefined, opts.resource);
|
|
299
|
+
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
300
|
+
lastErrorAt = query.errorUpdatedAt;
|
|
301
|
+
checkError(query.error);
|
|
302
|
+
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error);
|
|
303
|
+
}
|
|
254
304
|
});
|
|
255
305
|
|
|
256
|
-
return extendQuery(query,
|
|
306
|
+
return extendQuery(query, { overtime });
|
|
257
307
|
}
|
|
258
308
|
|
|
259
309
|
export function useApiUrl(dataProviderName?: string): string {
|
package/src/router-provider.ts
CHANGED
|
@@ -100,7 +100,9 @@ export function createHistoryRouterProvider(basePath = ''): RouterProvider {
|
|
|
100
100
|
if (typeof window === 'undefined') {
|
|
101
101
|
return { resource: undefined, action: undefined, id: undefined, params: {}, pathname: '/' };
|
|
102
102
|
}
|
|
103
|
-
const pathname = window.location.pathname.
|
|
103
|
+
const pathname = (basePath && window.location.pathname.startsWith(basePath)
|
|
104
|
+
? window.location.pathname.slice(basePath.length) || '/'
|
|
105
|
+
: window.location.pathname);
|
|
104
106
|
const params: Record<string, string> = {};
|
|
105
107
|
for (const [k, v] of new URLSearchParams(window.location.search).entries()) {
|
|
106
108
|
params[k] = v;
|
package/src/router.ts
CHANGED
|
@@ -7,13 +7,26 @@ interface RouteMatch {
|
|
|
7
7
|
params: Record<string, string>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
export type RouteGuard = (to: string, from: string) => boolean | Promise<boolean>;
|
|
11
|
+
|
|
11
12
|
let _routerProvider: RouterProvider | undefined;
|
|
13
|
+
let _beforeGuards: RouteGuard[] = [];
|
|
14
|
+
let _afterGuards: ((to: string, from: string) => void)[] = [];
|
|
12
15
|
|
|
13
16
|
export function setActiveRouterProvider(provider: RouterProvider | undefined) {
|
|
14
17
|
_routerProvider = provider;
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
export function beforeEach(guard: RouteGuard): () => void {
|
|
21
|
+
_beforeGuards.push(guard);
|
|
22
|
+
return () => { _beforeGuards = _beforeGuards.filter(g => g !== guard); };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function afterEach(guard: (to: string, from: string) => void): () => void {
|
|
26
|
+
_afterGuards.push(guard);
|
|
27
|
+
return () => { _afterGuards = _afterGuards.filter(g => g !== guard); };
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
// Parse route pattern like /products/edit/:id into regex
|
|
18
31
|
function compileRoute(pattern: string): { regex: RegExp; keys: string[] } {
|
|
19
32
|
const keys: string[] = [];
|
|
@@ -50,12 +63,20 @@ export function matchRoute(
|
|
|
50
63
|
return null;
|
|
51
64
|
}
|
|
52
65
|
|
|
53
|
-
export function navigate(path: string, options?: { replaceState?: boolean }): void {
|
|
66
|
+
export async function navigate(path: string, options?: { replaceState?: boolean }): Promise<void> {
|
|
67
|
+
const from = currentPath();
|
|
68
|
+
for (const guard of _beforeGuards) {
|
|
69
|
+
const allowed = await guard(path, from);
|
|
70
|
+
if (!allowed) return;
|
|
71
|
+
}
|
|
54
72
|
if (_routerProvider) {
|
|
55
73
|
_routerProvider.go({ to: path, type: options?.replaceState ? 'replace' : 'push' });
|
|
56
74
|
} else if (typeof window !== 'undefined') {
|
|
57
75
|
window.location.hash = '#' + path.replace(/^#/, '');
|
|
58
76
|
}
|
|
77
|
+
for (const guard of _afterGuards) {
|
|
78
|
+
guard(path, from);
|
|
79
|
+
}
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
/**
|
|
@@ -6,11 +6,24 @@ import { useParsed } from './useParsed.svelte';
|
|
|
6
6
|
|
|
7
7
|
export function useGetToPath() {
|
|
8
8
|
const routerProvider = getRouterProvider();
|
|
9
|
+
const parsed = useParsed();
|
|
9
10
|
|
|
10
11
|
return (options: { resource?: string; action?: 'list' | 'create' | 'edit' | 'show' | 'clone'; id?: string | number; meta?: Record<string, unknown> }) => {
|
|
11
|
-
const { resource, action, id } = options;
|
|
12
|
+
const { resource, action, id, meta } = options;
|
|
12
13
|
if (!resource) return '/';
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
// Support nested resources: use meta.parentPath or auto-detect from current URL
|
|
16
|
+
let basePath: string;
|
|
17
|
+
if (meta?.parentPath) {
|
|
18
|
+
basePath = `${meta.parentPath}/${resource}`;
|
|
19
|
+
} else if (parsed.resourcePath && parsed.resource === resource) {
|
|
20
|
+
// Re-use the current nested path context (e.g., /teams/123/users)
|
|
21
|
+
basePath = `/${parsed.resourcePath}`;
|
|
22
|
+
} else {
|
|
23
|
+
basePath = `/${resource}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let path = basePath;
|
|
14
27
|
if (action === 'create') path += '/create';
|
|
15
28
|
else if (action === 'edit' && id) path += `/edit/${id}`;
|
|
16
29
|
else if (action === 'show' && id) path += `/show/${id}`;
|
|
@@ -70,16 +83,28 @@ export function useLink() {
|
|
|
70
83
|
|
|
71
84
|
export function useResource(resourceName?: string) {
|
|
72
85
|
const parsed = useParsed();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
|
|
87
|
+
// Derived resource — re-evaluates when parsed.resource or resourceName changes
|
|
88
|
+
const resolvedResource = $derived.by(() => {
|
|
89
|
+
const target = resourceName ?? parsed.resource;
|
|
90
|
+
if (!target) return undefined;
|
|
91
|
+
try {
|
|
92
|
+
return getResource(target);
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
get resource() { return resolvedResource; },
|
|
100
|
+
get resources() { return getResources(); },
|
|
101
|
+
get identifier() { return resolvedResource?.identifier ?? resolvedResource?.name; },
|
|
102
|
+
/** Resolve a resource by name — useful for dynamic lookups inside callbacks */
|
|
103
|
+
select: (name: string) => {
|
|
104
|
+
const res = getResource(name);
|
|
105
|
+
return { resource: res, identifier: res.identifier ?? res.name };
|
|
106
|
+
},
|
|
107
|
+
};
|
|
83
108
|
}
|
|
84
109
|
|
|
85
110
|
export function useNavigation() {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useParsed } from './useParsed.svelte';
|
|
2
2
|
import { useList } from './query-hooks.svelte';
|
|
3
3
|
import { readURLState, writeURLState } from './url-sync';
|
|
4
|
+
import { getAdminOptions } from './options.svelte';
|
|
4
5
|
import type { Pagination, Sort, Filter, BaseRecord, HttpError, KnownResources, GetListResult } from './types';
|
|
5
6
|
import type { UseListOptions, MaybeGetter } from './query-hooks.svelte';
|
|
6
7
|
|
|
@@ -34,6 +35,7 @@ export function useTable<
|
|
|
34
35
|
// Use a local derived to compute these since they are used to initialize state
|
|
35
36
|
// Even if options change, init values are only used once, which makes sense for initial state
|
|
36
37
|
const initialOpts = getOptions();
|
|
38
|
+
const syncWithLocation = initialOpts.syncWithLocation ?? getAdminOptions().syncWithLocation;
|
|
37
39
|
|
|
38
40
|
const paginationMode = $derived(getOptions().pagination?.mode ?? 'server');
|
|
39
41
|
const sortersMode = $derived(getOptions().sorters?.mode ?? 'server');
|
|
@@ -47,7 +49,7 @@ export function useTable<
|
|
|
47
49
|
let initSorters = initialOpts.sorters?.initial ?? [];
|
|
48
50
|
let initFilters = initialOpts.filters?.initial ?? [];
|
|
49
51
|
|
|
50
|
-
if (
|
|
52
|
+
if (syncWithLocation && typeof window !== 'undefined') {
|
|
51
53
|
const urlState = readURLState();
|
|
52
54
|
if (urlState.page || urlState.pageSize) {
|
|
53
55
|
initPagination = {
|
|
@@ -93,13 +95,14 @@ export function useTable<
|
|
|
93
95
|
const query = tableQueryInfo;
|
|
94
96
|
const overtime = tableQueryInfo.overtime;
|
|
95
97
|
|
|
96
|
-
function setSorters(newSorters: Sort[]) { currentSorters = newSorters; }
|
|
98
|
+
function setSorters(newSorters: Sort[]) { currentSorters = newSorters; pagination = { ...pagination, current: 1 }; }
|
|
97
99
|
function setFilters(newFilters: Filter[], mode?: FilterSetMode) {
|
|
98
100
|
const behavior = mode ?? filterDefaultBehavior;
|
|
99
101
|
if (behavior === 'merge') {
|
|
100
102
|
const merged = [...currentFilters];
|
|
101
103
|
for (const nf of newFilters) {
|
|
102
|
-
|
|
104
|
+
if (!('field' in nf)) { merged.push(nf); continue; }
|
|
105
|
+
const idx = merged.findIndex(f => 'field' in f && f.field === nf.field && f.operator === nf.operator);
|
|
103
106
|
if (idx >= 0) {
|
|
104
107
|
if (nf.value === undefined || nf.value === null || nf.value === '') merged.splice(idx, 1);
|
|
105
108
|
else merged[idx] = nf;
|
|
@@ -117,7 +120,7 @@ export function useTable<
|
|
|
117
120
|
function setPage(page: number) { pagination = { ...pagination, current: page }; }
|
|
118
121
|
function setPageSize(size: number) { pagination = { ...pagination, pageSize: size, current: 1 }; }
|
|
119
122
|
|
|
120
|
-
if (
|
|
123
|
+
if (syncWithLocation && typeof window !== 'undefined') {
|
|
121
124
|
$effect(() => {
|
|
122
125
|
writeURLState({
|
|
123
126
|
page: pagination.current,
|
|
@@ -127,6 +130,39 @@ export function useTable<
|
|
|
127
130
|
filters: currentFilters,
|
|
128
131
|
});
|
|
129
132
|
});
|
|
133
|
+
|
|
134
|
+
$effect(() => {
|
|
135
|
+
const handler = () => {
|
|
136
|
+
const urlState = readURLState();
|
|
137
|
+
const newPage = urlState.page ?? 1;
|
|
138
|
+
if (newPage !== pagination.current) {
|
|
139
|
+
pagination = { ...pagination, current: newPage };
|
|
140
|
+
}
|
|
141
|
+
const newPageSize = urlState.pageSize ?? initialOpts.pagination?.pageSize ?? getAdminOptions().defaultPageSize ?? 10;
|
|
142
|
+
if (newPageSize !== pagination.pageSize) {
|
|
143
|
+
pagination = { ...pagination, pageSize: newPageSize };
|
|
144
|
+
}
|
|
145
|
+
if (urlState.sortField) {
|
|
146
|
+
const newSorter: Sort = { field: urlState.sortField, order: (urlState.sortOrder as 'asc' | 'desc') ?? 'asc' };
|
|
147
|
+
if (JSON.stringify([newSorter]) !== JSON.stringify(currentSorters)) {
|
|
148
|
+
currentSorters = [newSorter];
|
|
149
|
+
}
|
|
150
|
+
} else if (currentSorters.length > 0) {
|
|
151
|
+
currentSorters = [];
|
|
152
|
+
}
|
|
153
|
+
if (urlState.filters && JSON.stringify(urlState.filters) !== JSON.stringify(currentFilters)) {
|
|
154
|
+
currentFilters = urlState.filters;
|
|
155
|
+
} else if (!urlState.filters && currentFilters.length > 0) {
|
|
156
|
+
currentFilters = [];
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
window.addEventListener('popstate', handler);
|
|
160
|
+
window.addEventListener('hashchange', handler);
|
|
161
|
+
return () => {
|
|
162
|
+
window.removeEventListener('popstate', handler);
|
|
163
|
+
window.removeEventListener('hashchange', handler);
|
|
164
|
+
};
|
|
165
|
+
});
|
|
130
166
|
}
|
|
131
167
|
|
|
132
168
|
function createLinkForSyncWithLocation(): string {
|
|
@@ -160,23 +196,33 @@ export function useTable<
|
|
|
160
196
|
get pageSize() { return pagination.pageSize ?? 10; },
|
|
161
197
|
get pageCount() { return Math.ceil(((query.data as GetListResult<TQueryFnData> | undefined)?.total ?? 0) / (pagination.pageSize ?? 10)); },
|
|
162
198
|
get clientData(): TQueryFnData[] {
|
|
163
|
-
if (paginationMode
|
|
199
|
+
if (paginationMode === 'server') return [];
|
|
164
200
|
const allData = ((query.data as GetListResult<TQueryFnData> | undefined)?.data ?? []) as TQueryFnData[];
|
|
165
|
-
const start = ((pagination.current ?? 1) - 1) * (pagination.pageSize ?? 10);
|
|
166
|
-
const end = start + (pagination.pageSize ?? 10);
|
|
167
201
|
let sorted = [...allData];
|
|
168
|
-
const activeSorters = sortersMode === 'off' ? effectiveSorters :
|
|
202
|
+
const activeSorters = sortersMode === 'off' ? effectiveSorters : [];
|
|
169
203
|
if (activeSorters.length > 0) {
|
|
170
|
-
const { field, order } = activeSorters[0];
|
|
171
204
|
sorted.sort((a, b) => {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
205
|
+
for (const { field, order } of activeSorters) {
|
|
206
|
+
const va = (a as Record<string, unknown>)[field];
|
|
207
|
+
const vb = (b as Record<string, unknown>)[field];
|
|
208
|
+
if (va == null && vb != null) return 1;
|
|
209
|
+
if (vb == null && va != null) return -1;
|
|
210
|
+
if (va == null && vb == null) continue;
|
|
211
|
+
|
|
212
|
+
let cmp = 0;
|
|
213
|
+
if (typeof va === 'number' && typeof vb === 'number') cmp = va - vb;
|
|
214
|
+
else if (va instanceof Date && vb instanceof Date) cmp = va.getTime() - vb.getTime();
|
|
215
|
+
else if (typeof va === 'string' && typeof vb === 'string' && !isNaN(Number(va)) && !isNaN(Number(vb))) cmp = Number(va) - Number(vb);
|
|
216
|
+
else cmp = String(va).localeCompare(String(vb));
|
|
217
|
+
|
|
218
|
+
if (cmp !== 0) return order === 'desc' ? -cmp : cmp;
|
|
219
|
+
}
|
|
220
|
+
return 0;
|
|
178
221
|
});
|
|
179
222
|
}
|
|
223
|
+
if (paginationMode === 'off') return sorted;
|
|
224
|
+
const start = ((pagination.current ?? 1) - 1) * (pagination.pageSize ?? 10);
|
|
225
|
+
const end = start + (pagination.pageSize ?? 10);
|
|
180
226
|
return sorted.slice(start, end);
|
|
181
227
|
},
|
|
182
228
|
};
|
|
@@ -200,7 +200,8 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
200
200
|
if (behavior === 'merge') {
|
|
201
201
|
const merged = [...this.currentFilters];
|
|
202
202
|
for (const nf of newFilters) {
|
|
203
|
-
|
|
203
|
+
if (!('field' in nf)) { merged.push(nf); continue; }
|
|
204
|
+
const idx = merged.findIndex(f => 'field' in f && f.field === nf.field && f.operator === nf.operator);
|
|
204
205
|
if (idx >= 0) {
|
|
205
206
|
if (nf.value === undefined || nf.value === null || nf.value === '') merged.splice(idx, 1);
|
|
206
207
|
else merged[idx] = nf;
|
package/src/theme.svelte.ts
CHANGED
|
@@ -134,13 +134,13 @@ export const builtinPresets: Record<string, ColorPreset> = $state({
|
|
|
134
134
|
'--chart-1': 'oklch(0.577 0.245 27.325)',
|
|
135
135
|
},
|
|
136
136
|
dark: {
|
|
137
|
-
'--primary': 'oklch(0.
|
|
138
|
-
'--primary-foreground': 'oklch(0.
|
|
139
|
-
'--ring': 'oklch(0.
|
|
140
|
-
'--sidebar-primary': 'oklch(0.
|
|
137
|
+
'--primary': 'oklch(0.685 0.22 27)',
|
|
138
|
+
'--primary-foreground': 'oklch(0.15 0.04 27)',
|
|
139
|
+
'--ring': 'oklch(0.55 0.18 27)',
|
|
140
|
+
'--sidebar-primary': 'oklch(0.685 0.22 27)',
|
|
141
141
|
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
142
|
-
'--sidebar-ring': 'oklch(0.
|
|
143
|
-
'--chart-1': 'oklch(0.
|
|
142
|
+
'--sidebar-ring': 'oklch(0.55 0.18 27)',
|
|
143
|
+
'--chart-1': 'oklch(0.685 0.22 27)',
|
|
144
144
|
},
|
|
145
145
|
},
|
|
146
146
|
orange: {
|
|
@@ -157,13 +157,13 @@ export const builtinPresets: Record<string, ColorPreset> = $state({
|
|
|
157
157
|
'--chart-1': 'oklch(0.646 0.222 41.116)',
|
|
158
158
|
},
|
|
159
159
|
dark: {
|
|
160
|
-
'--primary': 'oklch(0.
|
|
161
|
-
'--primary-foreground': 'oklch(0.
|
|
162
|
-
'--ring': 'oklch(0.
|
|
163
|
-
'--sidebar-primary': 'oklch(0.
|
|
160
|
+
'--primary': 'oklch(0.73 0.195 41)',
|
|
161
|
+
'--primary-foreground': 'oklch(0.15 0.04 41)',
|
|
162
|
+
'--ring': 'oklch(0.6 0.18 41)',
|
|
163
|
+
'--sidebar-primary': 'oklch(0.73 0.195 41)',
|
|
164
164
|
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
165
|
-
'--sidebar-ring': 'oklch(0.
|
|
166
|
-
'--chart-1': 'oklch(0.
|
|
165
|
+
'--sidebar-ring': 'oklch(0.6 0.18 41)',
|
|
166
|
+
'--chart-1': 'oklch(0.73 0.195 41)',
|
|
167
167
|
},
|
|
168
168
|
},
|
|
169
169
|
violet: {
|
|
@@ -180,13 +180,13 @@ export const builtinPresets: Record<string, ColorPreset> = $state({
|
|
|
180
180
|
'--chart-1': 'oklch(0.541 0.281 293.009)',
|
|
181
181
|
},
|
|
182
182
|
dark: {
|
|
183
|
-
'--primary': 'oklch(0.
|
|
184
|
-
'--primary-foreground': 'oklch(0.
|
|
185
|
-
'--ring': 'oklch(0.
|
|
186
|
-
'--sidebar-primary': 'oklch(0.
|
|
183
|
+
'--primary': 'oklch(0.65 0.25 293)',
|
|
184
|
+
'--primary-foreground': 'oklch(0.15 0.04 293)',
|
|
185
|
+
'--ring': 'oklch(0.52 0.2 293)',
|
|
186
|
+
'--sidebar-primary': 'oklch(0.65 0.25 293)',
|
|
187
187
|
'--sidebar-primary-foreground': 'oklch(0.985 0 0)',
|
|
188
|
-
'--sidebar-ring': 'oklch(0.
|
|
189
|
-
'--chart-1': 'oklch(0.
|
|
188
|
+
'--sidebar-ring': 'oklch(0.52 0.2 293)',
|
|
189
|
+
'--chart-1': 'oklch(0.65 0.25 293)',
|
|
190
190
|
},
|
|
191
191
|
},
|
|
192
192
|
});
|
|
@@ -194,11 +194,6 @@ export const builtinPresets: Record<string, ColorPreset> = $state({
|
|
|
194
194
|
/** Register a custom color preset. Overwrites any built-in preset with the same name. */
|
|
195
195
|
export function registerColorPreset(preset: ColorPreset): void {
|
|
196
196
|
builtinPresets[preset.name] = preset;
|
|
197
|
-
// Update colorThemes display list
|
|
198
|
-
const idx = colorThemes.findIndex(ct => ct.id === preset.name);
|
|
199
|
-
const entry = { id: preset.name as ColorTheme, label: preset.label, color: preset.color };
|
|
200
|
-
if (idx >= 0) colorThemes[idx] = entry;
|
|
201
|
-
else colorThemes.push(entry);
|
|
202
197
|
}
|
|
203
198
|
|
|
204
199
|
/** Resolve a preset by name or return the preset object directly. */
|
|
@@ -208,14 +203,21 @@ function resolvePreset(preset: ColorPreset | string): ColorPreset | undefined {
|
|
|
208
203
|
}
|
|
209
204
|
|
|
210
205
|
/** Apply a color preset's CSS variables based on current resolved theme. */
|
|
206
|
+
let activePresetVars: string[] = [];
|
|
207
|
+
|
|
211
208
|
function applyColorPreset(preset: ColorPreset): void {
|
|
212
209
|
if (typeof document === 'undefined') return;
|
|
213
210
|
const resolved = mode === 'system' ? getSystemPreference() : mode;
|
|
214
211
|
const vars = resolved === 'dark' ? preset.dark : preset.light;
|
|
215
212
|
const root = document.documentElement;
|
|
213
|
+
for (const key of activePresetVars) {
|
|
214
|
+
root.style.removeProperty(key);
|
|
215
|
+
}
|
|
216
|
+
activePresetVars = [];
|
|
216
217
|
for (const [key, value] of Object.entries(vars)) {
|
|
217
218
|
const cssVar = key.startsWith('--') ? key : `--${key}`;
|
|
218
219
|
root.style.setProperty(cssVar, value);
|
|
220
|
+
activePresetVars.push(cssVar);
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
|
|
@@ -292,12 +294,16 @@ export function clearCssOverrides(keys?: string[]): void {
|
|
|
292
294
|
}
|
|
293
295
|
|
|
294
296
|
// ── Color themes (display metadata, derived from presets) ──
|
|
295
|
-
|
|
297
|
+
const _colorThemesCache: { id: ColorTheme | string; label: string; color: string }[] = $derived(Object.values(builtinPresets).map(p => ({
|
|
296
298
|
id: p.name as ColorTheme,
|
|
297
299
|
label: p.label,
|
|
298
300
|
color: p.color,
|
|
299
301
|
})));
|
|
300
302
|
|
|
303
|
+
export function getColorThemes() {
|
|
304
|
+
return _colorThemesCache;
|
|
305
|
+
}
|
|
306
|
+
|
|
301
307
|
// ── Dark/Light mode ──────────────────────────────────────
|
|
302
308
|
|
|
303
309
|
function getStoredTheme(): ThemeMode {
|
package/src/toast.svelte.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type ToastType = 'success' | 'error' | 'info' | 'warning' | 'undoable';
|
|
|
2
2
|
|
|
3
3
|
export interface ToastItem {
|
|
4
4
|
id: number;
|
|
5
|
+
key?: string;
|
|
5
6
|
type: ToastType;
|
|
6
7
|
message: string;
|
|
7
8
|
duration?: number;
|
|
@@ -10,18 +11,16 @@ export interface ToastItem {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
let nextId = 0;
|
|
14
|
+
const undoableTimers = new Map<number, ReturnType<typeof setTimeout>>();
|
|
13
15
|
|
|
14
|
-
// Queue for simple toasts (consumed by UI Toast bridge)
|
|
15
16
|
let queue = $state<ToastItem[]>([]);
|
|
16
17
|
export function getToastQueue() { return queue; }
|
|
17
18
|
export function consumeToastQueue() { queue = []; }
|
|
18
19
|
|
|
19
|
-
// Promise Queue
|
|
20
20
|
let promiseQueue = $state<{ id: number; promise: Promise<any>; opts: { loading: string; success: string; error: string } }[]>([]);
|
|
21
21
|
export function getPromiseQueue() { return promiseQueue; }
|
|
22
22
|
export function consumePromiseQueue() { promiseQueue = []; }
|
|
23
23
|
|
|
24
|
-
// Legacy compatibility — kept for existing consumers (Undoable)
|
|
25
24
|
let toasts = $state<ToastItem[]>([]);
|
|
26
25
|
export function getToasts(): ToastItem[] { return toasts; }
|
|
27
26
|
|
|
@@ -29,29 +28,41 @@ export function addToast(
|
|
|
29
28
|
type: ToastType,
|
|
30
29
|
message: string,
|
|
31
30
|
duration = 3000,
|
|
32
|
-
options?: { onUndo?: () => void, onTimeout?: () => void }
|
|
31
|
+
options?: { key?: string, onUndo?: () => void, onTimeout?: () => void }
|
|
33
32
|
): void {
|
|
33
|
+
if (options?.key) {
|
|
34
|
+
if (queue.some(q => q.key === options.key) || toasts.some(t => t.key === options.key)) return;
|
|
35
|
+
}
|
|
34
36
|
if (type !== 'undoable') {
|
|
35
|
-
queue.push({ id: nextId++, type, message, duration });
|
|
37
|
+
queue.push({ id: nextId++, type, message, duration, key: options?.key });
|
|
36
38
|
return;
|
|
37
39
|
}
|
|
38
40
|
const id = nextId++;
|
|
39
41
|
toasts = [...toasts, { id, type, message, duration, ...options }];
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
undoableTimers.delete(id);
|
|
44
|
+
options?.onTimeout?.();
|
|
45
|
+
removeToast(id);
|
|
46
|
+
}, duration);
|
|
47
|
+
undoableTimers.set(id, timer);
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
export function removeToast(id: number): void {
|
|
51
|
+
const timer = undoableTimers.get(id);
|
|
52
|
+
if (timer) {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
undoableTimers.delete(id);
|
|
55
|
+
}
|
|
43
56
|
toasts = toasts.filter(t => t.id !== id);
|
|
44
57
|
}
|
|
45
58
|
|
|
46
|
-
// Convenience methods — backward compatible
|
|
47
59
|
export const toast = {
|
|
48
|
-
success: (msg: string, duration?: number) => addToast('success', msg, duration),
|
|
49
|
-
error: (msg: string, duration?: number) => addToast('error', msg, duration ?? 5000),
|
|
50
|
-
info: (msg: string, duration?: number) => addToast('info', msg, duration),
|
|
51
|
-
warning: (msg: string, duration?: number) => addToast('warning', msg, duration ?? 4000),
|
|
60
|
+
success: (msg: string, duration?: number, opts?: { key?: string }) => addToast('success', msg, duration, opts),
|
|
61
|
+
error: (msg: string, duration?: number, opts?: { key?: string }) => addToast('error', msg, duration ?? 5000, opts),
|
|
62
|
+
info: (msg: string, duration?: number, opts?: { key?: string }) => addToast('info', msg, duration, opts),
|
|
63
|
+
warning: (msg: string, duration?: number, opts?: { key?: string }) => addToast('warning', msg, duration ?? 4000, opts),
|
|
52
64
|
undoable: (msg: string, duration: number, onUndo: () => void, onTimeout: () => void) =>
|
|
53
65
|
addToast('undoable', msg, duration, { onUndo, onTimeout }),
|
|
54
|
-
/** Promise toast — shows loading → success/error automatically */
|
|
55
66
|
promise: <T>(promise: Promise<T>, opts: { loading: string; success: string; error: string }) => {
|
|
56
67
|
promiseQueue.push({ id: nextId++, promise, opts });
|
|
57
68
|
return promise;
|
package/src/types.ts
CHANGED
|
@@ -48,12 +48,14 @@ export type CrudOperator =
|
|
|
48
48
|
| 'null' | 'nnull'
|
|
49
49
|
| 'between' | 'nbetween';
|
|
50
50
|
|
|
51
|
-
export interface
|
|
51
|
+
export interface FieldFilter {
|
|
52
52
|
field: string;
|
|
53
53
|
operator: CrudOperator;
|
|
54
54
|
value: unknown;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export type Filter = FieldFilter | LogicalFilter;
|
|
58
|
+
|
|
57
59
|
export interface LogicalFilter {
|
|
58
60
|
operator: 'or' | 'and';
|
|
59
61
|
value: Filter[];
|