@svadmin/core 0.0.6 → 0.4.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 +7 -1
- package/src/audit.ts +37 -0
- package/src/auth-hooks.svelte.ts +2 -3
- package/src/auth-hooks.test.ts +205 -0
- package/src/context.svelte.ts +123 -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 +277 -530
- package/src/http-error.test.ts +85 -0
- package/src/i18n.svelte.ts +87 -1
- package/src/index.ts +42 -9
- package/src/inferencer-openapi.test.ts +192 -0
- package/src/inferencer-openapi.ts +290 -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/query-hooks.svelte.ts +213 -0
- package/src/query-keys.ts +22 -0
- package/src/resource-types.test.ts +79 -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/table-state.svelte.ts +243 -0
- package/src/toast.svelte.ts +28 -8
- package/src/types.ts +129 -47
- package/src/unsaved-changes.svelte.ts +62 -0
- package/src/url-sync.ts +15 -0
- package/src/useParsed.ts +40 -4
- package/src/useStepsForm.svelte.ts +71 -0
- package/src/utility-hooks.svelte.ts +161 -0
- package/src/context.ts +0 -61
package/src/hooks.svelte.ts
CHANGED
|
@@ -1,618 +1,365 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
// @svadmin/core — Core Data and Hook APIs (Modularized for v0.2.29+)
|
|
2
|
+
|
|
3
|
+
// ─── Re-exports from modular hook files ─────────────────────────────
|
|
4
|
+
|
|
5
|
+
export * from './query-hooks.svelte';
|
|
6
|
+
export * from './mutation-hooks.svelte';
|
|
7
|
+
export * from './form-hooks.svelte';
|
|
8
|
+
export * from './table-hooks.svelte';
|
|
9
|
+
export * from './routing-hooks.svelte';
|
|
10
|
+
export * from './utility-hooks.svelte';
|
|
11
|
+
|
|
12
|
+
// ─── Re-export types from options ─────────────────────────────────
|
|
13
|
+
export type { InvalidateScope, OvertimeConfig } from './options';
|
|
14
|
+
|
|
15
|
+
// ─── Export shared utilities ────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export { createOvertimeTracker, createLiveSubscription } from './hook-utils.svelte';
|
|
18
|
+
export type { OvertimeResult, OvertimeOptions, NotificationConfig } from './hook-utils.svelte';
|
|
19
|
+
|
|
20
|
+
// ─── Missing hooks stubs ───────────────────────────────────────────
|
|
21
|
+
// These hooks were in the original monolithic file. They are re-created
|
|
22
|
+
// here as thin wrappers or stubs until they get their own modular files.
|
|
23
|
+
|
|
24
|
+
import { createQuery, createInfiniteQuery, createMutation, useQueryClient } from '@tanstack/svelte-query';
|
|
25
|
+
import { getAdminOptions } from './options';
|
|
26
|
+
import { getDataProviderForResource, getDataProvider } from './context.svelte';
|
|
27
|
+
import { useParsed } from './useParsed';
|
|
28
|
+
import { createOvertimeTracker, fireSuccessNotification, fireErrorNotification } from './hook-utils.svelte';
|
|
29
|
+
import type { NotificationConfig, OvertimeOptions } from './hook-utils.svelte';
|
|
30
|
+
import type { BaseRecord, HttpError, Pagination, Sort, Filter, DataProvider, KnownResources } from './types';
|
|
31
|
+
import type { LiveMode, LiveEvent } from './live';
|
|
32
|
+
import { useList } from './query-hooks.svelte';
|
|
33
|
+
import type { UseListOptions } from './query-hooks.svelte';
|
|
13
34
|
import { audit } from './audit';
|
|
14
|
-
import { navigate } from './router';
|
|
15
|
-
import { t } from './i18n.svelte';
|
|
16
|
-
import { readURLState, writeURLState } from './url-sync';
|
|
17
35
|
|
|
18
|
-
// ───
|
|
36
|
+
// ─── useInfiniteList ────────────────────────────────────────────────
|
|
19
37
|
|
|
20
|
-
interface
|
|
21
|
-
resource
|
|
38
|
+
export interface UseInfiniteListOptions<TData extends BaseRecord = BaseRecord, TError = HttpError> {
|
|
39
|
+
resource?: KnownResources;
|
|
22
40
|
pagination?: Pagination;
|
|
23
41
|
sorters?: Sort[];
|
|
24
42
|
filters?: Filter[];
|
|
25
43
|
meta?: Record<string, unknown>;
|
|
26
|
-
|
|
44
|
+
dataProviderName?: string;
|
|
45
|
+
queryOptions?: { staleTime?: number; enabled?: boolean };
|
|
46
|
+
successNotification?: NotificationConfig;
|
|
47
|
+
errorNotification?: NotificationConfig;
|
|
48
|
+
liveMode?: LiveMode;
|
|
49
|
+
onLiveEvent?: (event: LiveEvent) => void;
|
|
50
|
+
overtimeOptions?: OvertimeOptions;
|
|
27
51
|
}
|
|
28
52
|
|
|
29
|
-
export function
|
|
30
|
-
const
|
|
31
|
-
const
|
|
53
|
+
export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseInfiniteListOptions<TData, TError> = {}) {
|
|
54
|
+
const parsed = useParsed();
|
|
55
|
+
const resource = options.resource ?? parsed.resource ?? '';
|
|
56
|
+
const adminOptions = getAdminOptions();
|
|
57
|
+
const provider = getDataProviderForResource(resource, options.dataProviderName);
|
|
32
58
|
|
|
33
|
-
|
|
34
|
-
queryKey: [resource, '
|
|
35
|
-
queryFn:
|
|
36
|
-
|
|
37
|
-
}));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ─── useInfiniteList ────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
interface UseInfiniteListOptions {
|
|
43
|
-
resource: string;
|
|
44
|
-
pageSize?: number;
|
|
45
|
-
sorters?: Sort[];
|
|
46
|
-
filters?: Filter[];
|
|
47
|
-
meta?: Record<string, unknown>;
|
|
48
|
-
enabled?: boolean;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function useInfiniteList<T = Record<string, unknown>>(options: UseInfiniteListOptions) {
|
|
52
|
-
const provider = getDataProvider();
|
|
53
|
-
const { resource, pageSize = 10, sorters, filters, meta, enabled = true } = options;
|
|
54
|
-
|
|
55
|
-
return createInfiniteQuery<GetListResult<T>>(() => ({
|
|
56
|
-
queryKey: [resource, 'infinite', { sorters, filters }],
|
|
57
|
-
queryFn: ({ pageParam = 1 }) =>
|
|
58
|
-
provider.getList<T>({
|
|
59
|
+
const query = createInfiniteQuery<{ data: TData[]; total: number }, TError>(() => ({
|
|
60
|
+
queryKey: [resource, 'infiniteList', options.sorters, options.filters, options.meta],
|
|
61
|
+
queryFn: async ({ pageParam = 1 }) => {
|
|
62
|
+
const result = await provider.getList<TData>({
|
|
59
63
|
resource,
|
|
60
|
-
pagination: { current: pageParam as number, pageSize },
|
|
61
|
-
sorters,
|
|
62
|
-
filters,
|
|
63
|
-
meta,
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
getNextPageParam: (lastPage, allPages) => {
|
|
67
|
-
const totalFetched = allPages.reduce((sum, p) => sum + p.data.length, 0);
|
|
68
|
-
return totalFetched < lastPage.total ? allPages.length + 1 : undefined;
|
|
64
|
+
pagination: { current: pageParam as number, pageSize: options.pagination?.pageSize ?? 10 },
|
|
65
|
+
sorters: options.sorters,
|
|
66
|
+
filters: options.filters,
|
|
67
|
+
meta: options.meta,
|
|
68
|
+
});
|
|
69
|
+
return result;
|
|
69
70
|
},
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
interface UseOneOptions {
|
|
77
|
-
resource: string;
|
|
78
|
-
id: string | number;
|
|
79
|
-
meta?: Record<string, unknown>;
|
|
80
|
-
enabled?: boolean;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function useOne<T = Record<string, unknown>>(options: UseOneOptions) {
|
|
84
|
-
const provider = getDataProvider();
|
|
85
|
-
const { resource, id, meta, enabled = true } = options;
|
|
86
|
-
|
|
87
|
-
return createQuery(() => ({
|
|
88
|
-
queryKey: [resource, 'one', id],
|
|
89
|
-
queryFn: async () => {
|
|
90
|
-
const result = await provider.getOne<T>({ resource, id, meta });
|
|
91
|
-
return result.data;
|
|
71
|
+
initialPageParam: 1,
|
|
72
|
+
getNextPageParam: (lastPage: { data: TData[]; total: number }, allPages: { data: TData[]; total: number }[]) => {
|
|
73
|
+
const totalFetched = allPages.reduce((acc: number, p) => acc + (p.data?.length ?? 0), 0);
|
|
74
|
+
if (totalFetched >= (lastPage.total ?? 0)) return undefined;
|
|
75
|
+
return allPages.length + 1;
|
|
92
76
|
},
|
|
93
|
-
enabled: enabled
|
|
77
|
+
enabled: options.queryOptions?.enabled ?? true,
|
|
78
|
+
staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
94
79
|
}));
|
|
95
|
-
}
|
|
96
80
|
|
|
97
|
-
|
|
81
|
+
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
98
82
|
|
|
99
|
-
|
|
100
|
-
return useOne<T>(options);
|
|
83
|
+
return { query, get overtime() { return overtime; } };
|
|
101
84
|
}
|
|
102
85
|
|
|
103
|
-
// ─── useSelect
|
|
86
|
+
// ─── useSelect ──────────────────────────────────────────────────────
|
|
104
87
|
|
|
105
|
-
interface UseSelectOptions {
|
|
106
|
-
resource:
|
|
107
|
-
optionLabel?: string;
|
|
108
|
-
optionValue?: string;
|
|
109
|
-
filters?: Filter[];
|
|
88
|
+
export interface UseSelectOptions<TData extends BaseRecord = BaseRecord, TOption = { label: string; value: string | number }> {
|
|
89
|
+
resource: KnownResources;
|
|
90
|
+
optionLabel?: string | ((item: TData) => string);
|
|
91
|
+
optionValue?: string | ((item: TData) => string | number);
|
|
110
92
|
sorters?: Sort[];
|
|
111
|
-
|
|
93
|
+
filters?: Filter[];
|
|
94
|
+
defaultValue?: (string | number)[];
|
|
95
|
+
fetchSize?: number;
|
|
96
|
+
pagination?: Pagination;
|
|
97
|
+
debounce?: number;
|
|
98
|
+
queryOptions?: { staleTime?: number; enabled?: boolean };
|
|
99
|
+
defaultValueQueryOptions?: { staleTime?: number; enabled?: boolean };
|
|
100
|
+
meta?: Record<string, unknown>;
|
|
101
|
+
dataProviderName?: string;
|
|
102
|
+
onSearch?: (value: string) => Filter[];
|
|
103
|
+
successNotification?: NotificationConfig;
|
|
104
|
+
errorNotification?: NotificationConfig;
|
|
105
|
+
overtimeOptions?: OvertimeOptions;
|
|
112
106
|
}
|
|
113
107
|
|
|
114
|
-
export function useSelect(options: UseSelectOptions) {
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
filters, sorters, enabled = true,
|
|
119
|
-
} = options;
|
|
108
|
+
export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { label: string; value: string | number }>(options: UseSelectOptions<TData, TOption>) {
|
|
109
|
+
const { resource, optionLabel = 'title', optionValue = 'id', sorters, filters, pagination, meta, dataProviderName, onSearch, debounce: debounceMs = 300 } = options;
|
|
110
|
+
const adminOptions = getAdminOptions();
|
|
111
|
+
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
120
112
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
queryFn: async () => {
|
|
124
|
-
const result = await provider.getList({ resource, pagination: { current: 1, pageSize: 1000 }, filters, sorters });
|
|
125
|
-
return result.data.map(item => ({
|
|
126
|
-
label: String(item[optionLabel] ?? ''),
|
|
127
|
-
value: String(item[optionValue] ?? ''),
|
|
128
|
-
}));
|
|
129
|
-
},
|
|
130
|
-
enabled,
|
|
131
|
-
staleTime: 10 * 60 * 1000,
|
|
132
|
-
}));
|
|
133
|
-
}
|
|
113
|
+
let searchText = $state('');
|
|
114
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
134
115
|
|
|
135
|
-
|
|
116
|
+
const searchFilters = $derived.by<Filter[]>(() => {
|
|
117
|
+
if (!onSearch || !searchText) return [];
|
|
118
|
+
return onSearch(searchText);
|
|
119
|
+
});
|
|
136
120
|
|
|
137
|
-
|
|
138
|
-
resource: string;
|
|
139
|
-
ids: (string | number)[];
|
|
140
|
-
meta?: Record<string, unknown>;
|
|
141
|
-
enabled?: boolean;
|
|
142
|
-
}
|
|
121
|
+
const allFilters = $derived([...(filters ?? []), ...searchFilters]);
|
|
143
122
|
|
|
144
|
-
|
|
145
|
-
const provider = getDataProvider();
|
|
146
|
-
const { resource, ids, meta, enabled = true } = options;
|
|
123
|
+
const effectivePageSize = options.fetchSize ?? pagination?.pageSize ?? 999;
|
|
147
124
|
|
|
148
|
-
|
|
149
|
-
queryKey: [resource, '
|
|
150
|
-
queryFn:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return result.data;
|
|
154
|
-
}
|
|
155
|
-
const results = await Promise.all(ids.map(id => provider.getOne<T>({ resource, id, meta })));
|
|
156
|
-
return results.map(r => r.data);
|
|
157
|
-
},
|
|
158
|
-
enabled: enabled && ids.length > 0,
|
|
125
|
+
const query = createQuery<{ data: TData[]; total: number }>(() => ({
|
|
126
|
+
queryKey: [resource, 'select', allFilters, sorters, pagination, meta],
|
|
127
|
+
queryFn: () => provider.getList<TData>({ resource, sorters, filters: allFilters, pagination: { current: 1, pageSize: effectivePageSize }, meta }),
|
|
128
|
+
enabled: options.queryOptions?.enabled ?? true,
|
|
129
|
+
staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
159
130
|
}));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ─── useCustom ──────────────────────────────────────────────────
|
|
163
131
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
132
|
+
// Parallel query to ensure default values are always available in options
|
|
133
|
+
const defaultValueIds = options.defaultValue ?? [];
|
|
134
|
+
const defaultValueQuery = defaultValueIds.length > 0
|
|
135
|
+
? createQuery<{ data: TData[] }>(() => ({
|
|
136
|
+
queryKey: [resource, 'select-defaults', defaultValueIds],
|
|
137
|
+
queryFn: async () => {
|
|
138
|
+
if (provider.getMany) return provider.getMany<TData>({ resource, ids: defaultValueIds, meta });
|
|
139
|
+
const results = await Promise.all(defaultValueIds.map(id => provider.getOne<TData>({ resource, id, meta })));
|
|
140
|
+
return { data: results.map(r => r.data) };
|
|
141
|
+
},
|
|
142
|
+
enabled: (options.defaultValueQueryOptions?.enabled ?? true) && defaultValueIds.length > 0,
|
|
143
|
+
staleTime: options.defaultValueQueryOptions?.staleTime ?? Infinity,
|
|
144
|
+
}))
|
|
145
|
+
: null;
|
|
177
146
|
|
|
178
|
-
|
|
147
|
+
const selectOptions = $derived.by(() => {
|
|
148
|
+
const queryResult = query.data as { data: TData[]; total: number } | undefined;
|
|
149
|
+
const data: TData[] = queryResult?.data ?? [];
|
|
150
|
+
// Merge default value items if not already in data
|
|
151
|
+
const defaultQueryResult = defaultValueQuery as { data: { data: TData[] } | undefined } | null;
|
|
152
|
+
const defaultData: TData[] = defaultQueryResult?.data?.data ?? [];
|
|
153
|
+
const allData: TData[] = [...data];
|
|
154
|
+
const existingIds = new Set(data.map((d: TData) => String((d as Record<string, unknown>)[typeof optionValue === 'string' ? optionValue : 'id'])));
|
|
155
|
+
for (const item of defaultData) {
|
|
156
|
+
const itemId = String((item as Record<string, unknown>)[typeof optionValue === 'string' ? optionValue : 'id']);
|
|
157
|
+
if (!existingIds.has(itemId)) allData.push(item);
|
|
158
|
+
}
|
|
159
|
+
return allData.map((item: TData) => {
|
|
160
|
+
const label = typeof optionLabel === 'function' ? optionLabel(item) : String((item as Record<string, unknown>)[optionLabel] ?? '');
|
|
161
|
+
const value = typeof optionValue === 'function' ? optionValue(item) : (item as Record<string, unknown>)[optionValue];
|
|
162
|
+
return { label, value } as unknown as TOption;
|
|
163
|
+
});
|
|
164
|
+
});
|
|
179
165
|
|
|
180
|
-
|
|
181
|
-
const provider = getDataProvider();
|
|
182
|
-
return provider.getApiUrl();
|
|
183
|
-
}
|
|
166
|
+
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
184
167
|
|
|
185
|
-
|
|
168
|
+
function onSearchChange(value: string) {
|
|
169
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
170
|
+
debounceTimer = setTimeout(() => { searchText = value; }, debounceMs);
|
|
171
|
+
}
|
|
186
172
|
|
|
187
|
-
export function useNavigation() {
|
|
188
173
|
return {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
clone: (resource: string, id: string | number) => navigate(`/${resource}/create?clone_id=${id}`),
|
|
194
|
-
goBack: () => history.back(),
|
|
195
|
-
push: (path: string) => navigate(path),
|
|
174
|
+
query,
|
|
175
|
+
get options() { return selectOptions; },
|
|
176
|
+
get overtime() { return overtime; },
|
|
177
|
+
onSearchChange,
|
|
196
178
|
};
|
|
197
179
|
}
|
|
198
180
|
|
|
199
|
-
// ───
|
|
200
|
-
|
|
201
|
-
export { getResource as useResource } from './context';
|
|
202
|
-
|
|
203
|
-
// ─── Mutation options ───────────────────────────────────────────
|
|
181
|
+
// ─── useCustom ──────────────────────────────────────────────────────
|
|
204
182
|
|
|
205
|
-
interface
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
183
|
+
export interface UseCustomOptions<TData = unknown, TError = HttpError> {
|
|
184
|
+
url: string;
|
|
185
|
+
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
186
|
+
config?: { payload?: unknown; query?: Record<string, unknown>; headers?: Record<string, string>; sorters?: Sort[]; filters?: Filter[] };
|
|
187
|
+
meta?: Record<string, unknown>;
|
|
188
|
+
dataProviderName?: string;
|
|
189
|
+
queryOptions?: { staleTime?: number; enabled?: boolean };
|
|
190
|
+
successNotification?: NotificationConfig;
|
|
191
|
+
errorNotification?: NotificationConfig;
|
|
192
|
+
overtimeOptions?: OvertimeOptions;
|
|
209
193
|
}
|
|
210
194
|
|
|
211
|
-
|
|
195
|
+
export function useCustom<TData = unknown, TError = HttpError>(options: UseCustomOptions<TData, TError>) {
|
|
196
|
+
const adminOptions = getAdminOptions();
|
|
197
|
+
const provider = getDataProvider(options.dataProviderName);
|
|
212
198
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
: undefined,
|
|
228
|
-
onSuccess: (data: { data: T }) => {
|
|
229
|
-
queryClient.invalidateQueries({ queryKey: [resource] });
|
|
230
|
-
if (showToast) toast.success(t('common.createSuccess'));
|
|
231
|
-
if (auditLog) {
|
|
232
|
-
const record = data.data as Record<string, unknown>;
|
|
233
|
-
audit({ action: 'create', resource, recordId: record.id as string });
|
|
234
|
-
}
|
|
235
|
-
},
|
|
236
|
-
onError: (error: Error, _variables: Record<string, unknown>, context: { previousData?: unknown } | undefined) => {
|
|
237
|
-
if (mutationMode === 'optimistic' && context?.previousData) {
|
|
238
|
-
queryClient.setQueryData([resource, 'list'], context.previousData);
|
|
239
|
-
}
|
|
240
|
-
if (showToast) toast.error(t('common.operationFailed') + ': ' + error.message);
|
|
199
|
+
const query = createQuery<{ data: TData }, TError>(() => ({
|
|
200
|
+
queryKey: ['custom', options.url, options.method, options.config, options.meta],
|
|
201
|
+
queryFn: async () => {
|
|
202
|
+
if (!provider.custom) throw new Error('DataProvider does not support custom method');
|
|
203
|
+
return provider.custom<TData>({
|
|
204
|
+
url: options.url,
|
|
205
|
+
method: options.method,
|
|
206
|
+
payload: options.config?.payload,
|
|
207
|
+
query: options.config?.query,
|
|
208
|
+
headers: options.config?.headers,
|
|
209
|
+
sorters: options.config?.sorters,
|
|
210
|
+
filters: options.config?.filters,
|
|
211
|
+
meta: options.meta,
|
|
212
|
+
});
|
|
241
213
|
},
|
|
214
|
+
enabled: options.queryOptions?.enabled ?? true,
|
|
215
|
+
staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
242
216
|
}));
|
|
243
|
-
}
|
|
244
217
|
|
|
245
|
-
|
|
218
|
+
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
246
219
|
|
|
247
|
-
|
|
248
|
-
const provider = getDataProvider();
|
|
249
|
-
const queryClient = useQueryClient();
|
|
250
|
-
const { showToast = true, auditLog = true } = opts ?? {};
|
|
251
|
-
|
|
252
|
-
return createMutation(() => ({
|
|
253
|
-
mutationFn: async (variables: Record<string, unknown>[]) => {
|
|
254
|
-
if (provider.createMany) {
|
|
255
|
-
return provider.createMany<T>({ resource, variables });
|
|
256
|
-
}
|
|
257
|
-
const results = await Promise.all(variables.map(v => provider.create<T>({ resource, variables: v })));
|
|
258
|
-
return { data: results.map(r => r.data) };
|
|
259
|
-
},
|
|
260
|
-
onSuccess: () => {
|
|
261
|
-
queryClient.invalidateQueries({ queryKey: [resource] });
|
|
262
|
-
if (showToast) toast.success(t('common.createSuccess'));
|
|
263
|
-
if (auditLog) audit({ action: 'create', resource, details: { batch: true } });
|
|
264
|
-
},
|
|
265
|
-
onError: (error: Error) => {
|
|
266
|
-
if (showToast) toast.error(t('common.operationFailed') + ': ' + error.message);
|
|
267
|
-
},
|
|
268
|
-
}));
|
|
220
|
+
return { query, get overtime() { return overtime; } };
|
|
269
221
|
}
|
|
270
222
|
|
|
271
|
-
// ───
|
|
272
|
-
|
|
273
|
-
interface UpdateVariables {
|
|
274
|
-
id: string | number;
|
|
275
|
-
variables: Record<string, unknown>;
|
|
276
|
-
}
|
|
223
|
+
// ─── useCustomMutation ──────────────────────────────────────────────
|
|
277
224
|
|
|
278
|
-
export function
|
|
279
|
-
const provider = getDataProvider();
|
|
225
|
+
export function useCustomMutation<TData = unknown, TError = HttpError, TVariables = unknown>(dataProviderName?: string) {
|
|
226
|
+
const provider = getDataProvider(dataProviderName);
|
|
280
227
|
const queryClient = useQueryClient();
|
|
281
|
-
const { showToast = true, auditLog = true, mutationMode = 'pessimistic' } = opts ?? {};
|
|
282
|
-
|
|
283
|
-
return createMutation<{ data: T }, Error, UpdateVariables>(() => ({
|
|
284
|
-
mutationFn: ({ id, variables }: UpdateVariables) =>
|
|
285
|
-
provider.update<T>({ resource, id, variables }),
|
|
286
|
-
onMutate: mutationMode === 'optimistic'
|
|
287
|
-
? async ({ id }: UpdateVariables) => {
|
|
288
|
-
await queryClient.cancelQueries({ queryKey: [resource, 'one', id] });
|
|
289
|
-
const previousData = queryClient.getQueryData([resource, 'one', id]);
|
|
290
|
-
return { previousData };
|
|
291
|
-
}
|
|
292
|
-
: undefined,
|
|
293
|
-
onSuccess: (_data: { data: T }, vars: UpdateVariables) => {
|
|
294
|
-
queryClient.invalidateQueries({ queryKey: [resource] });
|
|
295
|
-
if (showToast) toast.success(t('common.updateSuccess'));
|
|
296
|
-
if (auditLog) audit({ action: 'update', resource, recordId: vars.id });
|
|
297
|
-
},
|
|
298
|
-
onError: (error: Error) => {
|
|
299
|
-
if (showToast) toast.error(t('common.operationFailed') + ': ' + error.message);
|
|
300
|
-
},
|
|
301
|
-
}));
|
|
302
|
-
}
|
|
303
228
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const queryClient = useQueryClient();
|
|
309
|
-
const { showToast = true, auditLog = true } = opts ?? {};
|
|
310
|
-
|
|
311
|
-
return createMutation(() => ({
|
|
312
|
-
mutationFn: async ({ ids, variables }: { ids: (string | number)[]; variables: Record<string, unknown> }) => {
|
|
313
|
-
if (provider.updateMany) {
|
|
314
|
-
return provider.updateMany<T>({ resource, ids, variables });
|
|
315
|
-
}
|
|
316
|
-
const results = await Promise.all(ids.map(id => provider.update<T>({ resource, id, variables })));
|
|
317
|
-
return { data: results.map(r => r.data) };
|
|
318
|
-
},
|
|
319
|
-
onSuccess: () => {
|
|
320
|
-
queryClient.invalidateQueries({ queryKey: [resource] });
|
|
321
|
-
if (showToast) toast.success(t('common.updateSuccess'));
|
|
322
|
-
if (auditLog) audit({ action: 'update', resource, details: { batch: true } });
|
|
323
|
-
},
|
|
324
|
-
onError: (error: Error) => {
|
|
325
|
-
if (showToast) toast.error(t('common.operationFailed') + ': ' + error.message);
|
|
229
|
+
const mutation = createMutation<{ data: TData }, TError, { url: string; method: 'get' | 'post' | 'put' | 'patch' | 'delete'; values?: TVariables; meta?: Record<string, unknown> }>(() => ({
|
|
230
|
+
mutationFn: async (params) => {
|
|
231
|
+
if (!provider.custom) throw new Error('DataProvider does not support custom method');
|
|
232
|
+
return provider.custom<TData>({ url: params.url, method: params.method, payload: params.values, meta: params.meta });
|
|
326
233
|
},
|
|
327
234
|
}));
|
|
235
|
+
|
|
236
|
+
return { mutation };
|
|
328
237
|
}
|
|
329
238
|
|
|
330
|
-
// ───
|
|
239
|
+
// ─── useCreateMany / useUpdateMany / useDeleteMany ──────────────────
|
|
331
240
|
|
|
332
|
-
export function
|
|
333
|
-
const
|
|
241
|
+
export function useCreateMany<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: { resource?: KnownResources; overtimeOptions?: OvertimeOptions } = {}) {
|
|
242
|
+
const parsed = useParsed();
|
|
243
|
+
const resource = options.resource ?? parsed.resource ?? '';
|
|
244
|
+
const adminOptions = getAdminOptions();
|
|
334
245
|
const queryClient = useQueryClient();
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
queryClient.invalidateQueries({ queryKey: [resource] });
|
|
349
|
-
if (showToast) toast.success(t('common.deleteSuccess'));
|
|
350
|
-
if (auditLog) audit({ action: 'delete', resource, recordId: id });
|
|
351
|
-
},
|
|
352
|
-
onError: (error: Error) => {
|
|
353
|
-
if (showToast) toast.error(t('common.operationFailed') + ': ' + error.message);
|
|
246
|
+
let isMutating = $state(false);
|
|
247
|
+
const overtime = createOvertimeTracker(() => isMutating, options.overtimeOptions ?? adminOptions.overtime);
|
|
248
|
+
|
|
249
|
+
const mutation = createMutation<{ data: TData[] }, TError, { resource?: KnownResources; variables: TVariables[]; meta?: Record<string, unknown>; dataProviderName?: string }>(() => ({
|
|
250
|
+
mutationFn: async (params) => {
|
|
251
|
+
isMutating = true;
|
|
252
|
+
const resName = params.resource ?? resource;
|
|
253
|
+
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
254
|
+
try {
|
|
255
|
+
if (provider.createMany) return await provider.createMany<TData, TVariables>({ resource: resName, variables: params.variables, meta: params.meta });
|
|
256
|
+
const results = await Promise.all(params.variables.map(v => provider.create<TData, TVariables>({ resource: resName, variables: v, meta: params.meta })));
|
|
257
|
+
return { data: results.map(r => r.data) };
|
|
258
|
+
} finally { isMutating = false; }
|
|
354
259
|
},
|
|
260
|
+
onSuccess: (_d, params) => { queryClient.invalidateQueries({ queryKey: [params.resource ?? resource] }); },
|
|
355
261
|
}));
|
|
356
|
-
}
|
|
357
262
|
|
|
358
|
-
|
|
263
|
+
return { mutation, get overtime() { return overtime; } };
|
|
264
|
+
}
|
|
359
265
|
|
|
360
|
-
export function
|
|
361
|
-
const
|
|
266
|
+
export function useUpdateMany<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: { resource?: KnownResources; overtimeOptions?: OvertimeOptions } = {}) {
|
|
267
|
+
const parsed = useParsed();
|
|
268
|
+
const resource = options.resource ?? parsed.resource ?? '';
|
|
269
|
+
const adminOptions = getAdminOptions();
|
|
362
270
|
const queryClient = useQueryClient();
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (auditLog) audit({ action: 'delete', resource, details: { batch: true } });
|
|
377
|
-
},
|
|
378
|
-
onError: (error: Error) => {
|
|
379
|
-
if (showToast) toast.error(t('common.operationFailed') + ': ' + error.message);
|
|
271
|
+
let isMutating = $state(false);
|
|
272
|
+
const overtime = createOvertimeTracker(() => isMutating, options.overtimeOptions ?? adminOptions.overtime);
|
|
273
|
+
|
|
274
|
+
const mutation = createMutation<{ data: TData[] }, TError, { resource?: KnownResources; ids: (string | number)[]; variables: TVariables; meta?: Record<string, unknown>; dataProviderName?: string }>(() => ({
|
|
275
|
+
mutationFn: async (params) => {
|
|
276
|
+
isMutating = true;
|
|
277
|
+
const resName = params.resource ?? resource;
|
|
278
|
+
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
279
|
+
try {
|
|
280
|
+
if (provider.updateMany) return await provider.updateMany<TData, TVariables>({ resource: resName, ids: params.ids, variables: params.variables, meta: params.meta });
|
|
281
|
+
const results = await Promise.all(params.ids.map(id => provider.update<TData, TVariables>({ resource: resName, id, variables: params.variables, meta: params.meta })));
|
|
282
|
+
return { data: results.map(r => r.data) };
|
|
283
|
+
} finally { isMutating = false; }
|
|
380
284
|
},
|
|
285
|
+
onSuccess: (_d, params) => { queryClient.invalidateQueries({ queryKey: [params.resource ?? resource] }); },
|
|
381
286
|
}));
|
|
382
|
-
}
|
|
383
287
|
|
|
384
|
-
|
|
385
|
-
// Standalone form hook (like Refine's useForm)
|
|
386
|
-
|
|
387
|
-
interface UseFormOptions {
|
|
388
|
-
resource: string;
|
|
389
|
-
action: 'create' | 'edit' | 'clone';
|
|
390
|
-
id?: string | number;
|
|
391
|
-
redirect?: 'list' | 'edit' | 'show' | false;
|
|
392
|
-
mutationMode?: MutationMode;
|
|
393
|
-
onMutationSuccess?: (data: unknown) => void;
|
|
394
|
-
onMutationError?: (error: Error) => void;
|
|
395
|
-
meta?: Record<string, unknown>;
|
|
396
|
-
validate?: (values: Record<string, unknown>) => Record<string, string> | null;
|
|
397
|
-
autoSave?: {
|
|
398
|
-
enabled: boolean;
|
|
399
|
-
debounce?: number; // ms, default 1000
|
|
400
|
-
onFinish?: (values: Record<string, unknown>) => Record<string, unknown>; // transform before save
|
|
401
|
-
};
|
|
288
|
+
return { mutation, get overtime() { return overtime; } };
|
|
402
289
|
}
|
|
403
290
|
|
|
404
|
-
export function
|
|
405
|
-
const
|
|
291
|
+
export function useDeleteMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: { resource?: KnownResources; overtimeOptions?: OvertimeOptions } = {}) {
|
|
292
|
+
const parsed = useParsed();
|
|
293
|
+
const resource = options.resource ?? parsed.resource ?? '';
|
|
294
|
+
const adminOptions = getAdminOptions();
|
|
406
295
|
const queryClient = useQueryClient();
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
function clearFieldError(field: string) {
|
|
421
|
-
const next = { ...errors };
|
|
422
|
-
delete next[field];
|
|
423
|
-
errors = next;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function runValidation(values: Record<string, unknown>): boolean {
|
|
427
|
-
clearErrors();
|
|
428
|
-
if (validate) {
|
|
429
|
-
const result = validate(values);
|
|
430
|
-
if (result && Object.keys(result).length > 0) {
|
|
431
|
-
errors = result;
|
|
432
|
-
return false;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Fetch existing data for edit/clone
|
|
439
|
-
const query = (action === 'edit' || action === 'clone') && id != null
|
|
440
|
-
? createQuery(() => ({
|
|
441
|
-
queryKey: [resource, 'one', id],
|
|
442
|
-
queryFn: async () => {
|
|
443
|
-
const result = await provider.getOne<T>({ resource, id, meta });
|
|
444
|
-
return result.data;
|
|
445
|
-
},
|
|
446
|
-
enabled: id != null,
|
|
447
|
-
}))
|
|
448
|
-
: null;
|
|
449
|
-
|
|
450
|
-
const createMut = createMutation(() => ({
|
|
451
|
-
mutationFn: (variables: Record<string, unknown>) =>
|
|
452
|
-
provider.create<T>({ resource, variables }),
|
|
453
|
-
onSuccess: (data: { data: T }) => {
|
|
454
|
-
queryClient.invalidateQueries({ queryKey: [resource] });
|
|
455
|
-
toast.success(t('common.createSuccess'));
|
|
456
|
-
const record = data.data as Record<string, unknown>;
|
|
457
|
-
audit({ action: 'create', resource, recordId: record.id as string });
|
|
458
|
-
onMutationSuccess?.(data);
|
|
459
|
-
handleRedirect();
|
|
460
|
-
},
|
|
461
|
-
onError: (error: Error) => {
|
|
462
|
-
toast.error(t('common.operationFailed') + ': ' + error.message);
|
|
463
|
-
onMutationError?.(error);
|
|
296
|
+
let isMutating = $state(false);
|
|
297
|
+
const overtime = createOvertimeTracker(() => isMutating, options.overtimeOptions ?? adminOptions.overtime);
|
|
298
|
+
|
|
299
|
+
const mutation = createMutation<{ data: TData[] }, TError, { resource?: KnownResources; ids: (string | number)[]; meta?: Record<string, unknown>; dataProviderName?: string }>(() => ({
|
|
300
|
+
mutationFn: async (params) => {
|
|
301
|
+
isMutating = true;
|
|
302
|
+
const resName = params.resource ?? resource;
|
|
303
|
+
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
304
|
+
try {
|
|
305
|
+
if (provider.deleteMany) return await provider.deleteMany<TData>({ resource: resName, ids: params.ids, meta: params.meta });
|
|
306
|
+
const results = await Promise.all(params.ids.map(id => provider.deleteOne<TData>({ resource: resName, id, meta: params.meta })));
|
|
307
|
+
return { data: results.map(r => r.data) };
|
|
308
|
+
} finally { isMutating = false; }
|
|
464
309
|
},
|
|
310
|
+
onSuccess: (_d, params) => { queryClient.invalidateQueries({ queryKey: [params.resource ?? resource] }); },
|
|
465
311
|
}));
|
|
466
312
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
provider.update<T>({ resource, id: id!, variables }),
|
|
470
|
-
onSuccess: (data: { data: T }) => {
|
|
471
|
-
queryClient.invalidateQueries({ queryKey: [resource] });
|
|
472
|
-
toast.success(t('common.updateSuccess'));
|
|
473
|
-
audit({ action: 'update', resource, recordId: id });
|
|
474
|
-
onMutationSuccess?.(data);
|
|
475
|
-
handleRedirect();
|
|
476
|
-
},
|
|
477
|
-
onError: (error: Error) => {
|
|
478
|
-
toast.error(t('common.operationFailed') + ': ' + error.message);
|
|
479
|
-
onMutationError?.(error);
|
|
480
|
-
},
|
|
481
|
-
}));
|
|
313
|
+
return { mutation, get overtime() { return overtime; } };
|
|
314
|
+
}
|
|
482
315
|
|
|
483
|
-
|
|
484
|
-
if (redirect === 'list') navigate(`/${resource}`);
|
|
485
|
-
else if (redirect === 'edit' && id) navigate(`/${resource}/edit/${id}`);
|
|
486
|
-
else if (redirect === 'show' && id) navigate(`/${resource}/show/${id}`);
|
|
487
|
-
}
|
|
316
|
+
// ─── useInvalidate ──────────────────────────────────────────────────
|
|
488
317
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
318
|
+
export function useInvalidate() {
|
|
319
|
+
const queryClient = useQueryClient();
|
|
320
|
+
return (params: { resource?: string; invalidates?: string[] | 'all' | false; id?: string | number; dataProviderName?: string }) => {
|
|
321
|
+
if (params.invalidates === false) return;
|
|
322
|
+
if (params.invalidates === 'all' || !params.resource) {
|
|
323
|
+
queryClient.invalidateQueries();
|
|
493
324
|
return;
|
|
494
325
|
}
|
|
495
|
-
|
|
496
|
-
if (action === 'create' || action === 'clone') {
|
|
497
|
-
// @ts-expect-error $ rune prefix — Svelte compiler transforms this
|
|
498
|
-
await $createMut.mutateAsync(values);
|
|
499
|
-
} else {
|
|
500
|
-
// @ts-expect-error $ rune prefix — Svelte compiler transforms this
|
|
501
|
-
await $updateMut.mutateAsync(values);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const base = {
|
|
506
|
-
query,
|
|
507
|
-
get formLoading() {
|
|
508
|
-
// @ts-expect-error $ rune prefix — Svelte compiler transforms this
|
|
509
|
-
return (query ? $query?.isLoading : false) || $createMut.isPending || $updateMut.isPending;
|
|
510
|
-
},
|
|
511
|
-
mutation: action === 'edit' ? updateMut : createMut,
|
|
512
|
-
onFinish,
|
|
513
|
-
get errors() { return errors; },
|
|
514
|
-
setFieldError,
|
|
515
|
-
clearErrors,
|
|
516
|
-
clearFieldError,
|
|
326
|
+
queryClient.invalidateQueries({ queryKey: [params.resource] });
|
|
517
327
|
};
|
|
328
|
+
}
|
|
518
329
|
|
|
519
|
-
|
|
520
|
-
let autoSaveStatus = $state<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
|
521
|
-
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
522
|
-
|
|
523
|
-
function triggerAutoSave(values: Record<string, unknown>) {
|
|
524
|
-
if (!autoSave?.enabled || action === 'create') return;
|
|
525
|
-
if (autoSaveTimer) clearTimeout(autoSaveTimer);
|
|
526
|
-
|
|
527
|
-
autoSaveTimer = setTimeout(async () => {
|
|
528
|
-
const finalValues = autoSave.onFinish ? autoSave.onFinish(values) : values;
|
|
529
|
-
autoSaveStatus = 'saving';
|
|
530
|
-
try {
|
|
531
|
-
await provider.update<T>({ resource, id: id!, variables: finalValues });
|
|
532
|
-
queryClient.invalidateQueries({ queryKey: [resource] });
|
|
533
|
-
autoSaveStatus = 'saved';
|
|
534
|
-
// Reset to idle after 2s
|
|
535
|
-
setTimeout(() => { autoSaveStatus = 'idle'; }, 2000);
|
|
536
|
-
} catch {
|
|
537
|
-
autoSaveStatus = 'error';
|
|
538
|
-
}
|
|
539
|
-
}, autoSave.debounce ?? 1000);
|
|
540
|
-
}
|
|
330
|
+
// ─── useOvertime ────────────────────────────────────────────────────
|
|
541
331
|
|
|
332
|
+
export function useOvertime(options?: OvertimeOptions) {
|
|
333
|
+
let isLoading = $state(false);
|
|
334
|
+
const overtime = createOvertimeTracker(() => isLoading, options);
|
|
542
335
|
return {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
336
|
+
get elapsedTime() { return overtime.elapsedTime; },
|
|
337
|
+
start() { isLoading = true; },
|
|
338
|
+
stop() { isLoading = false; },
|
|
546
339
|
};
|
|
547
340
|
}
|
|
548
341
|
|
|
549
|
-
// ───
|
|
550
|
-
// Standalone table hook (like Refine's useTable)
|
|
342
|
+
// ─── useDataProvider ────────────────────────────────────────────────
|
|
551
343
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
pagination?: Pagination;
|
|
555
|
-
sorters?: Sort[];
|
|
556
|
-
filters?: Filter[];
|
|
557
|
-
syncWithLocation?: boolean;
|
|
558
|
-
meta?: Record<string, unknown>;
|
|
344
|
+
export function useDataProvider(): (dataProviderName?: string) => DataProvider {
|
|
345
|
+
return (name?: string) => getDataProvider(name);
|
|
559
346
|
}
|
|
560
347
|
|
|
561
|
-
|
|
562
|
-
const { resource, meta, syncWithLocation = false } = options;
|
|
563
|
-
|
|
564
|
-
// Read initial state from URL if syncWithLocation
|
|
565
|
-
let initialPagination = options.pagination ?? { current: 1, pageSize: 10 };
|
|
566
|
-
let initialSorters = options.sorters ?? [];
|
|
567
|
-
|
|
568
|
-
if (syncWithLocation) {
|
|
569
|
-
const urlState = readURLState();
|
|
570
|
-
if (urlState.page || urlState.pageSize) {
|
|
571
|
-
initialPagination = {
|
|
572
|
-
current: urlState.page ?? initialPagination.current,
|
|
573
|
-
pageSize: urlState.pageSize ?? initialPagination.pageSize,
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
if (urlState.sortField) {
|
|
577
|
-
initialSorters = [{ field: urlState.sortField, order: urlState.sortOrder ?? 'asc' }];
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
let pagination = $state<Pagination>(initialPagination);
|
|
582
|
-
let sorters = $state<Sort[]>(initialSorters);
|
|
583
|
-
let filters = $state<Filter[]>(options.filters ?? []);
|
|
584
|
-
|
|
585
|
-
const query = useList<T>({ resource, pagination, sorters, filters, meta });
|
|
586
|
-
|
|
587
|
-
function setSorters(newSorters: Sort[]) { sorters = newSorters; }
|
|
588
|
-
function setFilters(newFilters: Filter[]) { filters = newFilters; }
|
|
589
|
-
function setPage(page: number) { pagination = { ...pagination, current: page }; }
|
|
590
|
-
function setPageSize(size: number) { pagination = { ...pagination, pageSize: size, current: 1 }; }
|
|
591
|
-
|
|
592
|
-
// Sync state to URL when syncWithLocation is enabled
|
|
593
|
-
if (syncWithLocation) {
|
|
594
|
-
$effect(() => {
|
|
595
|
-
writeURLState({
|
|
596
|
-
page: pagination.current,
|
|
597
|
-
pageSize: pagination.pageSize,
|
|
598
|
-
sortField: sorters[0]?.field,
|
|
599
|
-
sortOrder: sorters[0]?.order,
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
}
|
|
348
|
+
// ─── useThemedLayoutContext ─────────────────────────────────────────
|
|
603
349
|
|
|
350
|
+
export function useThemedLayoutContext() {
|
|
351
|
+
let sidebarCollapsed = $state(false);
|
|
604
352
|
return {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
filters,
|
|
609
|
-
setSorters,
|
|
610
|
-
setFilters,
|
|
611
|
-
setPage,
|
|
612
|
-
setPageSize,
|
|
613
|
-
get totalPages() {
|
|
614
|
-
// @ts-expect-error $ rune prefix — Svelte compiler transforms this
|
|
615
|
-
return Math.ceil(($query.data?.total ?? 0) / (pagination.pageSize ?? 10));
|
|
616
|
-
},
|
|
353
|
+
get sidebarCollapsed() { return sidebarCollapsed; },
|
|
354
|
+
setSidebarCollapsed(v: boolean) { sidebarCollapsed = v; },
|
|
355
|
+
toggleSidebar() { sidebarCollapsed = !sidebarCollapsed; },
|
|
617
356
|
};
|
|
618
357
|
}
|
|
358
|
+
|
|
359
|
+
// ─── Logging hooks (legacy aliases) ─────────────────────────────────
|
|
360
|
+
|
|
361
|
+
export function useLogList(options?: Partial<UseListOptions>) {
|
|
362
|
+
return useList({ resource: 'logs', ...options });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export const useLog = useLogList;
|