@svadmin/core 0.20.3 → 0.21.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 +17 -7
- package/src/form-hooks.svelte.ts +18 -9
- package/src/hooks.svelte.ts +6 -1
- package/src/i18n.svelte.ts +3 -1
- package/src/live-sse.ts +7 -2
- package/src/live-websocket.ts +1 -4
- package/src/mutation-hooks.svelte.ts +26 -4
- package/src/query-hooks.svelte.ts +49 -12
- package/src/router-provider.ts +5 -3
- package/src/router.ts +8 -2
- package/src/routing-hooks.svelte.ts +3 -2
- package/src/table-hooks.svelte.ts +9 -9
- package/src/table-state.svelte.ts +4 -4
- package/src/theme.svelte.ts +8 -8
- package/src/unsaved-changes.svelte.ts +0 -1
- package/src/useParsed.svelte.ts +7 -1
package/package.json
CHANGED
package/src/auth-hooks.svelte.ts
CHANGED
|
@@ -123,7 +123,9 @@ export function useGetIdentity() {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
if (typeof window !== 'undefined') {
|
|
127
|
+
fetch();
|
|
128
|
+
}
|
|
127
129
|
|
|
128
130
|
return {
|
|
129
131
|
get data() { return data; },
|
|
@@ -141,13 +143,19 @@ export function useIsAuthenticated() {
|
|
|
141
143
|
let isLoading = $state(true);
|
|
142
144
|
|
|
143
145
|
if (provider) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
if (typeof window !== 'undefined') {
|
|
147
|
+
provider.check().then((result: CheckResult) => {
|
|
148
|
+
isAuthenticated = result.authenticated;
|
|
149
|
+
isLoading = false;
|
|
150
|
+
}).catch(() => {
|
|
151
|
+
isAuthenticated = false;
|
|
152
|
+
isLoading = false;
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
// In SSR we assume false to prevent hydration mismatch before check
|
|
148
156
|
isAuthenticated = false;
|
|
149
157
|
isLoading = false;
|
|
150
|
-
}
|
|
158
|
+
}
|
|
151
159
|
} else {
|
|
152
160
|
// No auth provider — treat as authenticated
|
|
153
161
|
isAuthenticated = true;
|
|
@@ -230,7 +238,9 @@ export function usePermissions<T = unknown>() {
|
|
|
230
238
|
});
|
|
231
239
|
}
|
|
232
240
|
|
|
233
|
-
|
|
241
|
+
if (typeof window !== 'undefined') {
|
|
242
|
+
fetch();
|
|
243
|
+
}
|
|
234
244
|
|
|
235
245
|
return {
|
|
236
246
|
get isLoading() { return isLoading; },
|
package/src/form-hooks.svelte.ts
CHANGED
|
@@ -144,8 +144,9 @@ export function useForm<
|
|
|
144
144
|
const action = options.action ?? (parsed.action === 'list' ? 'create' : parsed.action as 'create' | 'edit' | 'clone') ?? 'create';
|
|
145
145
|
let currentId = $state<string | number | undefined>(options.id ?? parsed.id);
|
|
146
146
|
|
|
147
|
+
const defaultRedirectOpt = action === 'clone' ? adminOptions.redirect?.afterClone : action === 'edit' ? adminOptions.redirect?.afterEdit : adminOptions.redirect?.afterCreate;
|
|
147
148
|
const {
|
|
148
|
-
redirect: redirectDefault = 'list',
|
|
149
|
+
redirect: redirectDefault = defaultRedirectOpt ?? 'list',
|
|
149
150
|
successNotification, errorNotification,
|
|
150
151
|
onMutationSuccess, onMutationError,
|
|
151
152
|
meta: hookMeta, queryMeta: hookQueryMeta, mutationMeta: hookMutationMeta,
|
|
@@ -252,7 +253,7 @@ export function useForm<
|
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
// ─── Query (edit/clone mode) ────────────────────────────────────
|
|
255
|
-
const query = (action === 'edit' || action === 'clone')
|
|
256
|
+
const query = (action === 'edit' || action === 'clone')
|
|
256
257
|
? createQuery(() => ({
|
|
257
258
|
queryKey: [resource, 'one', currentId],
|
|
258
259
|
queryFn: async () => {
|
|
@@ -265,17 +266,21 @@ export function useForm<
|
|
|
265
266
|
: null;
|
|
266
267
|
|
|
267
268
|
// Auto-populate values from query data
|
|
268
|
-
let
|
|
269
|
+
let queryInitializedId: string | number | undefined = undefined;
|
|
269
270
|
if (query) {
|
|
270
271
|
$effect.pre(() => {
|
|
271
|
-
if (
|
|
272
|
+
if (queryInitializedId === currentId) return;
|
|
272
273
|
const data = query.data as Record<string, unknown> | undefined;
|
|
273
|
-
if (data) {
|
|
274
|
+
if (data && (currentId == null || String(data.id) === String(currentId) || !data.id)) {
|
|
275
|
+
// In clone mode, strip out identifiers
|
|
276
|
+
const targetData = action === 'clone'
|
|
277
|
+
? Object.fromEntries(Object.entries(data).filter(([k]) => k !== 'id' && k !== '_id'))
|
|
278
|
+
: data;
|
|
274
279
|
// Merge: defaultValues < query data
|
|
275
|
-
const merged = { ...(options.defaultValues ?? {}), ...
|
|
280
|
+
const merged = { ...(options.defaultValues ?? {}), ...targetData } as TVariables;
|
|
276
281
|
values = merged;
|
|
277
282
|
initialValues = { ...merged } as TVariables;
|
|
278
|
-
|
|
283
|
+
queryInitializedId = currentId;
|
|
279
284
|
}
|
|
280
285
|
});
|
|
281
286
|
}
|
|
@@ -291,7 +296,11 @@ export function useForm<
|
|
|
291
296
|
if (successNotification !== false) notify({ type: 'success', message: typeof successNotification === 'string' ? successNotification : t('common.createSuccess') });
|
|
292
297
|
audit({ action: 'create', resource, recordId: String((data.data as Record<string, unknown>).id) });
|
|
293
298
|
onMutationSuccess?.(data);
|
|
294
|
-
if (redirectOverride !== false)
|
|
299
|
+
if (redirectOverride !== false) {
|
|
300
|
+
const newId = (data.data as Record<string, unknown>).id;
|
|
301
|
+
if (newId != null) currentId = newId as string | number;
|
|
302
|
+
doRedirect(redirectOverride ?? redirectDefault);
|
|
303
|
+
}
|
|
295
304
|
},
|
|
296
305
|
onError: (error: Error) => { handleHttpError(error); onMutationError?.(error); },
|
|
297
306
|
}));
|
|
@@ -344,7 +353,7 @@ export function useForm<
|
|
|
344
353
|
let lastAutoSaveError = $state<unknown>(null);
|
|
345
354
|
|
|
346
355
|
function triggerAutoSave() {
|
|
347
|
-
if (!autoSaveOpts?.enabled || action === 'create') return;
|
|
356
|
+
if (!autoSaveOpts?.enabled || action === 'create' || action === 'clone') return;
|
|
348
357
|
if (autoSaveTimer) clearTimeout(autoSaveTimer);
|
|
349
358
|
|
|
350
359
|
autoSaveTimer = setTimeout(async () => {
|
package/src/hooks.svelte.ts
CHANGED
|
@@ -328,7 +328,12 @@ export function useInvalidate() {
|
|
|
328
328
|
queryClient.invalidateQueries();
|
|
329
329
|
return;
|
|
330
330
|
}
|
|
331
|
-
|
|
331
|
+
const scopes = params.invalidates || ['resourceAll'];
|
|
332
|
+
for (const scope of scopes) {
|
|
333
|
+
if (scope === 'resourceAll') queryClient.invalidateQueries({ queryKey: [params.resource] });
|
|
334
|
+
else if (scope === 'detail' && params.id) queryClient.invalidateQueries({ queryKey: [params.resource, 'one', params.id] });
|
|
335
|
+
else if (scope === 'list') queryClient.invalidateQueries({ queryKey: [params.resource, 'list'] });
|
|
336
|
+
}
|
|
332
337
|
};
|
|
333
338
|
}
|
|
334
339
|
|
package/src/i18n.svelte.ts
CHANGED
|
@@ -46,6 +46,7 @@ const locales: Record<string, Record<string, string>> = {
|
|
|
46
46
|
'common.darkMode': '暗色模式',
|
|
47
47
|
'common.lightMode': '亮色模式',
|
|
48
48
|
'common.detail': '详情',
|
|
49
|
+
'common.show': '查看',
|
|
49
50
|
'common.loadFailed': '加载失败: {message}',
|
|
50
51
|
'common.pageNotFound': '页面未找到',
|
|
51
52
|
'common.error': '出错了',
|
|
@@ -283,6 +284,7 @@ const locales: Record<string, Record<string, string>> = {
|
|
|
283
284
|
'common.darkMode': 'Dark mode',
|
|
284
285
|
'common.lightMode': 'Light mode',
|
|
285
286
|
'common.detail': 'Detail',
|
|
287
|
+
'common.show': 'Show',
|
|
286
288
|
'common.loadFailed': 'Load failed: {message}',
|
|
287
289
|
'common.pageNotFound': 'Page not found',
|
|
288
290
|
'common.error': 'Something went wrong',
|
|
@@ -513,7 +515,7 @@ export function t(key: string, params?: Record<string, string | number>): string
|
|
|
513
515
|
|
|
514
516
|
if (params) {
|
|
515
517
|
for (const [k, v] of Object.entries(params)) {
|
|
516
|
-
text = text.
|
|
518
|
+
text = text.replaceAll(`{${k}}`, String(v));
|
|
517
519
|
}
|
|
518
520
|
}
|
|
519
521
|
return text;
|
package/src/live-sse.ts
CHANGED
|
@@ -29,6 +29,7 @@ export function createSSELiveProvider(options: SSELiveProviderOptions): LiveProv
|
|
|
29
29
|
const subscribers = new Map<string, Set<Callback>>();
|
|
30
30
|
let eventSource: EventSource | null = null;
|
|
31
31
|
let status: 'connecting' | 'connected' | 'disconnected' = 'disconnected';
|
|
32
|
+
const registeredListeners = new Set<string>();
|
|
32
33
|
|
|
33
34
|
function connect() {
|
|
34
35
|
if (eventSource) return;
|
|
@@ -64,7 +65,8 @@ export function createSSELiveProvider(options: SSELiveProviderOptions): LiveProv
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function addNamedListener(resource: string) {
|
|
67
|
-
if (!eventSource) return;
|
|
68
|
+
if (!eventSource || registeredListeners.has(resource)) return;
|
|
69
|
+
registeredListeners.add(resource);
|
|
68
70
|
eventSource.addEventListener(resource, ((event: MessageEvent) => {
|
|
69
71
|
try {
|
|
70
72
|
const parsed = JSON.parse(event.data);
|
|
@@ -109,12 +111,15 @@ export function createSSELiveProvider(options: SSELiveProviderOptions): LiveProv
|
|
|
109
111
|
eventSource = null;
|
|
110
112
|
}
|
|
111
113
|
status = 'disconnected';
|
|
114
|
+
registeredListeners.clear();
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
return {
|
|
115
118
|
subscribe({ resource, callback }) {
|
|
119
|
+
let isNewResource = false;
|
|
116
120
|
if (!subscribers.has(resource)) {
|
|
117
121
|
subscribers.set(resource, new Set());
|
|
122
|
+
isNewResource = true;
|
|
118
123
|
}
|
|
119
124
|
subscribers.get(resource)!.add(callback);
|
|
120
125
|
|
|
@@ -123,7 +128,7 @@ export function createSSELiveProvider(options: SSELiveProviderOptions): LiveProv
|
|
|
123
128
|
connect();
|
|
124
129
|
}
|
|
125
130
|
// Add named listener for this resource if connected
|
|
126
|
-
if (eventSource && resource !== '*') {
|
|
131
|
+
if (eventSource && resource !== '*' && isNewResource) {
|
|
127
132
|
addNamedListener(resource);
|
|
128
133
|
}
|
|
129
134
|
|
package/src/live-websocket.ts
CHANGED
|
@@ -104,10 +104,7 @@ export function createWebSocketLiveProvider(options: WebSocketLiveProviderOption
|
|
|
104
104
|
status = 'disconnected';
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
if (typeof WebSocket !== 'undefined') {
|
|
109
|
-
connect();
|
|
110
|
-
}
|
|
107
|
+
|
|
111
108
|
|
|
112
109
|
return {
|
|
113
110
|
subscribe({ resource, callback }) {
|
|
@@ -41,6 +41,7 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
41
41
|
TError,
|
|
42
42
|
UseCreateMutateParams<TVariables>
|
|
43
43
|
>(() => ({
|
|
44
|
+
...options.mutationOptions,
|
|
44
45
|
mutationFn: async (params) => {
|
|
45
46
|
isMutating = true;
|
|
46
47
|
const resName = params.resource ?? defaultResource;
|
|
@@ -55,16 +56,23 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
55
56
|
isMutating = false;
|
|
56
57
|
}
|
|
57
58
|
},
|
|
58
|
-
|
|
59
|
+
onMutate: (options.mutationOptions as any)?.onMutate,
|
|
60
|
+
onSuccess: (data, params, context) => {
|
|
59
61
|
const resName = params.resource ?? defaultResource;
|
|
60
62
|
if (params.invalidates !== false) {
|
|
61
63
|
queryClient.invalidateQueries({ queryKey: [resName] });
|
|
62
64
|
}
|
|
63
65
|
fireSuccessNotification(params.successNotification, 'Created successfully', data.data, params.variables, resName);
|
|
64
66
|
audit({ action: 'create', resource: resName, recordId: String((data.data as Record<string, unknown>).id) });
|
|
67
|
+
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
68
|
+
(options.mutationOptions.onSuccess as Function)(data, params, context);
|
|
69
|
+
}
|
|
65
70
|
},
|
|
66
|
-
onError: (error, params) => {
|
|
71
|
+
onError: (error, params, context) => {
|
|
67
72
|
fireErrorNotification(params.errorNotification, 'Create failed', error);
|
|
73
|
+
if (typeof options.mutationOptions?.onError === 'function') {
|
|
74
|
+
(options.mutationOptions.onError as Function)(error, params, context);
|
|
75
|
+
}
|
|
68
76
|
},
|
|
69
77
|
}));
|
|
70
78
|
|
|
@@ -115,6 +123,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
115
123
|
TError,
|
|
116
124
|
UseUpdateMutateParams<TVariables>
|
|
117
125
|
>(() => ({
|
|
126
|
+
...options.mutationOptions,
|
|
118
127
|
mutationFn: async (params) => {
|
|
119
128
|
isMutating = true;
|
|
120
129
|
const resName = params.resource ?? defaultResource;
|
|
@@ -170,7 +179,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
170
179
|
|
|
171
180
|
return { previousDetail, previousList };
|
|
172
181
|
},
|
|
173
|
-
onSuccess: (data, params) => {
|
|
182
|
+
onSuccess: (data, params, context) => {
|
|
174
183
|
const resName = params.resource ?? defaultResource;
|
|
175
184
|
const targetId = params.id ?? defaultId;
|
|
176
185
|
if (params.invalidates !== false) {
|
|
@@ -178,6 +187,9 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
178
187
|
}
|
|
179
188
|
fireSuccessNotification(params.successNotification, 'Updated successfully', data.data, params.variables, resName);
|
|
180
189
|
audit({ action: 'update', resource: resName, recordId: String(targetId) });
|
|
190
|
+
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
191
|
+
(options.mutationOptions.onSuccess as Function)(data, params, context);
|
|
192
|
+
}
|
|
181
193
|
},
|
|
182
194
|
onError: (error, params, context: unknown) => {
|
|
183
195
|
const resName = params.resource ?? defaultResource;
|
|
@@ -189,6 +201,9 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
189
201
|
}
|
|
190
202
|
if (error instanceof UndoError) return;
|
|
191
203
|
fireErrorNotification(params.errorNotification, 'Update failed', error);
|
|
204
|
+
if (typeof options.mutationOptions?.onError === 'function') {
|
|
205
|
+
(options.mutationOptions.onError as Function)(error, params, context);
|
|
206
|
+
}
|
|
192
207
|
},
|
|
193
208
|
}));
|
|
194
209
|
|
|
@@ -235,6 +250,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
235
250
|
TError,
|
|
236
251
|
UseDeleteMutateParams<TVariables>
|
|
237
252
|
>(() => ({
|
|
253
|
+
...options.mutationOptions,
|
|
238
254
|
mutationFn: async (params) => {
|
|
239
255
|
isMutating = true;
|
|
240
256
|
const resName = params.resource ?? defaultResource;
|
|
@@ -278,7 +294,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
278
294
|
|
|
279
295
|
return { previousList };
|
|
280
296
|
},
|
|
281
|
-
onSuccess: (data, params) => {
|
|
297
|
+
onSuccess: (data, params, context) => {
|
|
282
298
|
const resName = params.resource ?? defaultResource;
|
|
283
299
|
const targetId = params.id ?? defaultId;
|
|
284
300
|
if (params.invalidates !== false) {
|
|
@@ -286,6 +302,9 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
286
302
|
}
|
|
287
303
|
fireSuccessNotification(params.successNotification, 'Deleted successfully', data.data, params.variables, resName);
|
|
288
304
|
audit({ action: 'delete', resource: resName, recordId: String(targetId) });
|
|
305
|
+
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
306
|
+
(options.mutationOptions.onSuccess as Function)(data, params, context);
|
|
307
|
+
}
|
|
289
308
|
},
|
|
290
309
|
onError: (error, params, context: unknown) => {
|
|
291
310
|
const ctx = context as { previousList?: [unknown, unknown][] } | undefined;
|
|
@@ -294,6 +313,9 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
294
313
|
}
|
|
295
314
|
if (error instanceof UndoError) return;
|
|
296
315
|
fireErrorNotification(params.errorNotification, 'Delete failed', error);
|
|
316
|
+
if (typeof options.mutationOptions?.onError === 'function') {
|
|
317
|
+
(options.mutationOptions.onError as Function)(error, params, context);
|
|
318
|
+
}
|
|
297
319
|
},
|
|
298
320
|
}));
|
|
299
321
|
|
|
@@ -16,6 +16,26 @@ import type {
|
|
|
16
16
|
} from './types';
|
|
17
17
|
import type { LiveMode, LiveEvent } from './live.svelte';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Extend a Tanstack Query result with additional properties.
|
|
21
|
+
* The query object is returned as-is, with extra properties accessible via Proxy.
|
|
22
|
+
*/
|
|
23
|
+
function extendQuery<Q extends object, E extends Record<string, unknown>>(
|
|
24
|
+
query: Q,
|
|
25
|
+
extensions: () => E,
|
|
26
|
+
): Q & E {
|
|
27
|
+
return new Proxy(query, {
|
|
28
|
+
get(target, prop, receiver) {
|
|
29
|
+
const ext = extensions();
|
|
30
|
+
if (prop in ext) return ext[prop as keyof E];
|
|
31
|
+
return Reflect.get(target, prop, receiver);
|
|
32
|
+
},
|
|
33
|
+
has(target, prop) {
|
|
34
|
+
return prop in extensions() || Reflect.has(target, prop);
|
|
35
|
+
},
|
|
36
|
+
}) as Q & E;
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
// ─── useList ───────────────────────────────────────────────────
|
|
20
40
|
|
|
21
41
|
export interface UseListOptions<TData extends BaseRecord = BaseRecord, TError = HttpError> {
|
|
@@ -82,10 +102,7 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
82
102
|
}
|
|
83
103
|
});
|
|
84
104
|
|
|
85
|
-
return {
|
|
86
|
-
query,
|
|
87
|
-
get overtime() { return overtime; }
|
|
88
|
-
};
|
|
105
|
+
return extendQuery(query, () => ({ overtime }));
|
|
89
106
|
}
|
|
90
107
|
|
|
91
108
|
// ─── useOne ────────────────────────────────────────────────────
|
|
@@ -131,6 +148,18 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
131
148
|
|
|
132
149
|
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
133
150
|
|
|
151
|
+
createLiveSubscription({
|
|
152
|
+
resource,
|
|
153
|
+
liveProvider: getLiveProvider(),
|
|
154
|
+
liveMode: options.liveMode ?? adminOptions.liveMode,
|
|
155
|
+
onLiveEvent: (e) => {
|
|
156
|
+
options.onLiveEvent?.(e);
|
|
157
|
+
adminOptions.onLiveEvent?.(e);
|
|
158
|
+
},
|
|
159
|
+
liveParams: options.liveParams,
|
|
160
|
+
enabled: (queryOptions?.enabled ?? true) && id != null,
|
|
161
|
+
});
|
|
162
|
+
|
|
134
163
|
$effect(() => {
|
|
135
164
|
if (query.isSuccess && options.successNotification) {
|
|
136
165
|
fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
|
|
@@ -139,8 +168,9 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
139
168
|
}
|
|
140
169
|
});
|
|
141
170
|
|
|
142
|
-
return
|
|
171
|
+
return extendQuery(query, () => ({ overtime }));
|
|
143
172
|
}
|
|
173
|
+
|
|
144
174
|
// ─── useShow ──────────────────────────────────────────────────
|
|
145
175
|
export function useShow<TData extends BaseRecord = BaseRecord, TError = HttpError>(
|
|
146
176
|
options: UseOneOptions<TData, TError> = {}
|
|
@@ -155,12 +185,7 @@ export function useShow<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
155
185
|
get id() { return showId; },
|
|
156
186
|
});
|
|
157
187
|
|
|
158
|
-
return {
|
|
159
|
-
get query() { return result.query; },
|
|
160
|
-
get showId() { return showId; },
|
|
161
|
-
setShowId,
|
|
162
|
-
get overtime() { return result.overtime; },
|
|
163
|
-
};
|
|
188
|
+
return extendQuery(result, () => ({ showId, setShowId }));
|
|
164
189
|
}
|
|
165
190
|
|
|
166
191
|
// ─── useMany ───────────────────────────────────────────────────
|
|
@@ -200,12 +225,24 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
200
225
|
|
|
201
226
|
const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
|
|
202
227
|
|
|
228
|
+
createLiveSubscription({
|
|
229
|
+
resource,
|
|
230
|
+
liveProvider: getLiveProvider(),
|
|
231
|
+
liveMode: options.liveMode ?? adminOptions.liveMode,
|
|
232
|
+
onLiveEvent: (e) => {
|
|
233
|
+
options.onLiveEvent?.(e);
|
|
234
|
+
adminOptions.onLiveEvent?.(e);
|
|
235
|
+
},
|
|
236
|
+
liveParams: options.liveParams,
|
|
237
|
+
enabled: (queryOptions?.enabled ?? true) && ids.length > 0,
|
|
238
|
+
});
|
|
239
|
+
|
|
203
240
|
$effect(() => {
|
|
204
241
|
if (query.isSuccess && options.successNotification) fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
|
|
205
242
|
else if (query.isError) fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
|
|
206
243
|
});
|
|
207
244
|
|
|
208
|
-
return
|
|
245
|
+
return extendQuery(query, () => ({ overtime }));
|
|
209
246
|
}
|
|
210
247
|
|
|
211
248
|
export function useApiUrl(dataProviderName?: string): string {
|
package/src/router-provider.ts
CHANGED
|
@@ -25,15 +25,17 @@ export interface RouterProvider {
|
|
|
25
25
|
export function createHashRouterProvider(): RouterProvider {
|
|
26
26
|
return {
|
|
27
27
|
go({ to, query, type = 'push' }) {
|
|
28
|
-
let url =
|
|
28
|
+
let url = to;
|
|
29
29
|
if (query) {
|
|
30
30
|
const params = new URLSearchParams(query).toString();
|
|
31
31
|
if (params) url += `?${params}`;
|
|
32
32
|
}
|
|
33
33
|
if (type === 'replace') {
|
|
34
|
-
window.location.
|
|
34
|
+
const urlObj = new URL(window.location.href);
|
|
35
|
+
urlObj.hash = url;
|
|
36
|
+
window.history.replaceState(null, '', urlObj.href);
|
|
35
37
|
} else {
|
|
36
|
-
window.location.hash =
|
|
38
|
+
window.location.hash = url;
|
|
37
39
|
}
|
|
38
40
|
},
|
|
39
41
|
back() {
|
package/src/router.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function matchRoute(
|
|
|
56
56
|
export function navigate(path: string, options?: { replaceState?: boolean }): void {
|
|
57
57
|
if (_routerProvider) {
|
|
58
58
|
_routerProvider.go({ to: path, type: options?.replaceState ? 'replace' : 'push' });
|
|
59
|
-
} else {
|
|
59
|
+
} else if (typeof window !== 'undefined') {
|
|
60
60
|
window.location.hash = '#' + path.replace(/^#/, '');
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -64,7 +64,13 @@ export function navigate(path: string, options?: { replaceState?: boolean }): vo
|
|
|
64
64
|
export function currentPath(): string {
|
|
65
65
|
if (_routerProvider) {
|
|
66
66
|
const parsed = _routerProvider.parse();
|
|
67
|
-
|
|
67
|
+
const pathname = parsed.pathname || '/';
|
|
68
|
+
let qs = '';
|
|
69
|
+
if (parsed.params && Object.keys(parsed.params).length > 0) {
|
|
70
|
+
qs = '?' + new URLSearchParams(parsed.params).toString();
|
|
71
|
+
}
|
|
72
|
+
return pathname + qs;
|
|
68
73
|
}
|
|
74
|
+
if (typeof window === 'undefined') return '/';
|
|
69
75
|
return window.location.hash.replace(/^#/, '') || '/';
|
|
70
76
|
}
|
|
@@ -7,13 +7,14 @@ import { useParsed } from './useParsed.svelte';
|
|
|
7
7
|
export function useGetToPath() {
|
|
8
8
|
const routerProvider = getRouterProvider();
|
|
9
9
|
|
|
10
|
-
return (options: { resource?: string; action?: 'list' | 'create' | 'edit' | 'show'; id?: string | number; meta?: Record<string, unknown> }) => {
|
|
10
|
+
return (options: { resource?: string; action?: 'list' | 'create' | 'edit' | 'show' | 'clone'; id?: string | number; meta?: Record<string, unknown> }) => {
|
|
11
11
|
const { resource, action, id } = options;
|
|
12
12
|
if (!resource) return '/';
|
|
13
13
|
let path = `/${resource}`;
|
|
14
14
|
if (action === 'create') path += '/create';
|
|
15
15
|
else if (action === 'edit' && id) path += `/edit/${id}`;
|
|
16
16
|
else if (action === 'show' && id) path += `/show/${id}`;
|
|
17
|
+
else if (action === 'clone' && id) path += `/clone/${id}`;
|
|
17
18
|
|
|
18
19
|
// Apply router mapping if the router provider defines custom path transformers
|
|
19
20
|
if (routerProvider?.go) {
|
|
@@ -30,7 +31,7 @@ export function useGo() {
|
|
|
30
31
|
return (options: { to?: string; query?: Record<string, unknown>; type?: 'push' | 'replace'; resource?: string; action?: 'list' | 'create' | 'edit' | 'show' | 'clone'; id?: string | number }) => {
|
|
31
32
|
let targetUrl = options.to;
|
|
32
33
|
if (!targetUrl && options.resource) {
|
|
33
|
-
targetUrl = getToPath({ resource: options.resource, action: options.action
|
|
34
|
+
targetUrl = getToPath({ resource: options.resource, action: options.action, id: options.id });
|
|
34
35
|
}
|
|
35
36
|
if (!targetUrl) targetUrl = '/';
|
|
36
37
|
|
|
@@ -83,7 +83,7 @@ export function useTable<
|
|
|
83
83
|
...restOptions
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
const query = tableQueryInfo
|
|
86
|
+
const query = tableQueryInfo;
|
|
87
87
|
const overtime = tableQueryInfo.overtime;
|
|
88
88
|
|
|
89
89
|
function setSorters(newSorters: Sort[]) { currentSorters = newSorters; }
|
|
@@ -115,9 +115,9 @@ export function useTable<
|
|
|
115
115
|
writeURLState({
|
|
116
116
|
page: pagination.current,
|
|
117
117
|
pageSize: pagination.pageSize,
|
|
118
|
-
sortField:
|
|
119
|
-
sortOrder:
|
|
120
|
-
filters:
|
|
118
|
+
sortField: currentSorters[0]?.field,
|
|
119
|
+
sortOrder: currentSorters[0]?.order,
|
|
120
|
+
filters: currentFilters,
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
123
|
}
|
|
@@ -126,12 +126,12 @@ export function useTable<
|
|
|
126
126
|
const params = new URLSearchParams();
|
|
127
127
|
if (pagination.current) params.set('page', String(pagination.current));
|
|
128
128
|
if (pagination.pageSize) params.set('pageSize', String(pagination.pageSize));
|
|
129
|
-
if (
|
|
130
|
-
params.set('
|
|
131
|
-
params.set('
|
|
129
|
+
if (currentSorters[0]) {
|
|
130
|
+
params.set('sort', currentSorters[0].field);
|
|
131
|
+
params.set('order', currentSorters[0].order);
|
|
132
132
|
}
|
|
133
|
-
if (
|
|
134
|
-
params.set('filters', JSON.stringify(
|
|
133
|
+
if (currentFilters.length > 0) {
|
|
134
|
+
params.set('filters', JSON.stringify(currentFilters));
|
|
135
135
|
}
|
|
136
136
|
return `?${params.toString()}`;
|
|
137
137
|
}
|
|
@@ -111,9 +111,9 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
111
111
|
writeURLState({
|
|
112
112
|
page: this.pagination.current,
|
|
113
113
|
pageSize: this.pagination.pageSize,
|
|
114
|
-
sortField: this.
|
|
115
|
-
sortOrder: this.
|
|
116
|
-
filters: this.
|
|
114
|
+
sortField: this.currentSorters[0]?.field,
|
|
115
|
+
sortOrder: this.currentSorters[0]?.order,
|
|
116
|
+
filters: this.currentFilters,
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
119
|
}
|
|
@@ -130,7 +130,7 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/** Underlying TanStack Query result */
|
|
133
|
-
get query() { return this._queryResult
|
|
133
|
+
get query() { return this._queryResult; }
|
|
134
134
|
|
|
135
135
|
/** Table data rows */
|
|
136
136
|
get data(): TData[] { return (this.query.data as GetListResult<TData> | undefined)?.data ?? []; }
|
package/src/theme.svelte.ts
CHANGED
|
@@ -301,8 +301,8 @@ export const colorThemes: { id: ColorTheme | string; label: string; color: strin
|
|
|
301
301
|
// ── Dark/Light mode ──────────────────────────────────────
|
|
302
302
|
|
|
303
303
|
function getStoredTheme(): ThemeMode {
|
|
304
|
-
if (typeof
|
|
305
|
-
return (localStorage.getItem(STORAGE_KEY) as ThemeMode) ?? 'system';
|
|
304
|
+
if (typeof window === 'undefined') return 'system';
|
|
305
|
+
try { return (localStorage.getItem(STORAGE_KEY) as ThemeMode) ?? 'system'; } catch { return 'system'; }
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
function getSystemPreference(): 'light' | 'dark' {
|
|
@@ -356,8 +356,8 @@ export function getTheme(): ThemeMode {
|
|
|
356
356
|
|
|
357
357
|
export function setTheme(newMode: ThemeMode): void {
|
|
358
358
|
mode = newMode;
|
|
359
|
-
if (typeof
|
|
360
|
-
localStorage.setItem(STORAGE_KEY, newMode);
|
|
359
|
+
if (typeof window !== 'undefined') {
|
|
360
|
+
try { localStorage.setItem(STORAGE_KEY, newMode); } catch {}
|
|
361
361
|
}
|
|
362
362
|
applyTheme(newMode);
|
|
363
363
|
// Re-apply color preset — light/dark CSS variable overrides differ
|
|
@@ -378,8 +378,8 @@ export function getResolvedTheme(): 'light' | 'dark' {
|
|
|
378
378
|
// ── Color theme ──────────────────────────────────────────
|
|
379
379
|
|
|
380
380
|
function getStoredColorTheme(): ColorTheme {
|
|
381
|
-
if (typeof
|
|
382
|
-
return (localStorage.getItem(COLOR_STORAGE_KEY) as ColorTheme) ?? 'blue';
|
|
381
|
+
if (typeof window === 'undefined') return 'blue';
|
|
382
|
+
try { return (localStorage.getItem(COLOR_STORAGE_KEY) as ColorTheme) ?? 'blue'; } catch { return 'blue'; }
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
let colorTheme = $state<ColorTheme>(getStoredColorTheme());
|
|
@@ -403,8 +403,8 @@ export function getColorTheme(): ColorTheme {
|
|
|
403
403
|
|
|
404
404
|
export function setColorTheme(ct: ColorTheme): void {
|
|
405
405
|
colorTheme = ct;
|
|
406
|
-
if (typeof
|
|
407
|
-
localStorage.setItem(COLOR_STORAGE_KEY, ct);
|
|
406
|
+
if (typeof window !== 'undefined') {
|
|
407
|
+
try { localStorage.setItem(COLOR_STORAGE_KEY, ct); } catch {}
|
|
408
408
|
}
|
|
409
409
|
applyColorTheme(ct);
|
|
410
410
|
// Apply the preset's CSS variable overrides for the current resolved theme
|
|
@@ -98,7 +98,6 @@ export function initUnsavedChangesNotifier(options?: {
|
|
|
98
98
|
const onHashChange = (e: HashChangeEvent) => {
|
|
99
99
|
if (!dirty) return;
|
|
100
100
|
if (!confirm(t('common.unsavedChanges'))) {
|
|
101
|
-
e.preventDefault();
|
|
102
101
|
if (e.oldURL) {
|
|
103
102
|
history.replaceState(null, '', new URL(e.oldURL).hash || '#');
|
|
104
103
|
}
|
package/src/useParsed.svelte.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { getResources } from './context.svelte';
|
|
|
5
5
|
|
|
6
6
|
interface ParsedRoute {
|
|
7
7
|
resource?: string;
|
|
8
|
-
action?: 'list' | 'create' | 'edit' | 'show';
|
|
8
|
+
action?: 'list' | 'create' | 'edit' | 'show' | 'clone';
|
|
9
9
|
id?: string;
|
|
10
10
|
params: Record<string, string>;
|
|
11
11
|
}
|
|
@@ -73,6 +73,9 @@ export function useParsed(): ParsedRoute {
|
|
|
73
73
|
} else if (restSegments[0] === 'show' && restSegments[1]) {
|
|
74
74
|
result.action = 'show';
|
|
75
75
|
result.id = restSegments[1];
|
|
76
|
+
} else if (restSegments[0] === 'clone' && restSegments[1]) {
|
|
77
|
+
result.action = 'clone';
|
|
78
|
+
result.id = restSegments[1];
|
|
76
79
|
} else if (restSegments[0]) {
|
|
77
80
|
// Legacy: /:resource/:id
|
|
78
81
|
result.action = 'show';
|
|
@@ -92,6 +95,9 @@ export function useParsed(): ParsedRoute {
|
|
|
92
95
|
} else if (segments[1] === 'show' && segments[2]) {
|
|
93
96
|
result.action = 'show';
|
|
94
97
|
result.id = segments[2];
|
|
98
|
+
} else if (segments[1] === 'clone' && segments[2]) {
|
|
99
|
+
result.action = 'clone';
|
|
100
|
+
result.id = segments[2];
|
|
95
101
|
} else if (segments[1]) {
|
|
96
102
|
result.action = 'show';
|
|
97
103
|
result.id = segments[1];
|