@svadmin/core 0.31.0 → 0.32.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 +3 -3
- package/src/auth-hooks.svelte.ts +42 -25
- package/src/context.svelte.ts +185 -46
- package/src/data-transfer.svelte.ts +9 -62
- package/src/export-format.ts +35 -31
- package/src/form-hooks.svelte.ts +322 -58
- package/src/helpers.ts +11 -7
- package/src/hook-utils.svelte.ts +13 -12
- package/src/hooks.svelte.ts +29 -23
- package/src/http.ts +114 -33
- package/src/i18n.svelte.ts +181 -35
- package/src/index.ts +7 -6
- package/src/mutation-hooks.svelte.ts +27 -16
- package/src/query-hooks.svelte.ts +16 -13
- package/src/router.ts +28 -8
- package/src/routing-hooks.svelte.ts +13 -18
- package/src/table-hooks.svelte.ts +5 -3
- package/src/table-state.svelte.ts +6 -3
- package/src/task-hooks.svelte.ts +27 -14
- package/src/theme.svelte.ts +248 -154
- package/src/toast.svelte.ts +1 -2
- package/src/types.ts +26 -4
- package/src/unsaved-changes.svelte.ts +4 -38
- package/src/url-sync.ts +47 -8
- package/src/useParsed.svelte.ts +16 -11
- package/src/utility-hooks.svelte.ts +8 -7
package/src/url-sync.ts
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
import type { Filter } from './types';
|
|
2
|
-
import {
|
|
2
|
+
import { captureAdminContext } from './context.svelte';
|
|
3
|
+
import type { AdminContextAccessor } from './context.svelte';
|
|
3
4
|
import { getAdminOptions } from './options.svelte';
|
|
4
5
|
|
|
6
|
+
const CRUD_OPERATORS = new Set([
|
|
7
|
+
'eq', 'ne', 'lt', 'gt', 'lte', 'gte',
|
|
8
|
+
'contains', 'ncontains', 'startswith', 'endswith',
|
|
9
|
+
'in', 'nin', 'null', 'nnull', 'between', 'nbetween',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isFilter(value: unknown, depth: number, budget: { remaining: number }): value is Filter {
|
|
17
|
+
if (depth > 20 || budget.remaining-- <= 0 || !isRecord(value)) return false;
|
|
18
|
+
|
|
19
|
+
if (typeof value.field === 'string') {
|
|
20
|
+
return value.field.trim().length > 0
|
|
21
|
+
&& typeof value.operator === 'string'
|
|
22
|
+
&& CRUD_OPERATORS.has(value.operator)
|
|
23
|
+
&& 'value' in value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (value.operator === 'and' || value.operator === 'or')
|
|
27
|
+
&& Array.isArray(value.value)
|
|
28
|
+
&& value.value.every((entry) => isFilter(entry, depth + 1, budget));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseFilters(value: string): Filter[] | undefined {
|
|
32
|
+
try {
|
|
33
|
+
const parsed: unknown = JSON.parse(value);
|
|
34
|
+
if (!Array.isArray(parsed)) return undefined;
|
|
35
|
+
const budget = { remaining: 200 };
|
|
36
|
+
return parsed.every((entry) => isFilter(entry, 0, budget)) ? parsed : undefined;
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
5
42
|
export interface URLState {
|
|
6
43
|
page?: number;
|
|
7
44
|
pageSize?: number;
|
|
@@ -11,9 +48,9 @@ export interface URLState {
|
|
|
11
48
|
filters?: Filter[];
|
|
12
49
|
}
|
|
13
50
|
|
|
14
|
-
export function readURLState(): URLState {
|
|
51
|
+
export function readURLState(adminContext: AdminContextAccessor = captureAdminContext()): URLState {
|
|
15
52
|
if (typeof window === 'undefined') return {};
|
|
16
|
-
const rp =
|
|
53
|
+
const rp = adminContext.routerProvider;
|
|
17
54
|
if (!rp) return {};
|
|
18
55
|
|
|
19
56
|
const { params } = rp.parse();
|
|
@@ -36,17 +73,19 @@ export function readURLState(): URLState {
|
|
|
36
73
|
|
|
37
74
|
const filtersRaw = params['filters'];
|
|
38
75
|
if (filtersRaw) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} catch { /* ignore invalid json */ }
|
|
76
|
+
const filters = parseFilters(filtersRaw);
|
|
77
|
+
if (filters) state.filters = filters;
|
|
42
78
|
}
|
|
43
79
|
|
|
44
80
|
return state;
|
|
45
81
|
}
|
|
46
82
|
|
|
47
|
-
export function writeURLState(
|
|
83
|
+
export function writeURLState(
|
|
84
|
+
state: URLState,
|
|
85
|
+
adminContext: AdminContextAccessor = captureAdminContext(),
|
|
86
|
+
): void {
|
|
48
87
|
if (typeof window === 'undefined') return;
|
|
49
|
-
const rp =
|
|
88
|
+
const rp = adminContext.routerProvider;
|
|
50
89
|
if (!rp) return;
|
|
51
90
|
|
|
52
91
|
const current = rp.parse();
|
package/src/useParsed.svelte.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
/* eslint-disable svelte/prefer-svelte-reactivity */
|
|
2
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
2
|
// useParsed — parse current URL hash into structured route info
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { registerRouterSync } from './router';
|
|
5
|
+
import { captureAdminContext } from './context.svelte';
|
|
7
6
|
|
|
8
7
|
interface ParsedRoute {
|
|
9
8
|
resource?: string;
|
|
@@ -16,13 +15,13 @@ interface ParsedRoute {
|
|
|
16
15
|
parentParams: Record<string, string>;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
let
|
|
18
|
+
let routeVersion = $state(0);
|
|
20
19
|
|
|
21
20
|
export function resetGlobalPath(): void {
|
|
22
|
-
|
|
21
|
+
routeVersion++;
|
|
23
22
|
}
|
|
24
23
|
export function syncGlobalPath(): void {
|
|
25
|
-
|
|
24
|
+
routeVersion++;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/** 简单的英文名词单数化,处理常见复数模式(-ies, -ses, -es, -s) */
|
|
@@ -37,11 +36,10 @@ function singularize(word: string): string {
|
|
|
37
36
|
registerRouterSync(syncGlobalPath);
|
|
38
37
|
|
|
39
38
|
if (typeof window !== 'undefined') {
|
|
40
|
-
globalPath = currentPath();
|
|
41
39
|
if (!(window as any).__svadminParsedInit) {
|
|
42
40
|
(window as any).__svadminParsedInit = true;
|
|
43
|
-
window.addEventListener('hashchange',
|
|
44
|
-
window.addEventListener('popstate',
|
|
41
|
+
window.addEventListener('hashchange', syncGlobalPath);
|
|
42
|
+
window.addEventListener('popstate', syncGlobalPath);
|
|
45
43
|
}
|
|
46
44
|
}
|
|
47
45
|
|
|
@@ -58,10 +56,17 @@ if (typeof window !== 'undefined') {
|
|
|
58
56
|
* // parsed.parentParams contains nested route params such as teamId
|
|
59
57
|
*/
|
|
60
58
|
export function useParsed(): ParsedRoute {
|
|
61
|
-
const
|
|
59
|
+
const adminContext = captureAdminContext();
|
|
60
|
+
const resources = $derived(adminContext.resources);
|
|
61
|
+
const activePath = $derived.by(() => {
|
|
62
|
+
// Any router navigation increments routeVersion. Each tree then asks its
|
|
63
|
+
// captured provider for its own path instead of consuming a global router.
|
|
64
|
+
void routeVersion;
|
|
65
|
+
return adminContext.currentPath();
|
|
66
|
+
});
|
|
62
67
|
|
|
63
68
|
const parsed = $derived.by(() => {
|
|
64
|
-
const p =
|
|
69
|
+
const p = activePath;
|
|
65
70
|
const result: ParsedRoute = { params: {}, parentParams: {} };
|
|
66
71
|
|
|
67
72
|
// Parse query params from hash
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
import { useForm, type UseFormOptions, type UseFormReturn } from './form-hooks.svelte';
|
|
1
|
+
import { useForm, type UseFormOptions } from './form-hooks.svelte';
|
|
3
2
|
import { useQueryClient } from '@tanstack/svelte-query';
|
|
4
3
|
import { useParsed } from './useParsed.svelte';
|
|
5
|
-
import {
|
|
4
|
+
import { captureAdminContext } from './context.svelte';
|
|
6
5
|
import { getAdminOptions } from './options.svelte';
|
|
7
6
|
import { t } from './i18n.svelte';
|
|
8
7
|
import type { BaseRecord, HttpError, Filter, KnownResources, ResourceDefinition } from './types';
|
|
9
|
-
import { useList
|
|
8
|
+
import { useList } from './query-hooks.svelte';
|
|
10
9
|
import { toast } from './toast.svelte';
|
|
11
10
|
import type { UseSelectOptions } from './hooks.svelte';
|
|
12
11
|
import { useSelect as useSelectImpl } from './hooks.svelte';
|
|
@@ -89,11 +88,12 @@ export interface MenuConfig {
|
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
export function useMenu() {
|
|
91
|
+
const adminContext = captureAdminContext();
|
|
92
92
|
const parsed = useParsed();
|
|
93
93
|
|
|
94
94
|
const menuItems = $derived.by<MenuConfig[]>(() => {
|
|
95
95
|
const adminOptions = getAdminOptions();
|
|
96
|
-
const resources =
|
|
96
|
+
const resources = adminContext.resources;
|
|
97
97
|
let items: MenuConfig[] = resources
|
|
98
98
|
.filter((r: ResourceDefinition) => r.showInMenu !== false)
|
|
99
99
|
.map((r: ResourceDefinition): MenuConfig => ({
|
|
@@ -125,6 +125,7 @@ export interface BreadcrumbItem {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
export function useBreadcrumb() {
|
|
128
|
+
const adminContext = captureAdminContext();
|
|
128
129
|
const parsed = useParsed();
|
|
129
130
|
|
|
130
131
|
const items = $derived.by<BreadcrumbItem[]>(() => {
|
|
@@ -132,7 +133,7 @@ export function useBreadcrumb() {
|
|
|
132
133
|
if (!parsed.resource) return list;
|
|
133
134
|
|
|
134
135
|
try {
|
|
135
|
-
const res = getResource(parsed.resource);
|
|
136
|
+
const res = adminContext.getResource(parsed.resource);
|
|
136
137
|
const chain: { label: string; link: string; icon?: string }[] = [];
|
|
137
138
|
let current: typeof res | undefined = res;
|
|
138
139
|
const visited = new Set<string>();
|
|
@@ -140,7 +141,7 @@ export function useBreadcrumb() {
|
|
|
140
141
|
if (visited.has(current.name)) break;
|
|
141
142
|
visited.add(current.name);
|
|
142
143
|
chain.unshift({ label: current.label || current.name, link: `/${current.name}`, icon: current.icon });
|
|
143
|
-
current = current.parentName ? getResource(current.parentName) : undefined;
|
|
144
|
+
current = current.parentName ? adminContext.getResource(current.parentName) : undefined;
|
|
144
145
|
}
|
|
145
146
|
list.push(...chain);
|
|
146
147
|
|