@svadmin/core 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/audit.ts +5 -0
- package/src/auth-hooks.svelte.ts +44 -34
- package/src/chatProvider.svelte.ts +8 -21
- package/src/context.svelte.ts +22 -2
- package/src/data-transfer.svelte.ts +66 -34
- package/src/form-hooks.svelte.ts +110 -42
- package/src/helpers-pure.ts +159 -0
- package/src/helpers.test.ts +177 -0
- package/src/helpers.ts +9 -118
- package/src/hook-utils.svelte.ts +27 -6
- package/src/hooks.svelte.ts +120 -18
- package/src/i18n.svelte.ts +41 -17
- package/src/index.ts +10 -3
- package/src/live-websocket.ts +20 -2
- package/src/live.svelte.ts +35 -19
- package/src/mutation-hooks.svelte.ts +162 -73
- package/src/notification.svelte.ts +9 -5
- package/src/options.svelte.ts +42 -19
- package/src/permissions.ts +4 -0
- package/src/query-hooks.svelte.ts +182 -100
- package/src/router-provider.ts +3 -1
- package/src/router.ts +23 -2
- package/src/routing-hooks.svelte.ts +42 -17
- package/src/table-hooks.svelte.ts +87 -34
- package/src/table-state.svelte.ts +2 -1
- package/src/theme.svelte.ts +27 -25
- package/src/toast.svelte.ts +22 -11
- package/src/types.ts +3 -1
- package/src/url-sync.ts +28 -16
- package/src/useCan.ts +6 -2
- package/src/useParsed.svelte.ts +4 -0
- package/src/useStepsForm.svelte.ts +31 -21
- package/src/utility-hooks.svelte.ts +14 -4
package/src/options.svelte.ts
CHANGED
|
@@ -54,33 +54,56 @@ const defaultTextTransformers: TextTransformers = {
|
|
|
54
54
|
singular: (text: string) => text.endsWith('s') ? text.slice(0, -1) : text,
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
function createDefaultOptions(): AdminOptions {
|
|
58
|
+
return {
|
|
59
|
+
mutationMode: 'pessimistic',
|
|
60
|
+
warnWhenUnsavedChanges: false,
|
|
61
|
+
syncWithLocation: false,
|
|
62
|
+
liveMode: 'auto',
|
|
63
|
+
disableServerSideValidation: false,
|
|
64
|
+
undoableTimeout: 5000,
|
|
65
|
+
textTransformers: defaultTextTransformers,
|
|
66
|
+
redirect: { afterCreate: 'list', afterEdit: 'list', afterClone: 'list' },
|
|
67
|
+
breadcrumb: 'default',
|
|
68
|
+
disableRouteChangeHandler: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
69
71
|
|
|
70
|
-
let adminOptions = $state<AdminOptions>(
|
|
72
|
+
let adminOptions = $state<AdminOptions>(createDefaultOptions());
|
|
71
73
|
|
|
72
|
-
export function setAdminOptions(options: AdminOptions): void {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
export function setAdminOptions(options: AdminOptions, merge?: boolean): void {
|
|
75
|
+
if (merge) {
|
|
76
|
+
adminOptions = {
|
|
77
|
+
...adminOptions,
|
|
78
|
+
...options,
|
|
79
|
+
textTransformers: { ...adminOptions.textTransformers, ...options.textTransformers },
|
|
80
|
+
redirect: { ...adminOptions.redirect, ...options.redirect },
|
|
81
|
+
reactQuery: { ...adminOptions.reactQuery, ...options.reactQuery },
|
|
82
|
+
title: { ...adminOptions.title, ...options.title },
|
|
83
|
+
overtime: { ...adminOptions.overtime, ...options.overtime },
|
|
84
|
+
};
|
|
85
|
+
} else {
|
|
86
|
+
const defaults = createDefaultOptions();
|
|
87
|
+
adminOptions = {
|
|
88
|
+
...defaults,
|
|
89
|
+
...options,
|
|
90
|
+
textTransformers: { ...defaultTextTransformers, ...options.textTransformers },
|
|
91
|
+
redirect: { ...defaults.redirect, ...options.redirect },
|
|
92
|
+
reactQuery: { ...defaults.reactQuery, ...options.reactQuery },
|
|
93
|
+
title: { ...defaults.title, ...options.title },
|
|
94
|
+
overtime: { ...defaults.overtime, ...options.overtime },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
export function getAdminOptions(): AdminOptions {
|
|
81
100
|
return adminOptions;
|
|
82
101
|
}
|
|
83
102
|
|
|
103
|
+
export function resetAdminOptions(): void {
|
|
104
|
+
adminOptions = createDefaultOptions();
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
/** Get the resolved text transformers (always returns full set) */
|
|
85
108
|
export function getTextTransformers(): TextTransformers {
|
|
86
109
|
const opts = getAdminOptions();
|
package/src/permissions.ts
CHANGED
|
@@ -67,6 +67,10 @@ export function getAccessControlProvider(): AccessControlProvider | null {
|
|
|
67
67
|
return provider;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export function resetAccessControlProvider(): void {
|
|
71
|
+
provider = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
70
74
|
/** Get global button options from the AccessControlProvider */
|
|
71
75
|
export function getAccessControlOptions() {
|
|
72
76
|
return provider?.options ?? {};
|
|
@@ -3,6 +3,7 @@ import { getAdminOptions } from './options.svelte';
|
|
|
3
3
|
import { getDataProviderForResource, getDataProvider, getLiveProvider } from './context.svelte';
|
|
4
4
|
import { useParsed } from './useParsed.svelte';
|
|
5
5
|
import {
|
|
6
|
+
checkError,
|
|
6
7
|
createOvertimeTracker,
|
|
7
8
|
createLiveSubscription,
|
|
8
9
|
fireSuccessNotification,
|
|
@@ -22,25 +23,26 @@ import type { LiveMode, LiveEvent } from './live.svelte';
|
|
|
22
23
|
*/
|
|
23
24
|
function extendQuery<Q extends object, E extends Record<string, unknown>>(
|
|
24
25
|
query: Q,
|
|
25
|
-
extensions:
|
|
26
|
+
extensions: E,
|
|
26
27
|
): Q & E {
|
|
27
28
|
return new Proxy(query, {
|
|
28
29
|
get(target, prop) {
|
|
29
|
-
if (typeof prop === 'string') {
|
|
30
|
-
|
|
31
|
-
if (prop in ext) return ext[prop as keyof E];
|
|
30
|
+
if (typeof prop === 'string' && prop in extensions) {
|
|
31
|
+
return extensions[prop as keyof E];
|
|
32
32
|
}
|
|
33
|
-
return target
|
|
33
|
+
return Reflect.get(target, prop);
|
|
34
34
|
},
|
|
35
35
|
has(target, prop) {
|
|
36
|
-
if (typeof prop === 'string' && prop in extensions
|
|
37
|
-
return prop
|
|
36
|
+
if (typeof prop === 'string' && prop in extensions) return true;
|
|
37
|
+
return Reflect.has(target, prop);
|
|
38
38
|
},
|
|
39
39
|
}) as Q & E;
|
|
40
40
|
}
|
|
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,55 +59,68 @@ 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: [opts.dataProviderName, 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
|
|
|
108
|
+
// Track last notification timestamps to prevent duplicate firings on re-render
|
|
109
|
+
let lastSuccessAt = 0;
|
|
110
|
+
let lastErrorAt = 0;
|
|
100
111
|
$effect(() => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
const opts = getOptions();
|
|
113
|
+
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && opts.successNotification) {
|
|
114
|
+
lastSuccessAt = query.dataUpdatedAt;
|
|
115
|
+
fireSuccessNotification(opts.successNotification, '', query.data, undefined, getResource());
|
|
116
|
+
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
117
|
+
lastErrorAt = query.errorUpdatedAt;
|
|
118
|
+
checkError(query.error);
|
|
119
|
+
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error);
|
|
105
120
|
}
|
|
106
121
|
});
|
|
107
122
|
|
|
108
|
-
return extendQuery(query,
|
|
123
|
+
return extendQuery(query, { overtime });
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
// ─── useOne ────────────────────────────────────────────────────
|
|
@@ -124,59 +139,95 @@ export interface UseOneOptions<TData extends BaseRecord = BaseRecord, TError = H
|
|
|
124
139
|
overtimeOptions?: OvertimeOptions;
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError>(
|
|
142
|
+
export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}) {
|
|
128
143
|
const parsed = useParsed();
|
|
129
|
-
const resource = options.resource ?? parsed.resource ?? '';
|
|
130
|
-
const id = options.id ?? parsed.id;
|
|
131
144
|
const adminOptions = getAdminOptions();
|
|
132
|
-
|
|
145
|
+
|
|
146
|
+
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
147
|
+
const getResource = () => getOptions().resource ?? parsed.resource ?? '';
|
|
148
|
+
const getId = () => getOptions().id ?? parsed.id;
|
|
133
149
|
|
|
134
|
-
const
|
|
150
|
+
const query = createQuery<GetOneResult<TData>, TError>(() => {
|
|
151
|
+
const opts = getOptions();
|
|
152
|
+
const resource = getResource();
|
|
153
|
+
const id = getId();
|
|
154
|
+
const provider = getDataProviderForResource(resource, opts.dataProviderName);
|
|
155
|
+
const queryOptions = opts.queryOptions;
|
|
135
156
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
return {
|
|
158
|
+
queryKey: [opts.dataProviderName, resource, 'one', id, opts.meta],
|
|
159
|
+
queryFn: async () => {
|
|
160
|
+
if (id == null) throw new Error('useOne requires an id');
|
|
161
|
+
const result = await provider.getOne<TData>({
|
|
162
|
+
resource,
|
|
163
|
+
id,
|
|
164
|
+
meta: opts.meta,
|
|
165
|
+
});
|
|
166
|
+
return result;
|
|
167
|
+
},
|
|
168
|
+
enabled: (queryOptions?.enabled ?? true) && id != null,
|
|
169
|
+
staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
170
|
+
gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
|
|
171
|
+
refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
151
174
|
|
|
152
|
-
const overtime = createOvertimeTracker(() => query.isLoading,
|
|
175
|
+
const overtime = createOvertimeTracker(() => query.isLoading, typeof optionsOrGetter === 'function' ? optionsOrGetter().overtimeOptions : optionsOrGetter.overtimeOptions ?? adminOptions.overtime);
|
|
153
176
|
|
|
177
|
+
createLiveSubscription((): LiveSubscriptionParams => {
|
|
178
|
+
const opts = getOptions();
|
|
179
|
+
const resource = getResource();
|
|
180
|
+
return {
|
|
181
|
+
resource,
|
|
182
|
+
liveProvider: getLiveProvider(),
|
|
183
|
+
liveMode: opts.liveMode ?? adminOptions.liveMode,
|
|
184
|
+
onLiveEvent: (e: LiveEvent) => {
|
|
185
|
+
opts.onLiveEvent?.(e);
|
|
186
|
+
adminOptions.onLiveEvent?.(e);
|
|
187
|
+
},
|
|
188
|
+
liveParams: opts.liveParams,
|
|
189
|
+
enabled: (opts.queryOptions?.enabled ?? true) && getId() != null,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
let lastSuccessAt = 0;
|
|
194
|
+
let lastErrorAt = 0;
|
|
154
195
|
$effect(() => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
196
|
+
const opts = getOptions();
|
|
197
|
+
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && opts.successNotification) {
|
|
198
|
+
lastSuccessAt = query.dataUpdatedAt;
|
|
199
|
+
fireSuccessNotification(opts.successNotification, '', query.data, undefined, getResource());
|
|
200
|
+
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
201
|
+
lastErrorAt = query.errorUpdatedAt;
|
|
202
|
+
checkError(query.error);
|
|
203
|
+
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error);
|
|
159
204
|
}
|
|
160
205
|
});
|
|
161
206
|
|
|
162
|
-
return extendQuery(query,
|
|
207
|
+
return extendQuery(query, { overtime });
|
|
163
208
|
}
|
|
164
209
|
|
|
165
210
|
// ─── useShow ──────────────────────────────────────────────────
|
|
166
211
|
export function useShow<TData extends BaseRecord = BaseRecord, TError = HttpError>(
|
|
167
|
-
|
|
212
|
+
optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}
|
|
168
213
|
) {
|
|
169
214
|
const parsed = useParsed();
|
|
170
|
-
|
|
215
|
+
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
216
|
+
|
|
217
|
+
let _showId = $state<string | number | undefined>(undefined);
|
|
218
|
+
|
|
219
|
+
// showId prioritizes explicit setShowId, then options.id, then URL id.
|
|
220
|
+
const showId = $derived(_showId ?? getOptions().id ?? parsed.id);
|
|
171
221
|
|
|
172
|
-
function setShowId(id: string | number) {
|
|
222
|
+
function setShowId(id: string | number) { _showId = id; }
|
|
173
223
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
224
|
+
// We wrap it in a getter so useOne evaluates it dynamically.
|
|
225
|
+
const result = useOne<TData, TError>(() => ({
|
|
226
|
+
...getOptions(),
|
|
227
|
+
id: showId,
|
|
228
|
+
}));
|
|
178
229
|
|
|
179
|
-
return extendQuery(result, ()
|
|
230
|
+
return extendQuery(result, { get showId() { return showId; }, setShowId });
|
|
180
231
|
}
|
|
181
232
|
|
|
182
233
|
// ─── useMany ───────────────────────────────────────────────────
|
|
@@ -195,33 +246,64 @@ export interface UseManyOptions<TData extends BaseRecord = BaseRecord, TError =
|
|
|
195
246
|
overtimeOptions?: OvertimeOptions;
|
|
196
247
|
}
|
|
197
248
|
|
|
198
|
-
export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(
|
|
199
|
-
const { resource, ids, meta, dataProviderName, queryOptions } = options;
|
|
249
|
+
export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseManyOptions<TData, TError>>) {
|
|
200
250
|
const adminOptions = getAdminOptions();
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
const query = createQuery<GetManyResult<TData>, TError>(() => ({
|
|
204
|
-
queryKey: [resource, 'many', ids, meta],
|
|
205
|
-
queryFn: async () => {
|
|
206
|
-
if (!ids.length) return { data: [] };
|
|
207
|
-
if (provider.getMany) {
|
|
208
|
-
return provider.getMany<TData>({ resource, ids, meta });
|
|
209
|
-
}
|
|
210
|
-
const results = await Promise.all(ids.map(id => provider.getOne<TData>({ resource, id, meta })));
|
|
211
|
-
return { data: results.map(r => r.data) };
|
|
212
|
-
},
|
|
213
|
-
enabled: (queryOptions?.enabled ?? true) && ids.length > 0,
|
|
214
|
-
staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
215
|
-
}));
|
|
251
|
+
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
216
252
|
|
|
217
|
-
const
|
|
253
|
+
const query = createQuery<GetManyResult<TData>, TError>(() => {
|
|
254
|
+
const opts = getOptions();
|
|
255
|
+
const { resource, ids, meta, dataProviderName, queryOptions } = opts;
|
|
256
|
+
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
218
257
|
|
|
258
|
+
return {
|
|
259
|
+
queryKey: [dataProviderName, resource, 'many', ids, meta],
|
|
260
|
+
queryFn: async () => {
|
|
261
|
+
if (!ids.length) return { data: [] };
|
|
262
|
+
if (provider.getMany) {
|
|
263
|
+
return provider.getMany<TData>({ resource, ids, meta });
|
|
264
|
+
}
|
|
265
|
+
const results = await Promise.all(ids.map(id => provider.getOne<TData>({ resource, id, meta })));
|
|
266
|
+
return { data: results.map(r => r.data) };
|
|
267
|
+
},
|
|
268
|
+
enabled: (queryOptions?.enabled ?? true) && ids.length > 0,
|
|
269
|
+
staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
|
|
270
|
+
gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
|
|
271
|
+
refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const overtime = createOvertimeTracker(() => query.isLoading, typeof optionsOrGetter === 'function' ? optionsOrGetter().overtimeOptions : optionsOrGetter.overtimeOptions ?? adminOptions.overtime);
|
|
276
|
+
|
|
277
|
+
createLiveSubscription((): LiveSubscriptionParams => {
|
|
278
|
+
const opts = getOptions();
|
|
279
|
+
return {
|
|
280
|
+
resource: opts.resource,
|
|
281
|
+
liveProvider: getLiveProvider(),
|
|
282
|
+
liveMode: opts.liveMode ?? adminOptions.liveMode,
|
|
283
|
+
onLiveEvent: (e: LiveEvent) => {
|
|
284
|
+
opts.onLiveEvent?.(e);
|
|
285
|
+
adminOptions.onLiveEvent?.(e);
|
|
286
|
+
},
|
|
287
|
+
liveParams: opts.liveParams,
|
|
288
|
+
enabled: (opts.queryOptions?.enabled ?? true) && opts.ids.length > 0,
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
let lastSuccessAt = 0;
|
|
293
|
+
let lastErrorAt = 0;
|
|
219
294
|
$effect(() => {
|
|
220
|
-
|
|
221
|
-
|
|
295
|
+
const opts = getOptions();
|
|
296
|
+
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && opts.successNotification) {
|
|
297
|
+
lastSuccessAt = query.dataUpdatedAt;
|
|
298
|
+
fireSuccessNotification(opts.successNotification, '', query.data, undefined, opts.resource);
|
|
299
|
+
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
300
|
+
lastErrorAt = query.errorUpdatedAt;
|
|
301
|
+
checkError(query.error);
|
|
302
|
+
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error);
|
|
303
|
+
}
|
|
222
304
|
});
|
|
223
305
|
|
|
224
|
-
return extendQuery(query,
|
|
306
|
+
return extendQuery(query, { overtime });
|
|
225
307
|
}
|
|
226
308
|
|
|
227
309
|
export function useApiUrl(dataProviderName?: string): string {
|
package/src/router-provider.ts
CHANGED
|
@@ -100,7 +100,9 @@ export function createHistoryRouterProvider(basePath = ''): RouterProvider {
|
|
|
100
100
|
if (typeof window === 'undefined') {
|
|
101
101
|
return { resource: undefined, action: undefined, id: undefined, params: {}, pathname: '/' };
|
|
102
102
|
}
|
|
103
|
-
const pathname = window.location.pathname.
|
|
103
|
+
const pathname = (basePath && window.location.pathname.startsWith(basePath)
|
|
104
|
+
? window.location.pathname.slice(basePath.length) || '/'
|
|
105
|
+
: window.location.pathname);
|
|
104
106
|
const params: Record<string, string> = {};
|
|
105
107
|
for (const [k, v] of new URLSearchParams(window.location.search).entries()) {
|
|
106
108
|
params[k] = v;
|
package/src/router.ts
CHANGED
|
@@ -7,13 +7,26 @@ interface RouteMatch {
|
|
|
7
7
|
params: Record<string, string>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
export type RouteGuard = (to: string, from: string) => boolean | Promise<boolean>;
|
|
11
|
+
|
|
11
12
|
let _routerProvider: RouterProvider | undefined;
|
|
13
|
+
let _beforeGuards: RouteGuard[] = [];
|
|
14
|
+
let _afterGuards: ((to: string, from: string) => void)[] = [];
|
|
12
15
|
|
|
13
16
|
export function setActiveRouterProvider(provider: RouterProvider | undefined) {
|
|
14
17
|
_routerProvider = provider;
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
export function beforeEach(guard: RouteGuard): () => void {
|
|
21
|
+
_beforeGuards.push(guard);
|
|
22
|
+
return () => { _beforeGuards = _beforeGuards.filter(g => g !== guard); };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function afterEach(guard: (to: string, from: string) => void): () => void {
|
|
26
|
+
_afterGuards.push(guard);
|
|
27
|
+
return () => { _afterGuards = _afterGuards.filter(g => g !== guard); };
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
// Parse route pattern like /products/edit/:id into regex
|
|
18
31
|
function compileRoute(pattern: string): { regex: RegExp; keys: string[] } {
|
|
19
32
|
const keys: string[] = [];
|
|
@@ -50,12 +63,20 @@ export function matchRoute(
|
|
|
50
63
|
return null;
|
|
51
64
|
}
|
|
52
65
|
|
|
53
|
-
export function navigate(path: string, options?: { replaceState?: boolean }): void {
|
|
66
|
+
export async function navigate(path: string, options?: { replaceState?: boolean }): Promise<void> {
|
|
67
|
+
const from = currentPath();
|
|
68
|
+
for (const guard of _beforeGuards) {
|
|
69
|
+
const allowed = await guard(path, from);
|
|
70
|
+
if (!allowed) return;
|
|
71
|
+
}
|
|
54
72
|
if (_routerProvider) {
|
|
55
73
|
_routerProvider.go({ to: path, type: options?.replaceState ? 'replace' : 'push' });
|
|
56
74
|
} else if (typeof window !== 'undefined') {
|
|
57
75
|
window.location.hash = '#' + path.replace(/^#/, '');
|
|
58
76
|
}
|
|
77
|
+
for (const guard of _afterGuards) {
|
|
78
|
+
guard(path, from);
|
|
79
|
+
}
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
/**
|
|
@@ -6,11 +6,24 @@ import { useParsed } from './useParsed.svelte';
|
|
|
6
6
|
|
|
7
7
|
export function useGetToPath() {
|
|
8
8
|
const routerProvider = getRouterProvider();
|
|
9
|
+
const parsed = useParsed();
|
|
9
10
|
|
|
10
11
|
return (options: { resource?: string; action?: 'list' | 'create' | 'edit' | 'show' | 'clone'; id?: string | number; meta?: Record<string, unknown> }) => {
|
|
11
|
-
const { resource, action, id } = options;
|
|
12
|
+
const { resource, action, id, meta } = options;
|
|
12
13
|
if (!resource) return '/';
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
// Support nested resources: use meta.parentPath or auto-detect from current URL
|
|
16
|
+
let basePath: string;
|
|
17
|
+
if (meta?.parentPath) {
|
|
18
|
+
basePath = `${meta.parentPath}/${resource}`;
|
|
19
|
+
} else if (parsed.resourcePath && parsed.resource === resource) {
|
|
20
|
+
// Re-use the current nested path context (e.g., /teams/123/users)
|
|
21
|
+
basePath = `/${parsed.resourcePath}`;
|
|
22
|
+
} else {
|
|
23
|
+
basePath = `/${resource}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let path = basePath;
|
|
14
27
|
if (action === 'create') path += '/create';
|
|
15
28
|
else if (action === 'edit' && id) path += `/edit/${id}`;
|
|
16
29
|
else if (action === 'show' && id) path += `/show/${id}`;
|
|
@@ -70,16 +83,28 @@ export function useLink() {
|
|
|
70
83
|
|
|
71
84
|
export function useResource(resourceName?: string) {
|
|
72
85
|
const parsed = useParsed();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
|
|
87
|
+
// Derived resource — re-evaluates when parsed.resource or resourceName changes
|
|
88
|
+
const resolvedResource = $derived.by(() => {
|
|
89
|
+
const target = resourceName ?? parsed.resource;
|
|
90
|
+
if (!target) return undefined;
|
|
91
|
+
try {
|
|
92
|
+
return getResource(target);
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
get resource() { return resolvedResource; },
|
|
100
|
+
get resources() { return getResources(); },
|
|
101
|
+
get identifier() { return resolvedResource?.identifier ?? resolvedResource?.name; },
|
|
102
|
+
/** Resolve a resource by name — useful for dynamic lookups inside callbacks */
|
|
103
|
+
select: (name: string) => {
|
|
104
|
+
const res = getResource(name);
|
|
105
|
+
return { resource: res, identifier: res.identifier ?? res.name };
|
|
106
|
+
},
|
|
107
|
+
};
|
|
83
108
|
}
|
|
84
109
|
|
|
85
110
|
export function useNavigation() {
|
|
@@ -87,11 +112,11 @@ export function useNavigation() {
|
|
|
87
112
|
const parsed = useParsed();
|
|
88
113
|
|
|
89
114
|
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' }),
|
|
115
|
+
create: (resource?: string) => go({ type: 'push', resource: resource ?? parsed.resourcePath ?? parsed.resource, action: 'create' }),
|
|
116
|
+
edit: (resource?: string, id?: string | number) => go({ type: 'push', resource: resource ?? parsed.resourcePath ?? parsed.resource, action: 'edit', id: id ?? parsed.id }),
|
|
117
|
+
clone: (resource?: string, id?: string | number) => go({ type: 'push', resource: resource ?? parsed.resourcePath ?? parsed.resource, action: 'clone', id: id ?? parsed.id }),
|
|
118
|
+
show: (resource?: string, id?: string | number) => go({ type: 'push', resource: resource ?? parsed.resourcePath ?? parsed.resource, action: 'show', id: id ?? parsed.id }),
|
|
119
|
+
list: (resource?: string) => go({ type: 'push', resource: resource ?? parsed.resourcePath ?? parsed.resource, action: 'list' }),
|
|
95
120
|
push: (url: string) => go({ to: url, type: 'push' }),
|
|
96
121
|
replace: (url: string) => go({ to: url, type: 'replace' }),
|
|
97
122
|
goBack: useBack(),
|