@svadmin/core 0.0.3 → 0.3.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 +4 -1
- package/src/audit.ts +37 -0
- package/src/auth-hooks.svelte.ts +293 -0
- package/src/auth-hooks.test.ts +205 -0
- package/src/context.ts +77 -15
- package/src/data-transfer.test.ts +118 -0
- package/src/data-transfer.ts +148 -70
- package/src/form-hooks.svelte.ts +266 -0
- package/src/helpers.ts +157 -0
- package/src/hook-utils.svelte.ts +121 -0
- package/src/hooks.svelte.ts +278 -496
- package/src/http-error.test.ts +85 -0
- package/src/i18n.svelte.ts +24 -4
- package/src/i18n.test.ts +90 -0
- package/src/index.ts +51 -8
- package/src/inferencer-openapi.test.ts +192 -0
- package/src/inferencer-openapi.ts +290 -0
- package/src/inferencer.test.ts +189 -0
- package/src/inferencer.ts +247 -0
- package/src/live-sse.ts +146 -0
- package/src/live-websocket.ts +141 -0
- package/src/live.test.ts +134 -0
- package/src/live.ts +60 -8
- package/src/mutation-hooks.svelte.ts +301 -0
- package/src/options.ts +95 -0
- package/src/permissions.test.ts +55 -0
- package/src/query-hooks.svelte.ts +213 -0
- package/src/query-keys.ts +22 -0
- package/src/resource-types.test.ts +79 -0
- package/src/router-provider.test.ts +71 -0
- package/src/router-provider.ts +100 -0
- package/src/router.ts +28 -4
- package/src/routing-hooks.svelte.ts +99 -0
- package/src/table-hooks.svelte.ts +178 -0
- package/src/toast.svelte.ts +8 -4
- package/src/types.ts +124 -47
- package/src/unsaved-changes.svelte.ts +62 -0
- package/src/url-sync.ts +15 -0
- package/src/useParsed.ts +110 -0
- package/src/useStepsForm.svelte.ts +71 -0
- package/src/utility-hooks.svelte.ts +161 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useParsed } from './useParsed';
|
|
2
|
+
import { useList } from './query-hooks.svelte';
|
|
3
|
+
import { readURLState, writeURLState } from './url-sync';
|
|
4
|
+
import type { Pagination, Sort, Filter, BaseRecord, HttpError, KnownResources } from './types';
|
|
5
|
+
import type { UseListOptions } from './query-hooks.svelte';
|
|
6
|
+
|
|
7
|
+
export type FilterSetMode = 'merge' | 'replace';
|
|
8
|
+
|
|
9
|
+
export interface UseTableOptions<TQueryFnData extends BaseRecord = BaseRecord, TError = HttpError, TSearchVariables = Record<string, unknown>> extends Omit<UseListOptions<TQueryFnData, TError>, 'pagination' | 'sorters' | 'filters'> {
|
|
10
|
+
resource?: KnownResources;
|
|
11
|
+
pagination?: Pagination;
|
|
12
|
+
sorters?: {
|
|
13
|
+
initial?: Sort[];
|
|
14
|
+
permanent?: Sort[];
|
|
15
|
+
mode?: 'off' | 'server';
|
|
16
|
+
};
|
|
17
|
+
filters?: {
|
|
18
|
+
initial?: Filter[];
|
|
19
|
+
permanent?: Filter[];
|
|
20
|
+
mode?: 'off' | 'server';
|
|
21
|
+
defaultBehavior?: FilterSetMode;
|
|
22
|
+
};
|
|
23
|
+
syncWithLocation?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useTable<
|
|
27
|
+
TQueryFnData extends BaseRecord = BaseRecord,
|
|
28
|
+
TError = HttpError,
|
|
29
|
+
TSearchVariables = Record<string, unknown>
|
|
30
|
+
>(options: UseTableOptions<TQueryFnData, TError, TSearchVariables> = {}) {
|
|
31
|
+
const parsed = useParsed();
|
|
32
|
+
const resource = options.resource ?? parsed.resource ?? '';
|
|
33
|
+
const { meta, syncWithLocation = false, dataProviderName, pagination: _p, sorters: _s, filters: _f, ...restOptions } = options;
|
|
34
|
+
|
|
35
|
+
const paginationMode = options.pagination?.mode ?? 'server';
|
|
36
|
+
const sortersMode = options.sorters?.mode ?? 'server';
|
|
37
|
+
const filtersMode = options.filters?.mode ?? 'server';
|
|
38
|
+
const filterDefaultBehavior = options.filters?.defaultBehavior ?? 'replace';
|
|
39
|
+
|
|
40
|
+
const permanentSorters = options.sorters?.permanent ?? [];
|
|
41
|
+
const permanentFilters = options.filters?.permanent ?? [];
|
|
42
|
+
|
|
43
|
+
let initPagination = options.pagination ?? { current: 1, pageSize: 10 };
|
|
44
|
+
let initSorters = options.sorters?.initial ?? [];
|
|
45
|
+
let initFilters = options.filters?.initial ?? [];
|
|
46
|
+
|
|
47
|
+
if (syncWithLocation && typeof window !== 'undefined') {
|
|
48
|
+
const urlState = readURLState();
|
|
49
|
+
if (urlState.page || urlState.pageSize) {
|
|
50
|
+
initPagination = {
|
|
51
|
+
current: urlState.page ?? initPagination.current,
|
|
52
|
+
pageSize: urlState.pageSize ?? initPagination.pageSize,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (urlState.sortField) {
|
|
56
|
+
initSorters = [{ field: urlState.sortField, order: (urlState.sortOrder as 'asc' | 'desc') ?? 'asc' }];
|
|
57
|
+
}
|
|
58
|
+
if (urlState.filters) {
|
|
59
|
+
initFilters = urlState.filters;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let pagination = $state<Pagination>(initPagination);
|
|
64
|
+
let currentSorters = $state<Sort[]>(initSorters);
|
|
65
|
+
let currentFilters = $state<Filter[]>(initFilters);
|
|
66
|
+
|
|
67
|
+
const effectiveSorters = $derived([...currentSorters, ...permanentSorters]);
|
|
68
|
+
const effectiveFilters = $derived([...currentFilters, ...permanentFilters]);
|
|
69
|
+
|
|
70
|
+
const querySorters = $derived(sortersMode === 'server' ? effectiveSorters : []);
|
|
71
|
+
const queryFilters = $derived(filtersMode === 'server' ? effectiveFilters : []);
|
|
72
|
+
const queryPagination = paginationMode === 'server'
|
|
73
|
+
? pagination
|
|
74
|
+
: { current: 1, pageSize: 999999, mode: paginationMode as 'client' | 'off' };
|
|
75
|
+
|
|
76
|
+
const tableQueryInfo = useList<TQueryFnData, TError>({
|
|
77
|
+
resource,
|
|
78
|
+
pagination: queryPagination,
|
|
79
|
+
sorters: querySorters,
|
|
80
|
+
filters: queryFilters,
|
|
81
|
+
meta,
|
|
82
|
+
dataProviderName,
|
|
83
|
+
...restOptions
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const query = tableQueryInfo.query;
|
|
87
|
+
const overtime = tableQueryInfo.overtime;
|
|
88
|
+
|
|
89
|
+
function setSorters(newSorters: Sort[]) { currentSorters = newSorters; }
|
|
90
|
+
function setFilters(newFilters: Filter[], mode?: FilterSetMode) {
|
|
91
|
+
const behavior = mode ?? filterDefaultBehavior;
|
|
92
|
+
if (behavior === 'merge') {
|
|
93
|
+
const merged = [...currentFilters];
|
|
94
|
+
for (const nf of newFilters) {
|
|
95
|
+
const idx = merged.findIndex(f => f.field === nf.field && f.operator === nf.operator);
|
|
96
|
+
if (idx >= 0) {
|
|
97
|
+
if (nf.value === undefined || nf.value === null || nf.value === '') merged.splice(idx, 1);
|
|
98
|
+
else merged[idx] = nf;
|
|
99
|
+
} else if (nf.value !== undefined && nf.value !== null && nf.value !== '') {
|
|
100
|
+
merged.push(nf);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
currentFilters = merged;
|
|
104
|
+
} else {
|
|
105
|
+
currentFilters = newFilters;
|
|
106
|
+
}
|
|
107
|
+
pagination = { ...pagination, current: 1 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setPage(page: number) { pagination = { ...pagination, current: page }; }
|
|
111
|
+
function setPageSize(size: number) { pagination = { ...pagination, pageSize: size, current: 1 }; }
|
|
112
|
+
|
|
113
|
+
if (syncWithLocation && typeof window !== 'undefined') {
|
|
114
|
+
$effect(() => {
|
|
115
|
+
writeURLState({
|
|
116
|
+
page: pagination.current,
|
|
117
|
+
pageSize: pagination.pageSize,
|
|
118
|
+
sortField: effectiveSorters[0]?.field,
|
|
119
|
+
sortOrder: effectiveSorters[0]?.order,
|
|
120
|
+
filters: effectiveFilters,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function createLinkForSyncWithLocation(): string {
|
|
126
|
+
const params = new URLSearchParams();
|
|
127
|
+
if (pagination.current) params.set('page', String(pagination.current));
|
|
128
|
+
if (pagination.pageSize) params.set('pageSize', String(pagination.pageSize));
|
|
129
|
+
if (effectiveSorters[0]) {
|
|
130
|
+
params.set('sortField', effectiveSorters[0].field);
|
|
131
|
+
params.set('sortOrder', effectiveSorters[0].order);
|
|
132
|
+
}
|
|
133
|
+
if (effectiveFilters.length > 0) {
|
|
134
|
+
params.set('filters', JSON.stringify(effectiveFilters));
|
|
135
|
+
}
|
|
136
|
+
return `?${params.toString()}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
query,
|
|
141
|
+
tableQuery: query,
|
|
142
|
+
get overtime() { return overtime; },
|
|
143
|
+
get pagination() { return pagination; },
|
|
144
|
+
get sorters() { return currentSorters; },
|
|
145
|
+
get filters() { return currentFilters; },
|
|
146
|
+
setSorters, setFilters, setPage, setPageSize,
|
|
147
|
+
setCurrentPage: setPage,
|
|
148
|
+
createLinkForSyncWithLocation,
|
|
149
|
+
get totalPages() {
|
|
150
|
+
// ts-expect-error reactive access
|
|
151
|
+
const total = (query as any).data?.total ?? 0;
|
|
152
|
+
return Math.ceil(total / (pagination.pageSize ?? 10));
|
|
153
|
+
},
|
|
154
|
+
get current() { return pagination.current ?? 1; },
|
|
155
|
+
get pageSize() { return pagination.pageSize ?? 10; },
|
|
156
|
+
get pageCount() { return Math.ceil(((query as any).data?.total ?? 0) / (pagination.pageSize ?? 10)); },
|
|
157
|
+
get clientData(): TQueryFnData[] {
|
|
158
|
+
if (paginationMode !== 'client') return [];
|
|
159
|
+
const allData = ((query as any).data?.data ?? []) as TQueryFnData[];
|
|
160
|
+
const start = ((pagination.current ?? 1) - 1) * (pagination.pageSize ?? 10);
|
|
161
|
+
const end = start + (pagination.pageSize ?? 10);
|
|
162
|
+
let sorted = [...allData];
|
|
163
|
+
const activeSorters = sortersMode === 'off' ? effectiveSorters : currentSorters;
|
|
164
|
+
if (activeSorters.length > 0) {
|
|
165
|
+
const { field, order } = activeSorters[0];
|
|
166
|
+
sorted.sort((a, b) => {
|
|
167
|
+
const va = (a as Record<string, unknown>)[field];
|
|
168
|
+
const vb = (b as Record<string, unknown>)[field];
|
|
169
|
+
if (va == null) return 1;
|
|
170
|
+
if (vb == null) return -1;
|
|
171
|
+
const cmp = va < vb ? -1 : va > vb ? 1 : 0;
|
|
172
|
+
return order === 'desc' ? -cmp : cmp;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return sorted.slice(start, end);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
package/src/toast.svelte.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// Toast notification system — Svelte 5 runes-based
|
|
2
2
|
|
|
3
|
-
export type ToastType = 'success' | 'error' | 'info' | 'warning';
|
|
3
|
+
export type ToastType = 'success' | 'error' | 'info' | 'warning' | 'undoable';
|
|
4
4
|
|
|
5
5
|
export interface Toast {
|
|
6
6
|
id: number;
|
|
7
7
|
type: ToastType;
|
|
8
8
|
message: string;
|
|
9
9
|
duration: number;
|
|
10
|
+
onUndo?: () => void;
|
|
11
|
+
onTimeout?: () => void;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
let nextId = 0;
|
|
@@ -16,10 +18,10 @@ export function getToasts(): Toast[] {
|
|
|
16
18
|
return toasts;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export function addToast(type: ToastType, message: string, duration = 3000): void {
|
|
21
|
+
export function addToast(type: ToastType, message: string, duration = 3000, options?: { onUndo?: () => void, onTimeout?: () => void }): void {
|
|
20
22
|
const id = nextId++;
|
|
21
|
-
toasts = [...toasts, { id, type, message, duration }];
|
|
22
|
-
if (duration > 0) {
|
|
23
|
+
toasts = [...toasts, { id, type, message, duration, ...options }];
|
|
24
|
+
if (duration > 0 && type !== 'undoable') {
|
|
23
25
|
setTimeout(() => removeToast(id), duration);
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -34,4 +36,6 @@ export const toast = {
|
|
|
34
36
|
error: (msg: string, duration?: number) => addToast('error', msg, duration ?? 5000),
|
|
35
37
|
info: (msg: string, duration?: number) => addToast('info', msg, duration),
|
|
36
38
|
warning: (msg: string, duration?: number) => addToast('warning', msg, duration ?? 4000),
|
|
39
|
+
undoable: (msg: string, duration: number, onUndo: () => void, onTimeout: () => void) =>
|
|
40
|
+
addToast('undoable', msg, duration, { onUndo, onTimeout }),
|
|
37
41
|
};
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
// Core type definitions —
|
|
1
|
+
// Core type definitions — DataProvider + AuthProvider + Providers
|
|
2
|
+
|
|
3
|
+
// ─── HttpError ─────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export type ValidationErrors = Record<string, string | string[]>;
|
|
6
|
+
|
|
7
|
+
export class HttpError extends Error {
|
|
8
|
+
statusCode: number;
|
|
9
|
+
errors?: ValidationErrors;
|
|
10
|
+
|
|
11
|
+
constructor(message: string, statusCode: number, errors?: ValidationErrors) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'HttpError';
|
|
14
|
+
this.statusCode = statusCode;
|
|
15
|
+
this.errors = errors;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class UndoError extends Error {
|
|
20
|
+
constructor() {
|
|
21
|
+
super('Mutation undone');
|
|
22
|
+
this.name = 'UndoError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Base Types ───────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export type BaseRecord = Record<string, unknown>;
|
|
2
29
|
|
|
3
30
|
// ─── DataProvider ─────────────────────────────────────────────
|
|
4
31
|
|
|
@@ -13,12 +40,25 @@ export interface Sort {
|
|
|
13
40
|
order: 'asc' | 'desc';
|
|
14
41
|
}
|
|
15
42
|
|
|
43
|
+
export type CrudOperator =
|
|
44
|
+
| 'eq' | 'ne' | 'lt' | 'gt' | 'lte' | 'gte'
|
|
45
|
+
| 'contains' | 'ncontains'
|
|
46
|
+
| 'startswith' | 'endswith'
|
|
47
|
+
| 'in' | 'nin'
|
|
48
|
+
| 'null' | 'nnull'
|
|
49
|
+
| 'between' | 'nbetween';
|
|
50
|
+
|
|
16
51
|
export interface Filter {
|
|
17
52
|
field: string;
|
|
18
|
-
operator:
|
|
53
|
+
operator: CrudOperator;
|
|
19
54
|
value: unknown;
|
|
20
55
|
}
|
|
21
56
|
|
|
57
|
+
export interface LogicalFilter {
|
|
58
|
+
operator: 'or' | 'and';
|
|
59
|
+
value: Filter[];
|
|
60
|
+
}
|
|
61
|
+
|
|
22
62
|
export interface GetListParams {
|
|
23
63
|
resource: string;
|
|
24
64
|
pagination?: Pagination;
|
|
@@ -27,8 +67,8 @@ export interface GetListParams {
|
|
|
27
67
|
meta?: Record<string, unknown>;
|
|
28
68
|
}
|
|
29
69
|
|
|
30
|
-
export interface GetListResult<
|
|
31
|
-
data:
|
|
70
|
+
export interface GetListResult<TData extends BaseRecord = BaseRecord> {
|
|
71
|
+
data: TData[];
|
|
32
72
|
total: number;
|
|
33
73
|
}
|
|
34
74
|
|
|
@@ -38,8 +78,8 @@ export interface GetOneParams {
|
|
|
38
78
|
meta?: Record<string, unknown>;
|
|
39
79
|
}
|
|
40
80
|
|
|
41
|
-
export interface GetOneResult<
|
|
42
|
-
data:
|
|
81
|
+
export interface GetOneResult<TData extends BaseRecord = BaseRecord> {
|
|
82
|
+
data: TData;
|
|
43
83
|
}
|
|
44
84
|
|
|
45
85
|
export interface GetManyParams {
|
|
@@ -48,78 +88,78 @@ export interface GetManyParams {
|
|
|
48
88
|
meta?: Record<string, unknown>;
|
|
49
89
|
}
|
|
50
90
|
|
|
51
|
-
export interface GetManyResult<
|
|
52
|
-
data:
|
|
91
|
+
export interface GetManyResult<TData extends BaseRecord = BaseRecord> {
|
|
92
|
+
data: TData[];
|
|
53
93
|
}
|
|
54
94
|
|
|
55
|
-
export interface CreateParams {
|
|
95
|
+
export interface CreateParams<TVariables = unknown> {
|
|
56
96
|
resource: string;
|
|
57
|
-
variables:
|
|
97
|
+
variables: TVariables;
|
|
58
98
|
meta?: Record<string, unknown>;
|
|
59
99
|
}
|
|
60
100
|
|
|
61
|
-
export interface CreateResult<
|
|
62
|
-
data:
|
|
101
|
+
export interface CreateResult<TData extends BaseRecord = BaseRecord> {
|
|
102
|
+
data: TData;
|
|
63
103
|
}
|
|
64
104
|
|
|
65
|
-
export interface CreateManyParams {
|
|
105
|
+
export interface CreateManyParams<TVariables = unknown> {
|
|
66
106
|
resource: string;
|
|
67
|
-
variables:
|
|
107
|
+
variables: TVariables[];
|
|
68
108
|
meta?: Record<string, unknown>;
|
|
69
109
|
}
|
|
70
110
|
|
|
71
|
-
export interface CreateManyResult<
|
|
72
|
-
data:
|
|
111
|
+
export interface CreateManyResult<TData extends BaseRecord = BaseRecord> {
|
|
112
|
+
data: TData[];
|
|
73
113
|
}
|
|
74
114
|
|
|
75
|
-
export interface UpdateParams {
|
|
115
|
+
export interface UpdateParams<TVariables = unknown> {
|
|
76
116
|
resource: string;
|
|
77
117
|
id: string | number;
|
|
78
|
-
variables:
|
|
118
|
+
variables: TVariables;
|
|
79
119
|
meta?: Record<string, unknown>;
|
|
80
120
|
}
|
|
81
121
|
|
|
82
|
-
export interface UpdateResult<
|
|
83
|
-
data:
|
|
122
|
+
export interface UpdateResult<TData extends BaseRecord = BaseRecord> {
|
|
123
|
+
data: TData;
|
|
84
124
|
}
|
|
85
125
|
|
|
86
|
-
export interface UpdateManyParams {
|
|
126
|
+
export interface UpdateManyParams<TVariables = unknown> {
|
|
87
127
|
resource: string;
|
|
88
128
|
ids: (string | number)[];
|
|
89
|
-
variables:
|
|
129
|
+
variables: TVariables;
|
|
90
130
|
meta?: Record<string, unknown>;
|
|
91
131
|
}
|
|
92
132
|
|
|
93
|
-
export interface UpdateManyResult<
|
|
94
|
-
data:
|
|
133
|
+
export interface UpdateManyResult<TData extends BaseRecord = BaseRecord> {
|
|
134
|
+
data: TData[];
|
|
95
135
|
}
|
|
96
136
|
|
|
97
|
-
export interface DeleteParams {
|
|
137
|
+
export interface DeleteParams<TVariables = unknown> {
|
|
98
138
|
resource: string;
|
|
99
139
|
id: string | number;
|
|
100
|
-
variables?:
|
|
140
|
+
variables?: TVariables;
|
|
101
141
|
meta?: Record<string, unknown>;
|
|
102
142
|
}
|
|
103
143
|
|
|
104
|
-
export interface DeleteResult<
|
|
105
|
-
data:
|
|
144
|
+
export interface DeleteResult<TData extends BaseRecord = BaseRecord> {
|
|
145
|
+
data: TData;
|
|
106
146
|
}
|
|
107
147
|
|
|
108
|
-
export interface DeleteManyParams {
|
|
148
|
+
export interface DeleteManyParams<TVariables = unknown> {
|
|
109
149
|
resource: string;
|
|
110
150
|
ids: (string | number)[];
|
|
111
|
-
variables?:
|
|
151
|
+
variables?: TVariables;
|
|
112
152
|
meta?: Record<string, unknown>;
|
|
113
153
|
}
|
|
114
154
|
|
|
115
|
-
export interface DeleteManyResult<
|
|
116
|
-
data:
|
|
155
|
+
export interface DeleteManyResult<TData extends BaseRecord = BaseRecord> {
|
|
156
|
+
data: TData[];
|
|
117
157
|
}
|
|
118
158
|
|
|
119
|
-
export interface CustomParams {
|
|
159
|
+
export interface CustomParams<TVariables = unknown> {
|
|
120
160
|
url: string;
|
|
121
161
|
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
122
|
-
payload?:
|
|
162
|
+
payload?: TVariables;
|
|
123
163
|
query?: Record<string, unknown>;
|
|
124
164
|
headers?: Record<string, string>;
|
|
125
165
|
sorters?: Sort[];
|
|
@@ -127,27 +167,27 @@ export interface CustomParams {
|
|
|
127
167
|
meta?: Record<string, unknown>;
|
|
128
168
|
}
|
|
129
169
|
|
|
130
|
-
export interface CustomResult<
|
|
131
|
-
data:
|
|
170
|
+
export interface CustomResult<TData = unknown> {
|
|
171
|
+
data: TData;
|
|
132
172
|
}
|
|
133
173
|
|
|
134
174
|
export interface DataProvider {
|
|
135
175
|
// Required methods
|
|
136
|
-
getList: <
|
|
137
|
-
getOne: <
|
|
138
|
-
create: <
|
|
139
|
-
update: <
|
|
140
|
-
deleteOne: <
|
|
176
|
+
getList: <TData extends BaseRecord = BaseRecord>(params: GetListParams) => Promise<GetListResult<TData>>;
|
|
177
|
+
getOne: <TData extends BaseRecord = BaseRecord>(params: GetOneParams) => Promise<GetOneResult<TData>>;
|
|
178
|
+
create: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateParams<TVariables>) => Promise<CreateResult<TData>>;
|
|
179
|
+
update: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateParams<TVariables>) => Promise<UpdateResult<TData>>;
|
|
180
|
+
deleteOne: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteParams<TVariables>) => Promise<DeleteResult<TData>>;
|
|
141
181
|
getApiUrl: () => string;
|
|
142
182
|
|
|
143
183
|
// Optional bulk methods
|
|
144
|
-
getMany?: <
|
|
145
|
-
createMany?: <
|
|
146
|
-
updateMany?: <
|
|
147
|
-
deleteMany?: <
|
|
184
|
+
getMany?: <TData extends BaseRecord = BaseRecord>(params: GetManyParams) => Promise<GetManyResult<TData>>;
|
|
185
|
+
createMany?: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateManyParams<TVariables>) => Promise<CreateManyResult<TData>>;
|
|
186
|
+
updateMany?: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateManyParams<TVariables>) => Promise<UpdateManyResult<TData>>;
|
|
187
|
+
deleteMany?: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteManyParams<TVariables>) => Promise<DeleteManyResult<TData>>;
|
|
148
188
|
|
|
149
189
|
// Optional custom method
|
|
150
|
-
custom?: <
|
|
190
|
+
custom?: <TData = unknown, TVariables = unknown>(params: CustomParams<TVariables>) => Promise<CustomResult<TData>>;
|
|
151
191
|
}
|
|
152
192
|
|
|
153
193
|
// ─── AuthProvider ─────────────────────────────────────────────
|
|
@@ -200,6 +240,8 @@ export type MutationMode = 'pessimistic' | 'optimistic' | 'undoable';
|
|
|
200
240
|
|
|
201
241
|
export interface ResourceDefinition {
|
|
202
242
|
name: string;
|
|
243
|
+
/** Unique identifier — use when multiple resources share the same `name` but target different DataProviders */
|
|
244
|
+
identifier?: string;
|
|
203
245
|
label: string;
|
|
204
246
|
icon?: string;
|
|
205
247
|
primaryKey?: string;
|
|
@@ -210,7 +252,10 @@ export interface ResourceDefinition {
|
|
|
210
252
|
canEdit?: boolean;
|
|
211
253
|
canDelete?: boolean;
|
|
212
254
|
canShow?: boolean;
|
|
213
|
-
|
|
255
|
+
showInMenu?: boolean;
|
|
256
|
+
parentName?: string;
|
|
257
|
+
menuOrder?: number;
|
|
258
|
+
meta?: Record<string, unknown> & { dataProviderName?: string; parent?: string };
|
|
214
259
|
}
|
|
215
260
|
|
|
216
261
|
export interface FieldDefinition {
|
|
@@ -236,3 +281,35 @@ export interface FieldDefinition {
|
|
|
236
281
|
// Validation
|
|
237
282
|
validate?: (value: unknown) => string | null;
|
|
238
283
|
}
|
|
284
|
+
|
|
285
|
+
// ─── Resource Type Registry ───────────────────────────────────
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Extend this interface via declaration merging to register resource types.
|
|
289
|
+
* When registered, all hooks automatically infer data types from resource names.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```ts
|
|
293
|
+
* declare module '@svadmin/core' {
|
|
294
|
+
* interface ResourceTypeMap {
|
|
295
|
+
* users: { id: string; name: string; email: string }
|
|
296
|
+
* posts: { id: string; title: string; content: string }
|
|
297
|
+
* }
|
|
298
|
+
* }
|
|
299
|
+
*
|
|
300
|
+
* // Now hooks auto-infer:
|
|
301
|
+
* const list = useList({ resource: 'users' })
|
|
302
|
+
* // list.data → { id: string; name: string; email: string }[]
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
export interface ResourceTypeMap {}
|
|
306
|
+
|
|
307
|
+
/** When ResourceTypeMap is empty → string (backward compatible); otherwise → registered keys */
|
|
308
|
+
export type KnownResources = keyof ResourceTypeMap extends never
|
|
309
|
+
? string
|
|
310
|
+
: Extract<keyof ResourceTypeMap, string>
|
|
311
|
+
|
|
312
|
+
/** Infer data type from resource name. Falls back to Record<string, unknown> for unregistered resources */
|
|
313
|
+
export type InferData<R extends string> = R extends keyof ResourceTypeMap
|
|
314
|
+
? ResourceTypeMap[R]
|
|
315
|
+
: Record<string, unknown>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// UnsavedChangesNotifier — warns user before leaving page with unsaved changes
|
|
2
|
+
// Svelte 5 component using runes
|
|
3
|
+
|
|
4
|
+
import { onDestroy, onMount } from 'svelte';
|
|
5
|
+
|
|
6
|
+
let warnWhenUnsavedChanges = $state(false);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Call this to mark the form as dirty (has unsaved changes).
|
|
10
|
+
* When set to true, navigating away or closing the tab will trigger a warning.
|
|
11
|
+
*/
|
|
12
|
+
export function setUnsavedChanges(dirty: boolean) {
|
|
13
|
+
warnWhenUnsavedChanges = dirty;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getUnsavedChanges(): boolean {
|
|
17
|
+
return warnWhenUnsavedChanges;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const beforeUnloadHandler = (e: BeforeUnloadEvent) => {
|
|
21
|
+
if (warnWhenUnsavedChanges) {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
// Modern browsers ignore custom messages but require returnValue to be set
|
|
24
|
+
e.returnValue = '';
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the UnsavedChangesNotifier.
|
|
31
|
+
* Call this in your root layout or AdminApp component.
|
|
32
|
+
* It handles both browser tab close and in-app navigation.
|
|
33
|
+
*/
|
|
34
|
+
export function initUnsavedChangesNotifier() {
|
|
35
|
+
if (typeof window === 'undefined') return;
|
|
36
|
+
|
|
37
|
+
window.addEventListener('beforeunload', beforeUnloadHandler);
|
|
38
|
+
|
|
39
|
+
// For hash-based navigation, intercept hashchange
|
|
40
|
+
const hashChangeHandler = (e: HashChangeEvent) => {
|
|
41
|
+
if (warnWhenUnsavedChanges) {
|
|
42
|
+
const confirmed = confirm('You have unsaved changes. Are you sure you want to leave?');
|
|
43
|
+
if (!confirmed) {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
// Restore original hash
|
|
46
|
+
if (e.oldURL) {
|
|
47
|
+
const oldHash = new URL(e.oldURL).hash;
|
|
48
|
+
history.replaceState(null, '', oldHash || '#');
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
warnWhenUnsavedChanges = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
window.addEventListener('hashchange', hashChangeHandler);
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
|
60
|
+
window.removeEventListener('hashchange', hashChangeHandler);
|
|
61
|
+
};
|
|
62
|
+
}
|
package/src/url-sync.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
// URL state sync — sync pagination/sort/filters with hash params
|
|
2
2
|
|
|
3
|
+
import type { Filter } from './types';
|
|
4
|
+
|
|
3
5
|
export interface URLState {
|
|
4
6
|
page?: number;
|
|
5
7
|
pageSize?: number;
|
|
6
8
|
sortField?: string;
|
|
7
9
|
sortOrder?: 'asc' | 'desc';
|
|
8
10
|
search?: string;
|
|
11
|
+
filters?: Filter[];
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
export function readURLState(): URLState {
|
|
15
|
+
if (typeof window === 'undefined') return {};
|
|
12
16
|
const hash = window.location.hash;
|
|
13
17
|
const queryIdx = hash.indexOf('?');
|
|
14
18
|
if (queryIdx === -1) return {};
|
|
@@ -31,10 +35,18 @@ export function readURLState(): URLState {
|
|
|
31
35
|
const search = params.get('q');
|
|
32
36
|
if (search) state.search = search;
|
|
33
37
|
|
|
38
|
+
const filtersRaw = params.get('filters');
|
|
39
|
+
if (filtersRaw) {
|
|
40
|
+
try {
|
|
41
|
+
state.filters = JSON.parse(filtersRaw);
|
|
42
|
+
} catch { /* ignore invalid json */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
return state;
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
export function writeURLState(state: URLState): void {
|
|
49
|
+
if (typeof window === 'undefined') return;
|
|
38
50
|
const hash = window.location.hash;
|
|
39
51
|
const pathIdx = hash.indexOf('?');
|
|
40
52
|
const basePath = pathIdx === -1 ? hash : hash.slice(0, pathIdx);
|
|
@@ -45,6 +57,9 @@ export function writeURLState(state: URLState): void {
|
|
|
45
57
|
if (state.sortField) params.set('sort', state.sortField);
|
|
46
58
|
if (state.sortOrder) params.set('order', state.sortOrder);
|
|
47
59
|
if (state.search) params.set('q', state.search);
|
|
60
|
+
if (state.filters && state.filters.length > 0) {
|
|
61
|
+
params.set('filters', JSON.stringify(state.filters));
|
|
62
|
+
}
|
|
48
63
|
|
|
49
64
|
const qs = params.toString();
|
|
50
65
|
const newHash = qs ? `${basePath}?${qs}` : basePath;
|