@svadmin/core 0.22.0 → 0.22.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/auth-hooks.svelte.ts +10 -10
- package/src/context.svelte.ts +2 -2
- package/src/data-transfer.svelte.ts +58 -29
- package/src/form-hooks.svelte.ts +8 -0
- package/src/hooks.svelte.ts +24 -5
- package/src/i18n.svelte.ts +18 -0
- package/src/live-websocket.ts +20 -2
- package/src/query-hooks.svelte.ts +119 -87
- package/src/routing-hooks.svelte.ts +5 -5
- package/src/table-hooks.svelte.ts +30 -23
- package/src/theme.svelte.ts +1 -1
- package/src/useCan.ts +6 -2
- package/src/useParsed.svelte.ts +4 -0
- package/src/useStepsForm.svelte.ts +31 -21
package/package.json
CHANGED
package/src/auth-hooks.svelte.ts
CHANGED
|
@@ -242,6 +242,14 @@ export function usePermissions<T = unknown>() {
|
|
|
242
242
|
fetch();
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
const hasFn = (perm: string): boolean => {
|
|
246
|
+
if (!permissions) return false;
|
|
247
|
+
if (Array.isArray(permissions)) return permissions.includes(perm);
|
|
248
|
+
if (permissions instanceof Set) return (permissions as Set<string>).has(perm);
|
|
249
|
+
if (typeof permissions === 'object') return !!(permissions as Record<string, boolean>)[perm];
|
|
250
|
+
return false;
|
|
251
|
+
};
|
|
252
|
+
|
|
245
253
|
return {
|
|
246
254
|
get isLoading() { return isLoading; },
|
|
247
255
|
get error() { return error; },
|
|
@@ -250,18 +258,10 @@ export function usePermissions<T = unknown>() {
|
|
|
250
258
|
refetch: fetch,
|
|
251
259
|
|
|
252
260
|
/** Check if a specific permission string exists */
|
|
253
|
-
has
|
|
254
|
-
if (!permissions) return false;
|
|
255
|
-
if (Array.isArray(permissions)) return permissions.includes(perm);
|
|
256
|
-
if (permissions instanceof Set) return (permissions as Set<string>).has(perm);
|
|
257
|
-
if (typeof permissions === 'object') return !!(permissions as Record<string, boolean>)[perm];
|
|
258
|
-
return false;
|
|
259
|
-
},
|
|
261
|
+
has: hasFn,
|
|
260
262
|
|
|
261
263
|
/** Check resource:action style permission */
|
|
262
|
-
can(resource: string, action: string): boolean {
|
|
263
|
-
return this.has(`${resource}:${action}`);
|
|
264
|
-
},
|
|
264
|
+
can: (resource: string, action: string): boolean => hasFn(`${resource}:${action}`),
|
|
265
265
|
};
|
|
266
266
|
}
|
|
267
267
|
|
package/src/context.svelte.ts
CHANGED
|
@@ -68,8 +68,8 @@ function isDataProvider(value: unknown): value is DataProvider {
|
|
|
68
68
|
|
|
69
69
|
let authProvider = $state<AuthProvider | null>(null);
|
|
70
70
|
|
|
71
|
-
export function setAuthProvider(provider: AuthProvider): void {
|
|
72
|
-
authProvider = provider;
|
|
71
|
+
export function setAuthProvider(provider: AuthProvider | null | undefined): void {
|
|
72
|
+
authProvider = provider || null;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export function getAuthProvider(): AuthProvider;
|
|
@@ -120,15 +120,20 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
|
|
|
120
120
|
const errored: { request: unknown; error: unknown }[] = [];
|
|
121
121
|
|
|
122
122
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
123
|
+
let text = await info.file.text();
|
|
124
|
+
// Strip BOM
|
|
125
|
+
if (text.charCodeAt(0) === 0xFEFF) {
|
|
126
|
+
text = text.slice(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const rows = parseCSV(text);
|
|
130
|
+
if (rows.length < 2) return;
|
|
126
131
|
|
|
127
|
-
const headers =
|
|
132
|
+
const headers = rows[0];
|
|
128
133
|
const records: Record<string, unknown>[] = [];
|
|
129
134
|
|
|
130
|
-
for (let i = 1; i <
|
|
131
|
-
const values =
|
|
135
|
+
for (let i = 1; i < rows.length; i++) {
|
|
136
|
+
const values = rows[i];
|
|
132
137
|
const record: Record<string, unknown> = {};
|
|
133
138
|
headers.forEach((h, idx) => { record[h] = values[idx] ?? ''; });
|
|
134
139
|
records.push(options.mapData ? (options.mapData(record) as unknown as Record<string, unknown>) : record);
|
|
@@ -151,18 +156,23 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
|
|
|
151
156
|
} else {
|
|
152
157
|
for (let i = 0; i < records.length; i += batchSize) {
|
|
153
158
|
const batch = records.slice(i, i + batchSize);
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
if (provider.createMany) {
|
|
160
|
+
try {
|
|
156
161
|
const res = await provider.createMany({ resource, variables: batch, meta: options.meta });
|
|
157
162
|
succeeded.push(res);
|
|
158
|
-
}
|
|
159
|
-
|
|
163
|
+
} catch (error) {
|
|
164
|
+
errored.push({ request: batch, error });
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// Fallback: loop internal items inside batch securely instead of breaking whole batch
|
|
168
|
+
for (const record of batch) {
|
|
169
|
+
try {
|
|
160
170
|
const res = await provider.create({ resource, variables: record, meta: options.meta });
|
|
161
171
|
succeeded.push(res);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
errored.push({ request: record, error });
|
|
162
174
|
}
|
|
163
175
|
}
|
|
164
|
-
} catch (error) {
|
|
165
|
-
errored.push({ request: batch, error });
|
|
166
176
|
}
|
|
167
177
|
processed += batch.length;
|
|
168
178
|
options.onProgress?.({ totalAmount: records.length, processedAmount: processed });
|
|
@@ -184,34 +194,53 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
|
|
|
184
194
|
};
|
|
185
195
|
}
|
|
186
196
|
|
|
187
|
-
/** Parse
|
|
188
|
-
function
|
|
189
|
-
const
|
|
190
|
-
let
|
|
197
|
+
/** Parse full CSV text respecting quoted multi-line fields */
|
|
198
|
+
function parseCSV(text: string): string[][] {
|
|
199
|
+
const rows: string[][] = [];
|
|
200
|
+
let currentRow: string[] = [];
|
|
201
|
+
let currentVal = '';
|
|
191
202
|
let inQuotes = false;
|
|
192
203
|
|
|
193
|
-
for (let i = 0; i <
|
|
194
|
-
const ch =
|
|
204
|
+
for (let i = 0; i < text.length; i++) {
|
|
205
|
+
const ch = text[i];
|
|
195
206
|
if (inQuotes) {
|
|
196
|
-
if (ch === '"'
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
207
|
+
if (ch === '"') {
|
|
208
|
+
if (i + 1 < text.length && text[i + 1] === '"') {
|
|
209
|
+
currentVal += '"';
|
|
210
|
+
i++; // skip escaped quote
|
|
211
|
+
} else {
|
|
212
|
+
inQuotes = false;
|
|
213
|
+
}
|
|
201
214
|
} else {
|
|
202
|
-
|
|
215
|
+
currentVal += ch;
|
|
203
216
|
}
|
|
204
217
|
} else {
|
|
205
218
|
if (ch === '"') {
|
|
206
219
|
inQuotes = true;
|
|
207
220
|
} else if (ch === ',') {
|
|
208
|
-
|
|
209
|
-
|
|
221
|
+
currentRow.push(currentVal.trim());
|
|
222
|
+
currentVal = '';
|
|
223
|
+
} else if (ch === '\n' || ch === '\r') {
|
|
224
|
+
currentRow.push(currentVal.trim());
|
|
225
|
+
if (currentRow.some(v => v !== '')) {
|
|
226
|
+
rows.push(currentRow);
|
|
227
|
+
}
|
|
228
|
+
currentRow = [];
|
|
229
|
+
currentVal = '';
|
|
230
|
+
if (ch === '\r' && i + 1 < text.length && text[i + 1] === '\n') {
|
|
231
|
+
i++; // consume \n of \r\n
|
|
232
|
+
}
|
|
210
233
|
} else {
|
|
211
|
-
|
|
234
|
+
currentVal += ch;
|
|
212
235
|
}
|
|
213
236
|
}
|
|
214
237
|
}
|
|
215
|
-
|
|
216
|
-
|
|
238
|
+
|
|
239
|
+
if (currentVal || currentRow.length > 0) {
|
|
240
|
+
currentRow.push(currentVal.trim());
|
|
241
|
+
if (currentRow.some(v => v !== '')) {
|
|
242
|
+
rows.push(currentRow);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return rows;
|
|
217
246
|
}
|
package/src/form-hooks.svelte.ts
CHANGED
|
@@ -275,6 +275,14 @@ export function useForm<
|
|
|
275
275
|
let queryInitializedId: string | number | undefined = undefined;
|
|
276
276
|
{
|
|
277
277
|
$effect.pre(() => {
|
|
278
|
+
// Discard previous fetch lock and default when switched strictly back to create
|
|
279
|
+
if (action === 'create' && currentId === undefined && queryInitializedId !== undefined) {
|
|
280
|
+
queryInitializedId = undefined;
|
|
281
|
+
const def = (options.defaultValues ?? {}) as TVariables;
|
|
282
|
+
values = { ...def };
|
|
283
|
+
initialValues = { ...def };
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
278
286
|
if (queryInitializedId === currentId) return;
|
|
279
287
|
const data = query.data as Record<string, unknown> | undefined;
|
|
280
288
|
const pk = getResource(resource).primaryKey ?? 'id';
|
package/src/hooks.svelte.ts
CHANGED
|
@@ -23,10 +23,10 @@ export type { OvertimeResult, OvertimeOptions, NotificationConfig } from './hook
|
|
|
23
23
|
|
|
24
24
|
import { createQuery, createInfiniteQuery, createMutation, useQueryClient } from '@tanstack/svelte-query';
|
|
25
25
|
import { getAdminOptions } from './options.svelte';
|
|
26
|
-
import { getDataProviderForResource, getDataProvider } from './context.svelte';
|
|
26
|
+
import { getDataProviderForResource, getDataProvider, getLiveProvider } from './context.svelte';
|
|
27
27
|
import { useParsed } from './useParsed.svelte';
|
|
28
|
-
import { createOvertimeTracker, fireSuccessNotification, fireErrorNotification } from './hook-utils.svelte';
|
|
29
|
-
import type { NotificationConfig, OvertimeOptions } from './hook-utils.svelte';
|
|
28
|
+
import { createOvertimeTracker, createLiveSubscription, fireSuccessNotification, fireErrorNotification } from './hook-utils.svelte';
|
|
29
|
+
import type { NotificationConfig, OvertimeOptions, LiveSubscriptionParams } from './hook-utils.svelte';
|
|
30
30
|
import type { BaseRecord, HttpError, Pagination, Sort, Filter, DataProvider, KnownResources } from './types';
|
|
31
31
|
import type { LiveMode, LiveEvent } from './live.svelte';
|
|
32
32
|
import { useList } from './query-hooks.svelte';
|
|
@@ -52,13 +52,13 @@ export interface UseInfiniteListOptions<TData extends BaseRecord = BaseRecord, T
|
|
|
52
52
|
|
|
53
53
|
export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseInfiniteListOptions<TData, TError> = {}) {
|
|
54
54
|
const parsed = useParsed();
|
|
55
|
-
const resource = options.resource ?? parsed.resource ?? '';
|
|
56
55
|
const adminOptions = getAdminOptions();
|
|
57
56
|
|
|
58
57
|
const query = createInfiniteQuery<{ data: TData[]; total: number }, TError>(() => {
|
|
58
|
+
const resource = options.resource ?? parsed.resource ?? '';
|
|
59
59
|
const provider = getDataProviderForResource(resource, options.dataProviderName);
|
|
60
60
|
return {
|
|
61
|
-
queryKey: [resource, 'infiniteList', options.sorters, options.filters, options.meta],
|
|
61
|
+
queryKey: [resource, 'infiniteList', options.pagination?.pageSize, options.sorters, options.filters, options.meta],
|
|
62
62
|
queryFn: async ({ pageParam = 1 }) => {
|
|
63
63
|
const result = await provider.getList<TData>({
|
|
64
64
|
resource,
|
|
@@ -82,6 +82,25 @@ export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError =
|
|
|
82
82
|
|
|
83
83
|
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
84
84
|
|
|
85
|
+
createLiveSubscription((): LiveSubscriptionParams => ({
|
|
86
|
+
resource: options.resource ?? parsed.resource ?? '',
|
|
87
|
+
liveProvider: getLiveProvider(),
|
|
88
|
+
liveMode: options.liveMode ?? adminOptions.liveMode,
|
|
89
|
+
onLiveEvent: (e: LiveEvent) => {
|
|
90
|
+
options.onLiveEvent?.(e);
|
|
91
|
+
adminOptions.onLiveEvent?.(e);
|
|
92
|
+
},
|
|
93
|
+
enabled: options.queryOptions?.enabled ?? true,
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
$effect(() => {
|
|
97
|
+
if (query.isSuccess && options.successNotification) {
|
|
98
|
+
fireSuccessNotification(options.successNotification, '', query.data, undefined, options.resource ?? parsed.resource ?? '');
|
|
99
|
+
} else if (query.isError) {
|
|
100
|
+
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
85
104
|
return { query, get overtime() { return overtime; } };
|
|
86
105
|
}
|
|
87
106
|
|
package/src/i18n.svelte.ts
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
const locales: Record<string, Record<string, string>> = {
|
|
6
6
|
'zh-CN': {
|
|
7
|
+
'common.list': '列表',
|
|
8
|
+
'common.prev': '上一页',
|
|
9
|
+
'common.pageOf': '第 {page} 页,共 {total} 页',
|
|
10
|
+
'common.areYouSure': '确认操作?',
|
|
11
|
+
'common.backToList': '返回列表',
|
|
12
|
+
'common.refresh': '刷新',
|
|
13
|
+
'common.importData': '导入数据',
|
|
14
|
+
'common.upload': '上传',
|
|
15
|
+
'common.returnHome': '返回首页',
|
|
7
16
|
// Common
|
|
8
17
|
'common.save': '保存',
|
|
9
18
|
'common.cancel': '取消',
|
|
@@ -242,6 +251,15 @@ const locales: Record<string, Record<string, string>> = {
|
|
|
242
251
|
'profile.updatePassword': '更新密码',
|
|
243
252
|
},
|
|
244
253
|
'en': {
|
|
254
|
+
'common.list': 'List',
|
|
255
|
+
'common.prev': 'Prev',
|
|
256
|
+
'common.pageOf': 'Page {page} of {total}',
|
|
257
|
+
'common.areYouSure': 'Are you sure?',
|
|
258
|
+
'common.backToList': 'Back to List',
|
|
259
|
+
'common.refresh': 'Refresh',
|
|
260
|
+
'common.importData': 'Import data',
|
|
261
|
+
'common.upload': 'Upload',
|
|
262
|
+
'common.returnHome': 'Return to Home',
|
|
245
263
|
// Common
|
|
246
264
|
'common.save': 'Save',
|
|
247
265
|
'common.cancel': 'Cancel',
|
package/src/live-websocket.ts
CHANGED
|
@@ -27,6 +27,7 @@ export function createWebSocketLiveProvider(options: WebSocketLiveProviderOption
|
|
|
27
27
|
|
|
28
28
|
type Callback = (event: LiveEvent) => void;
|
|
29
29
|
const subscribers = new Map<string, Set<Callback>>();
|
|
30
|
+
const lastParams = new Map<string, Record<string, unknown> | undefined>();
|
|
30
31
|
let ws: WebSocket | null = null;
|
|
31
32
|
let reconnectAttempts = 0;
|
|
32
33
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -51,6 +52,14 @@ export function createWebSocketLiveProvider(options: WebSocketLiveProviderOption
|
|
|
51
52
|
status = 'connected';
|
|
52
53
|
reconnectAttempts = 0;
|
|
53
54
|
options.onOpen?.();
|
|
55
|
+
|
|
56
|
+
// Resend subscriptions upon reconnects
|
|
57
|
+
for (const [res, callbacks] of subscribers.entries()) {
|
|
58
|
+
if (callbacks.size > 0) {
|
|
59
|
+
const liveParams = lastParams.get(res);
|
|
60
|
+
ws?.send(JSON.stringify({ type: 'SUBSCRIBE', resource: res, liveParams }));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
54
63
|
};
|
|
55
64
|
|
|
56
65
|
ws.onmessage = (msgEvent) => {
|
|
@@ -107,22 +116,31 @@ export function createWebSocketLiveProvider(options: WebSocketLiveProviderOption
|
|
|
107
116
|
|
|
108
117
|
|
|
109
118
|
return {
|
|
110
|
-
subscribe({ resource, callback }) {
|
|
119
|
+
subscribe({ resource, liveParams, callback }) {
|
|
111
120
|
if (!subscribers.has(resource)) {
|
|
112
121
|
subscribers.set(resource, new Set());
|
|
113
122
|
}
|
|
114
123
|
subscribers.get(resource)!.add(callback);
|
|
124
|
+
lastParams.set(resource, liveParams);
|
|
115
125
|
|
|
116
126
|
// Ensure connection is open
|
|
117
127
|
if (status === 'disconnected' && typeof WebSocket !== 'undefined') {
|
|
118
128
|
connect();
|
|
129
|
+
} else if (status === 'connected' && ws && ws.readyState === WebSocket.OPEN) {
|
|
130
|
+
// If already connected, dispatch subscription payload dynamically
|
|
131
|
+
ws.send(JSON.stringify({ type: 'SUBSCRIBE', resource, liveParams }));
|
|
119
132
|
}
|
|
120
133
|
|
|
121
134
|
return () => {
|
|
122
135
|
const callbacks = subscribers.get(resource);
|
|
123
136
|
if (callbacks) {
|
|
124
137
|
callbacks.delete(callback);
|
|
125
|
-
if (callbacks.size === 0)
|
|
138
|
+
if (callbacks.size === 0) {
|
|
139
|
+
subscribers.delete(resource);
|
|
140
|
+
if (status === 'connected' && ws && ws.readyState === WebSocket.OPEN) {
|
|
141
|
+
ws.send(JSON.stringify({ type: 'UNSUBSCRIBE', resource }));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
126
144
|
}
|
|
127
145
|
// Auto-disconnect if no subscribers left
|
|
128
146
|
if (subscribers.size === 0) disconnect();
|
|
@@ -41,6 +41,8 @@ function extendQuery<Q extends object, E extends Record<string, unknown>>(
|
|
|
41
41
|
|
|
42
42
|
// ─── useList ───────────────────────────────────────────────────
|
|
43
43
|
|
|
44
|
+
export type MaybeGetter<T> = T | (() => T);
|
|
45
|
+
|
|
44
46
|
export interface UseListOptions<TData extends BaseRecord = BaseRecord, TError = HttpError> {
|
|
45
47
|
resource?: KnownResources;
|
|
46
48
|
pagination?: Pagination;
|
|
@@ -57,51 +59,58 @@ export interface UseListOptions<TData extends BaseRecord = BaseRecord, TError =
|
|
|
57
59
|
overtimeOptions?: OvertimeOptions;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
export function useList<TData extends BaseRecord = BaseRecord, TError = HttpError>(
|
|
62
|
+
export function useList<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseListOptions<TData, TError>> = {}) {
|
|
61
63
|
const parsed = useParsed();
|
|
62
|
-
const resource = options.resource ?? parsed.resource ?? '';
|
|
63
64
|
const adminOptions = getAdminOptions();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
65
|
+
|
|
66
|
+
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
67
|
+
const getResource = () => getOptions().resource ?? parsed.resource ?? '';
|
|
67
68
|
|
|
68
|
-
const query = createQuery<GetListResult<TData>, TError>(() =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const query = createQuery<GetListResult<TData>, TError>(() => {
|
|
70
|
+
const opts = getOptions();
|
|
71
|
+
const resource = getResource();
|
|
72
|
+
const provider = getDataProviderForResource(resource, opts.dataProviderName);
|
|
73
|
+
const queryOptions = opts.queryOptions;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
queryKey: [resource, 'list', opts.pagination, opts.sorters, opts.filters, opts.meta],
|
|
77
|
+
queryFn: async () => provider.getList<TData>({
|
|
72
78
|
resource,
|
|
73
|
-
pagination:
|
|
74
|
-
sorters:
|
|
75
|
-
filters:
|
|
76
|
-
meta:
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}));
|
|
79
|
+
pagination: opts.pagination,
|
|
80
|
+
sorters: opts.sorters,
|
|
81
|
+
filters: opts.filters,
|
|
82
|
+
meta: opts.meta,
|
|
83
|
+
}),
|
|
84
|
+
enabled: queryOptions?.enabled ?? true,
|
|
85
|
+
staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
86
|
+
gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
|
|
87
|
+
refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
85
90
|
|
|
86
|
-
const overtime = createOvertimeTracker(() => query.isLoading,
|
|
91
|
+
const overtime = createOvertimeTracker(() => query.isLoading, typeof optionsOrGetter === 'function' ? optionsOrGetter().overtimeOptions : optionsOrGetter.overtimeOptions ?? adminOptions.overtime);
|
|
87
92
|
|
|
88
|
-
createLiveSubscription((): LiveSubscriptionParams =>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
createLiveSubscription((): LiveSubscriptionParams => {
|
|
94
|
+
const opts = getOptions();
|
|
95
|
+
return {
|
|
96
|
+
resource: getResource(),
|
|
97
|
+
liveProvider: getLiveProvider(),
|
|
98
|
+
liveMode: opts.liveMode ?? adminOptions.liveMode,
|
|
99
|
+
onLiveEvent: (e: LiveEvent) => {
|
|
100
|
+
opts.onLiveEvent?.(e);
|
|
101
|
+
adminOptions.onLiveEvent?.(e);
|
|
102
|
+
},
|
|
103
|
+
liveParams: opts.liveParams,
|
|
104
|
+
enabled: opts.queryOptions?.enabled ?? true,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
99
107
|
|
|
100
108
|
$effect(() => {
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
const opts = getOptions();
|
|
110
|
+
if (query.isSuccess && opts.successNotification) {
|
|
111
|
+
fireSuccessNotification(opts.successNotification, '', query.data, undefined, getResource());
|
|
103
112
|
} else if (query.isError) {
|
|
104
|
-
fireErrorNotification(
|
|
113
|
+
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error);
|
|
105
114
|
}
|
|
106
115
|
});
|
|
107
116
|
|
|
@@ -124,38 +133,47 @@ export interface UseOneOptions<TData extends BaseRecord = BaseRecord, TError = H
|
|
|
124
133
|
overtimeOptions?: OvertimeOptions;
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError>(
|
|
136
|
+
export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}) {
|
|
128
137
|
const parsed = useParsed();
|
|
129
|
-
const resource = options.resource ?? parsed.resource ?? '';
|
|
130
|
-
const id = options.id ?? parsed.id;
|
|
131
138
|
const adminOptions = getAdminOptions();
|
|
132
|
-
|
|
139
|
+
|
|
140
|
+
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
141
|
+
const getResource = () => getOptions().resource ?? parsed.resource ?? '';
|
|
142
|
+
const getId = () => getOptions().id ?? parsed.id;
|
|
133
143
|
|
|
134
|
-
const
|
|
144
|
+
const query = createQuery<GetOneResult<TData>, TError>(() => {
|
|
145
|
+
const opts = getOptions();
|
|
146
|
+
const resource = getResource();
|
|
147
|
+
const id = getId();
|
|
148
|
+
const provider = getDataProviderForResource(resource, opts.dataProviderName);
|
|
149
|
+
const queryOptions = opts.queryOptions;
|
|
135
150
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
return {
|
|
152
|
+
queryKey: [resource, 'one', id, opts.meta],
|
|
153
|
+
queryFn: async () => {
|
|
154
|
+
if (id == null) throw new Error('useOne requires an id');
|
|
155
|
+
const result = await provider.getOne<TData>({
|
|
156
|
+
resource,
|
|
157
|
+
id,
|
|
158
|
+
meta: opts.meta,
|
|
159
|
+
});
|
|
160
|
+
return result;
|
|
161
|
+
},
|
|
162
|
+
enabled: (queryOptions?.enabled ?? true) && id != null,
|
|
163
|
+
staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
164
|
+
gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
|
|
165
|
+
refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
|
|
166
|
+
};
|
|
167
|
+
});
|
|
151
168
|
|
|
152
|
-
const overtime = createOvertimeTracker(() => query.isLoading,
|
|
169
|
+
const overtime = createOvertimeTracker(() => query.isLoading, typeof optionsOrGetter === 'function' ? optionsOrGetter().overtimeOptions : optionsOrGetter.overtimeOptions ?? adminOptions.overtime);
|
|
153
170
|
|
|
154
171
|
$effect(() => {
|
|
155
|
-
|
|
156
|
-
|
|
172
|
+
const opts = getOptions();
|
|
173
|
+
if (query.isSuccess && opts.successNotification) {
|
|
174
|
+
fireSuccessNotification(opts.successNotification, '', query.data, undefined, getResource());
|
|
157
175
|
} else if (query.isError) {
|
|
158
|
-
fireErrorNotification(
|
|
176
|
+
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error);
|
|
159
177
|
}
|
|
160
178
|
});
|
|
161
179
|
|
|
@@ -164,17 +182,23 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
164
182
|
|
|
165
183
|
// ─── useShow ──────────────────────────────────────────────────
|
|
166
184
|
export function useShow<TData extends BaseRecord = BaseRecord, TError = HttpError>(
|
|
167
|
-
|
|
185
|
+
optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}
|
|
168
186
|
) {
|
|
169
187
|
const parsed = useParsed();
|
|
170
|
-
|
|
188
|
+
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
189
|
+
|
|
190
|
+
let _showId = $state<string | number | undefined>(undefined);
|
|
191
|
+
|
|
192
|
+
// showId prioritizes explicit setShowId, then options.id, then URL id.
|
|
193
|
+
const showId = $derived(_showId ?? getOptions().id ?? parsed.id);
|
|
171
194
|
|
|
172
|
-
function setShowId(id: string | number) {
|
|
195
|
+
function setShowId(id: string | number) { _showId = id; }
|
|
173
196
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
197
|
+
// We wrap it in a getter so useOne evaluates it dynamically.
|
|
198
|
+
const result = useOne<TData, TError>(() => ({
|
|
199
|
+
...getOptions(),
|
|
200
|
+
id: showId,
|
|
201
|
+
}));
|
|
178
202
|
|
|
179
203
|
return extendQuery(result, () => ({ showId, setShowId }));
|
|
180
204
|
}
|
|
@@ -195,30 +219,38 @@ export interface UseManyOptions<TData extends BaseRecord = BaseRecord, TError =
|
|
|
195
219
|
overtimeOptions?: OvertimeOptions;
|
|
196
220
|
}
|
|
197
221
|
|
|
198
|
-
export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(
|
|
199
|
-
const { resource, ids, meta, dataProviderName, queryOptions } = options;
|
|
222
|
+
export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseManyOptions<TData, TError>>) {
|
|
200
223
|
const adminOptions = getAdminOptions();
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
const query = createQuery<GetManyResult<TData>, TError>(() =>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
225
|
+
|
|
226
|
+
const query = createQuery<GetManyResult<TData>, TError>(() => {
|
|
227
|
+
const opts = getOptions();
|
|
228
|
+
const { resource, ids, meta, dataProviderName, queryOptions } = opts;
|
|
229
|
+
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
queryKey: [resource, 'many', ids, meta],
|
|
233
|
+
queryFn: async () => {
|
|
234
|
+
if (!ids.length) return { data: [] };
|
|
235
|
+
if (provider.getMany) {
|
|
236
|
+
return provider.getMany<TData>({ resource, ids, meta });
|
|
237
|
+
}
|
|
238
|
+
const results = await Promise.all(ids.map(id => provider.getOne<TData>({ resource, id, meta })));
|
|
239
|
+
return { data: results.map(r => r.data) };
|
|
240
|
+
},
|
|
241
|
+
enabled: (queryOptions?.enabled ?? true) && ids.length > 0,
|
|
242
|
+
staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
243
|
+
gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
|
|
244
|
+
refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
|
|
245
|
+
};
|
|
246
|
+
});
|
|
216
247
|
|
|
217
|
-
const overtime = createOvertimeTracker(() => query.isLoading,
|
|
248
|
+
const overtime = createOvertimeTracker(() => query.isLoading, typeof optionsOrGetter === 'function' ? optionsOrGetter().overtimeOptions : optionsOrGetter.overtimeOptions ?? adminOptions.overtime);
|
|
218
249
|
|
|
219
250
|
$effect(() => {
|
|
220
|
-
|
|
221
|
-
|
|
251
|
+
const opts = getOptions();
|
|
252
|
+
if (query.isSuccess && opts.successNotification) fireSuccessNotification(opts.successNotification, '', query.data, undefined, opts.resource);
|
|
253
|
+
else if (query.isError) fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error);
|
|
222
254
|
});
|
|
223
255
|
|
|
224
256
|
return extendQuery(query, () => ({ overtime }));
|
|
@@ -87,11 +87,11 @@ export function useNavigation() {
|
|
|
87
87
|
const parsed = useParsed();
|
|
88
88
|
|
|
89
89
|
return {
|
|
90
|
-
create: (resource?: string) => go({ type: 'push', resource: resource ?? parsed.resource, action: 'create' }),
|
|
91
|
-
edit: (resource?: string, id?: string | number) => go({ type: 'push', resource: resource ?? parsed.resource, action: 'edit', id: id ?? parsed.id }),
|
|
92
|
-
clone: (resource?: string, id?: string | number) => go({ type: 'push', resource: resource ?? parsed.resource, action: 'clone', id: id ?? parsed.id }),
|
|
93
|
-
show: (resource?: string, id?: string | number) => go({ type: 'push', resource: resource ?? parsed.resource, action: 'show', id: id ?? parsed.id }),
|
|
94
|
-
list: (resource?: string) => go({ type: 'push', resource: resource ?? parsed.resource, action: 'list' }),
|
|
90
|
+
create: (resource?: string) => go({ type: 'push', resource: resource ?? parsed.resourcePath ?? parsed.resource, action: 'create' }),
|
|
91
|
+
edit: (resource?: string, id?: string | number) => go({ type: 'push', resource: resource ?? parsed.resourcePath ?? parsed.resource, action: 'edit', id: id ?? parsed.id }),
|
|
92
|
+
clone: (resource?: string, id?: string | number) => go({ type: 'push', resource: resource ?? parsed.resourcePath ?? parsed.resource, action: 'clone', id: id ?? parsed.id }),
|
|
93
|
+
show: (resource?: string, id?: string | number) => go({ type: 'push', resource: resource ?? parsed.resourcePath ?? parsed.resource, action: 'show', id: id ?? parsed.id }),
|
|
94
|
+
list: (resource?: string) => go({ type: 'push', resource: resource ?? parsed.resourcePath ?? parsed.resource, action: 'list' }),
|
|
95
95
|
push: (url: string) => go({ to: url, type: 'push' }),
|
|
96
96
|
replace: (url: string) => go({ to: url, type: 'replace' }),
|
|
97
97
|
goBack: useBack(),
|
|
@@ -2,7 +2,7 @@ import { useParsed } from './useParsed.svelte';
|
|
|
2
2
|
import { useList } from './query-hooks.svelte';
|
|
3
3
|
import { readURLState, writeURLState } from './url-sync';
|
|
4
4
|
import type { Pagination, Sort, Filter, BaseRecord, HttpError, KnownResources, GetListResult } from './types';
|
|
5
|
-
import type { UseListOptions } from './query-hooks.svelte';
|
|
5
|
+
import type { UseListOptions, MaybeGetter } from './query-hooks.svelte';
|
|
6
6
|
|
|
7
7
|
export type FilterSetMode = 'merge' | 'replace';
|
|
8
8
|
|
|
@@ -27,24 +27,27 @@ export function useTable<
|
|
|
27
27
|
TQueryFnData extends BaseRecord = BaseRecord,
|
|
28
28
|
TError = HttpError,
|
|
29
29
|
TSearchVariables = Record<string, unknown>
|
|
30
|
-
>(
|
|
30
|
+
>(optionsOrGetter: MaybeGetter<UseTableOptions<TQueryFnData, TError, TSearchVariables>> = {}) {
|
|
31
|
+
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
31
32
|
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
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
34
|
+
// Use a local derived to compute these since they are used to initialize state
|
|
35
|
+
// Even if options change, init values are only used once, which makes sense for initial state
|
|
36
|
+
const initialOpts = getOptions();
|
|
37
|
+
|
|
38
|
+
const paginationMode = $derived(getOptions().pagination?.mode ?? 'server');
|
|
39
|
+
const sortersMode = $derived(getOptions().sorters?.mode ?? 'server');
|
|
40
|
+
const filtersMode = $derived(getOptions().filters?.mode ?? 'server');
|
|
41
|
+
const filterDefaultBehavior = $derived(getOptions().filters?.defaultBehavior ?? 'replace');
|
|
39
42
|
|
|
40
|
-
const permanentSorters =
|
|
41
|
-
const permanentFilters =
|
|
43
|
+
const permanentSorters = $derived(getOptions().sorters?.permanent ?? []);
|
|
44
|
+
const permanentFilters = $derived(getOptions().filters?.permanent ?? []);
|
|
42
45
|
|
|
43
|
-
let initPagination =
|
|
44
|
-
let initSorters =
|
|
45
|
-
let initFilters =
|
|
46
|
+
let initPagination = initialOpts.pagination ?? { current: 1, pageSize: 10 };
|
|
47
|
+
let initSorters = initialOpts.sorters?.initial ?? [];
|
|
48
|
+
let initFilters = initialOpts.filters?.initial ?? [];
|
|
46
49
|
|
|
47
|
-
if (syncWithLocation && typeof window !== 'undefined') {
|
|
50
|
+
if (initialOpts.syncWithLocation && typeof window !== 'undefined') {
|
|
48
51
|
const urlState = readURLState();
|
|
49
52
|
if (urlState.page || urlState.pageSize) {
|
|
50
53
|
initPagination = {
|
|
@@ -73,14 +76,18 @@ export function useTable<
|
|
|
73
76
|
? pagination
|
|
74
77
|
: { current: 1, pageSize: 999999, mode: paginationMode as 'client' | 'off' });
|
|
75
78
|
|
|
76
|
-
const tableQueryInfo = useList<TQueryFnData, TError>({
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
const tableQueryInfo = useList<TQueryFnData, TError>(() => {
|
|
80
|
+
const currentOpts = getOptions();
|
|
81
|
+
const { meta, syncWithLocation, dataProviderName, pagination: _p, sorters: _s, filters: _f, ...restOptions } = currentOpts;
|
|
82
|
+
return {
|
|
83
|
+
resource: currentOpts.resource ?? parsed.resource ?? '',
|
|
84
|
+
pagination: queryPagination,
|
|
85
|
+
sorters: querySorters,
|
|
86
|
+
filters: queryFilters,
|
|
87
|
+
meta,
|
|
88
|
+
dataProviderName,
|
|
89
|
+
...restOptions
|
|
90
|
+
};
|
|
84
91
|
});
|
|
85
92
|
|
|
86
93
|
const query = tableQueryInfo;
|
|
@@ -110,7 +117,7 @@ export function useTable<
|
|
|
110
117
|
function setPage(page: number) { pagination = { ...pagination, current: page }; }
|
|
111
118
|
function setPageSize(size: number) { pagination = { ...pagination, pageSize: size, current: 1 }; }
|
|
112
119
|
|
|
113
|
-
if (syncWithLocation && typeof window !== 'undefined') {
|
|
120
|
+
if (initialOpts.syncWithLocation && typeof window !== 'undefined') {
|
|
114
121
|
$effect(() => {
|
|
115
122
|
writeURLState({
|
|
116
123
|
page: pagination.current,
|
package/src/theme.svelte.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// - 'dark-first': adds 'light' class for light mode (dark-first)
|
|
6
6
|
|
|
7
7
|
export type ThemeMode = 'light' | 'dark' | 'system';
|
|
8
|
-
export type ColorTheme = 'blue' | 'green' | 'rose' | 'orange' | 'violet' | '
|
|
8
|
+
export type ColorTheme = 'blue' | 'green' | 'rose' | 'orange' | 'violet' | 'neutral';
|
|
9
9
|
|
|
10
10
|
/** Controls how the theme class is applied to <html> */
|
|
11
11
|
export type ThemeStrategy = 'standard' | 'dark-first';
|
package/src/useCan.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// useCan — reactive permission check hook with TanStack Query integration
|
|
2
2
|
|
|
3
3
|
import { createQuery } from '@tanstack/svelte-query';
|
|
4
|
-
import { canAccessAsync } from './permissions';
|
|
4
|
+
import { canAccessAsync, getAccessControlProvider } from './permissions';
|
|
5
5
|
import type { Action, CanResult } from './permissions';
|
|
6
6
|
|
|
7
7
|
export interface UseCanOptions {
|
|
@@ -42,7 +42,11 @@ export function useCan(options: () => UseCanOptions): UseCanResult {
|
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
return {
|
|
45
|
-
get allowed() {
|
|
45
|
+
get allowed() {
|
|
46
|
+
const p = getAccessControlProvider();
|
|
47
|
+
if (!p) return true;
|
|
48
|
+
return (query.data as CanResult | undefined)?.can ?? false;
|
|
49
|
+
},
|
|
46
50
|
get reason() { return (query.data as CanResult | undefined)?.reason; },
|
|
47
51
|
get isLoading() { return query.isLoading; },
|
|
48
52
|
};
|
package/src/useParsed.svelte.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { getResources } from './context.svelte';
|
|
|
5
5
|
|
|
6
6
|
interface ParsedRoute {
|
|
7
7
|
resource?: string;
|
|
8
|
+
resourcePath?: string;
|
|
8
9
|
action?: 'list' | 'create' | 'edit' | 'show' | 'clone';
|
|
9
10
|
id?: string;
|
|
10
11
|
params: Record<string, string>;
|
|
@@ -60,6 +61,7 @@ export function useParsed(): ParsedRoute {
|
|
|
60
61
|
|
|
61
62
|
if (resourceIndex !== -1) {
|
|
62
63
|
result.resource = segments[resourceIndex];
|
|
64
|
+
result.resourcePath = segments.slice(0, resourceIndex + 1).join('/');
|
|
63
65
|
|
|
64
66
|
// Add parent path params based on `<parentResource>/<parentId>` structure
|
|
65
67
|
for (let i = 0; i < resourceIndex; i += 2) {
|
|
@@ -92,6 +94,7 @@ export function useParsed(): ParsedRoute {
|
|
|
92
94
|
} else {
|
|
93
95
|
// Fallback if resource definitions are not loaded yet
|
|
94
96
|
result.resource = segments[0];
|
|
97
|
+
result.resourcePath = segments[0];
|
|
95
98
|
|
|
96
99
|
if (segments.length === 1) {
|
|
97
100
|
result.action = 'list';
|
|
@@ -117,6 +120,7 @@ export function useParsed(): ParsedRoute {
|
|
|
117
120
|
|
|
118
121
|
return {
|
|
119
122
|
get resource() { return parsed.resource; },
|
|
123
|
+
get resourcePath() { return parsed.resourcePath; },
|
|
120
124
|
get action() { return parsed.action; },
|
|
121
125
|
get id() { return parsed.id; },
|
|
122
126
|
get params() { return parsed.params; },
|
|
@@ -7,6 +7,8 @@ export interface UseStepsFormOptions<
|
|
|
7
7
|
TData extends BaseRecord = BaseRecord,
|
|
8
8
|
TError = HttpError,
|
|
9
9
|
> extends UseFormOptions<TVariables, TData, TError> {
|
|
10
|
+
/** Total number of steps. Defaults to 1 */
|
|
11
|
+
stepsCount?: number;
|
|
10
12
|
/** Default step index. Starts at 0. */
|
|
11
13
|
defaultStep?: number;
|
|
12
14
|
/** Validate the form when navigating back? */
|
|
@@ -17,15 +19,20 @@ export interface UseStepsFormReturn<
|
|
|
17
19
|
TVariables extends Record<string, unknown> = Record<string, unknown>,
|
|
18
20
|
TData extends BaseRecord = BaseRecord,
|
|
19
21
|
> extends UseFormReturn<TVariables, TData> {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
steps: {
|
|
23
|
+
currentStep: number;
|
|
24
|
+
totalSteps: number;
|
|
25
|
+
canGoNext: boolean;
|
|
26
|
+
canGoPrev: boolean;
|
|
27
|
+
gotoStep: (step: number) => void;
|
|
28
|
+
nextStep: () => void;
|
|
29
|
+
prevStep: () => void;
|
|
30
|
+
};
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
/**
|
|
27
34
|
* useStepsForm
|
|
28
|
-
* Extends `useForm` with
|
|
35
|
+
* Extends `useForm` with a nested `steps` object tracking navigation state.
|
|
29
36
|
*/
|
|
30
37
|
export function useStepsForm<
|
|
31
38
|
TVariables extends Record<string, unknown> = Record<string, unknown>,
|
|
@@ -35,33 +42,36 @@ export function useStepsForm<
|
|
|
35
42
|
const form = useForm<TVariables, TData, TError>(options);
|
|
36
43
|
|
|
37
44
|
let currentStep = $state(options.defaultStep ?? 0);
|
|
45
|
+
const totalSteps = options.stepsCount ?? 1;
|
|
38
46
|
|
|
39
47
|
function gotoStep(step: number) {
|
|
40
|
-
currentStep = Math.max(0, step);
|
|
48
|
+
currentStep = Math.max(0, Math.min(step, totalSteps - 1));
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
function
|
|
44
|
-
gotoStep(currentStep + 1);
|
|
51
|
+
function nextStep() {
|
|
52
|
+
if (currentStep < totalSteps - 1) gotoStep(currentStep + 1);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
function
|
|
48
|
-
if (currentStep > 0)
|
|
49
|
-
gotoStep(currentStep - 1);
|
|
50
|
-
}
|
|
55
|
+
function prevStep() {
|
|
56
|
+
if (currentStep > 0) gotoStep(currentStep - 1);
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
// Inject stepping features into the returned useForm object while preserving its reactive getters.
|
|
54
59
|
const result = form as unknown as UseStepsFormReturn<TVariables, TData>;
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
get
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
const steps = {
|
|
62
|
+
get currentStep() { return currentStep; },
|
|
63
|
+
get totalSteps() { return totalSteps; },
|
|
64
|
+
get canGoNext() { return currentStep < totalSteps - 1; },
|
|
65
|
+
get canGoPrev() { return currentStep > 0; },
|
|
66
|
+
gotoStep,
|
|
67
|
+
nextStep,
|
|
68
|
+
prevStep,
|
|
69
|
+
};
|
|
61
70
|
|
|
62
|
-
result
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
Object.defineProperty(result, 'steps', {
|
|
72
|
+
get: () => steps,
|
|
73
|
+
enumerable: true
|
|
74
|
+
});
|
|
65
75
|
|
|
66
76
|
return result;
|
|
67
77
|
}
|