@svadmin/core 0.21.0 → 0.21.2

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.
@@ -1,6 +1,6 @@
1
1
  import { createMutation, useQueryClient } from '@tanstack/svelte-query';
2
2
  import { getAdminOptions } from './options.svelte';
3
- import { getDataProviderForResource } from './context.svelte';
3
+ import { getDataProviderForResource, getResource } from './context.svelte';
4
4
  import { useParsed } from './useParsed.svelte';
5
5
  import { audit } from './audit';
6
6
  import { UndoError } from './types';
@@ -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,25 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
55
56
  isMutating = false;
56
57
  }
57
58
  },
58
- onSuccess: (data, params) => {
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
- audit({ action: 'create', resource: resName, recordId: String((data.data as Record<string, unknown>).id) });
66
+ const res = getResource(resName);
67
+ const pk = res.primaryKey ?? 'id';
68
+ audit({ action: 'create', resource: resName, recordId: String((data.data as Record<string, unknown>)[pk]) });
69
+ if (typeof options.mutationOptions?.onSuccess === 'function') {
70
+ (options.mutationOptions.onSuccess as Function)(data, params, context);
71
+ }
65
72
  },
66
- onError: (error, params) => {
73
+ onError: (error, params, context) => {
67
74
  fireErrorNotification(params.errorNotification, 'Create failed', error);
75
+ if (typeof options.mutationOptions?.onError === 'function') {
76
+ (options.mutationOptions.onError as Function)(error, params, context);
77
+ }
68
78
  },
69
79
  }));
70
80
 
@@ -115,6 +125,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
115
125
  TError,
116
126
  UseUpdateMutateParams<TVariables>
117
127
  >(() => ({
128
+ ...options.mutationOptions,
118
129
  mutationFn: async (params) => {
119
130
  isMutating = true;
120
131
  const resName = params.resource ?? defaultResource;
@@ -143,7 +154,10 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
143
154
  }
144
155
  },
145
156
  onMutate: async (params) => {
146
- if (mutationMode === 'pessimistic') return undefined;
157
+ const userOnMutate = (options.mutationOptions as any)?.onMutate;
158
+ const userContext = typeof userOnMutate === 'function' ? await userOnMutate(params) : undefined;
159
+
160
+ if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
147
161
  const resName = params.resource ?? defaultResource;
148
162
  const targetId = params.id ?? defaultId;
149
163
 
@@ -151,7 +165,9 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
151
165
  const previousDetail = queryClient.getQueryData([resName, 'one', targetId]);
152
166
  const previousList = queryClient.getQueriesData({ queryKey: [resName, 'list'] });
153
167
 
168
+ const pk = getResource(resName).primaryKey ?? 'id';
154
169
  const updater = params.optimisticUpdateMap;
170
+
155
171
  if (updater?.detail) {
156
172
  queryClient.setQueryData([resName, 'one', targetId], (old: unknown) => updater.detail!(old, params.variables, targetId));
157
173
  } else {
@@ -164,13 +180,14 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
164
180
  queryClient.setQueriesData({ queryKey: [resName, 'list'] }, (old: unknown) => {
165
181
  if (!old || typeof old !== 'object' || !('data' in old)) return old;
166
182
  const o = old as { data: Record<string, unknown>[] };
167
- return { ...o, data: o.data.map((item) => String(item.id) === String(targetId) ? { ...item, ...params.variables } : item) };
183
+ return { ...o, data: o.data.map((item) => String(item[pk]) === String(targetId) ? { ...item, ...params.variables } : item) };
168
184
  });
169
185
  }
170
186
 
171
- return { previousDetail, previousList };
187
+ return { _svadmin_ctx: true, userContext, previousDetail, previousList };
172
188
  },
173
- onSuccess: (data, params) => {
189
+ onSuccess: (data, params, context) => {
190
+ const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
174
191
  const resName = params.resource ?? defaultResource;
175
192
  const targetId = params.id ?? defaultId;
176
193
  if (params.invalidates !== false) {
@@ -178,8 +195,12 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
178
195
  }
179
196
  fireSuccessNotification(params.successNotification, 'Updated successfully', data.data, params.variables, resName);
180
197
  audit({ action: 'update', resource: resName, recordId: String(targetId) });
198
+ if (typeof options.mutationOptions?.onSuccess === 'function') {
199
+ (options.mutationOptions.onSuccess as Function)(data, params, extractedCtx);
200
+ }
181
201
  },
182
202
  onError: (error, params, context: unknown) => {
203
+ const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
183
204
  const resName = params.resource ?? defaultResource;
184
205
  const targetId = params.id ?? defaultId;
185
206
  const ctx = context as { previousDetail?: unknown; previousList?: [unknown, unknown][] } | undefined;
@@ -189,6 +210,9 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
189
210
  }
190
211
  if (error instanceof UndoError) return;
191
212
  fireErrorNotification(params.errorNotification, 'Update failed', error);
213
+ if (typeof options.mutationOptions?.onError === 'function') {
214
+ (options.mutationOptions.onError as Function)(error, params, extractedCtx);
215
+ }
192
216
  },
193
217
  }));
194
218
 
@@ -235,6 +259,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
235
259
  TError,
236
260
  UseDeleteMutateParams<TVariables>
237
261
  >(() => ({
262
+ ...options.mutationOptions,
238
263
  mutationFn: async (params) => {
239
264
  isMutating = true;
240
265
  const resName = params.resource ?? defaultResource;
@@ -263,22 +288,28 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
263
288
  }
264
289
  },
265
290
  onMutate: async (params) => {
266
- if (mutationMode === 'pessimistic') return undefined;
291
+ const userOnMutate = (options.mutationOptions as any)?.onMutate;
292
+ const userContext = typeof userOnMutate === 'function' ? await userOnMutate(params) : undefined;
293
+
294
+ if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
267
295
  const resName = params.resource ?? defaultResource;
268
296
  const targetId = params.id ?? defaultId;
269
297
 
270
298
  await queryClient.cancelQueries({ queryKey: [resName] });
271
299
  const previousList = queryClient.getQueriesData({ queryKey: [resName, 'list'] });
272
300
 
301
+ const pk = getResource(resName).primaryKey ?? 'id';
302
+
273
303
  queryClient.setQueriesData({ queryKey: [resName, 'list'] }, (old: unknown) => {
274
304
  if (!old || typeof old !== 'object' || !('data' in old)) return old;
275
305
  const o = old as { data: Record<string, unknown>[] };
276
- return { ...o, data: o.data.filter((item) => String(item.id) !== String(targetId)) };
306
+ return { ...o, data: o.data.filter((item) => String(item[pk]) !== String(targetId)) };
277
307
  });
278
308
 
279
- return { previousList };
309
+ return { _svadmin_ctx: true, userContext, previousList };
280
310
  },
281
- onSuccess: (data, params) => {
311
+ onSuccess: (data, params, context) => {
312
+ const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
282
313
  const resName = params.resource ?? defaultResource;
283
314
  const targetId = params.id ?? defaultId;
284
315
  if (params.invalidates !== false) {
@@ -286,14 +317,21 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
286
317
  }
287
318
  fireSuccessNotification(params.successNotification, 'Deleted successfully', data.data, params.variables, resName);
288
319
  audit({ action: 'delete', resource: resName, recordId: String(targetId) });
320
+ if (typeof options.mutationOptions?.onSuccess === 'function') {
321
+ (options.mutationOptions.onSuccess as Function)(data, params, extractedCtx);
322
+ }
289
323
  },
290
324
  onError: (error, params, context: unknown) => {
325
+ const extractedCtx = (context as any)?._svadmin_ctx ? (context as any).userContext : context;
291
326
  const ctx = context as { previousList?: [unknown, unknown][] } | undefined;
292
327
  if (ctx?.previousList) {
293
328
  ctx.previousList.forEach(([qk, data]) => queryClient.setQueryData(qk as string[], data));
294
329
  }
295
330
  if (error instanceof UndoError) return;
296
331
  fireErrorNotification(params.errorNotification, 'Delete failed', error);
332
+ if (typeof options.mutationOptions?.onError === 'function') {
333
+ (options.mutationOptions.onError as Function)(error, params, extractedCtx);
334
+ }
297
335
  },
298
336
  }));
299
337
 
@@ -56,34 +56,33 @@ export interface UseListOptions<TData extends BaseRecord = BaseRecord, TError =
56
56
 
57
57
  export function useList<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseListOptions<TData, TError> = {}) {
58
58
  const parsed = useParsed();
59
- const resource = options.resource ?? parsed.resource ?? '';
60
59
  const adminOptions = getAdminOptions();
61
- const provider = getDataProviderForResource(resource, options.dataProviderName);
62
60
 
63
- const queryOptions = options.queryOptions;
61
+ const query = createQuery<GetListResult<TData>, TError>(() => {
62
+ const resource = options.resource ?? parsed.resource ?? '';
63
+ const queryOptions = options.queryOptions;
64
+ const provider = getDataProviderForResource(resource, options.dataProviderName);
64
65
 
65
- const query = createQuery<GetListResult<TData>, TError>(() => ({
66
- queryKey: [resource, 'list', options.pagination, options.sorters, options.filters, options.meta],
67
- queryFn: async () => {
68
- const result = await provider.getList<TData>({
66
+ return {
67
+ queryKey: [resource, 'list', options.pagination, options.sorters, options.filters, options.meta],
68
+ queryFn: async () => provider.getList<TData>({
69
69
  resource,
70
70
  pagination: options.pagination,
71
71
  sorters: options.sorters,
72
72
  filters: options.filters,
73
73
  meta: options.meta,
74
- });
75
- return result;
76
- },
77
- enabled: queryOptions?.enabled ?? true,
78
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
79
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
80
- refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
81
- }));
74
+ }),
75
+ enabled: queryOptions?.enabled ?? true,
76
+ staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
77
+ gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
78
+ refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
79
+ };
80
+ });
82
81
 
83
82
  const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
84
83
 
85
- createLiveSubscription({
86
- resource,
84
+ createLiveSubscription(() => ({
85
+ resource: options.resource ?? parsed.resource ?? '',
87
86
  liveProvider: getLiveProvider(),
88
87
  liveMode: options.liveMode ?? adminOptions.liveMode,
89
88
  onLiveEvent: (e) => {
@@ -91,13 +90,19 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
91
90
  adminOptions.onLiveEvent?.(e);
92
91
  },
93
92
  liveParams: options.liveParams,
94
- enabled: queryOptions?.enabled ?? true,
95
- });
93
+ enabled: options.queryOptions?.enabled ?? true,
94
+ }));
95
+
96
+ let lastSuccess = $state(0);
97
+ let lastError = $state(0);
96
98
 
97
99
  $effect(() => {
98
- if (query.isSuccess && options.successNotification) {
100
+ if (query.isSuccess && options.successNotification && query.dataUpdatedAt > lastSuccess) {
101
+ lastSuccess = query.dataUpdatedAt;
102
+ const resource = options.resource ?? parsed.resource ?? '';
99
103
  fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
100
- } else if (query.isError) {
104
+ } else if (query.isError && query.errorUpdatedAt > lastError) {
105
+ lastError = query.errorUpdatedAt;
101
106
  fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
102
107
  }
103
108
  });
@@ -123,35 +128,54 @@ export interface UseOneOptions<TData extends BaseRecord = BaseRecord, TError = H
123
128
 
124
129
  export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseOneOptions<TData, TError> = {}) {
125
130
  const parsed = useParsed();
126
- const resource = options.resource ?? parsed.resource ?? '';
127
- const id = options.id ?? parsed.id;
128
131
  const adminOptions = getAdminOptions();
129
- const provider = getDataProviderForResource(resource, options.dataProviderName);
130
132
 
131
- const queryOptions = options.queryOptions;
133
+ const query = createQuery<GetOneResult<TData>, TError>(() => {
134
+ const resource = options.resource ?? parsed.resource ?? '';
135
+ const id = options.id ?? parsed.id;
136
+ const queryOptions = options.queryOptions;
137
+ const provider = getDataProviderForResource(resource, options.dataProviderName);
132
138
 
133
- const query = createQuery<GetOneResult<TData>, TError>(() => ({
134
- queryKey: [resource, 'one', id, options.meta],
135
- queryFn: async () => {
136
- if (id == null) throw new Error('useOne requires an id');
137
- const result = await provider.getOne<TData>({
138
- resource,
139
- id,
140
- meta: options.meta,
141
- });
142
- return result;
139
+ return {
140
+ queryKey: [resource, 'one', id, options.meta],
141
+ queryFn: async () => {
142
+ if (id == null) throw new Error('useOne requires an id');
143
+ return provider.getOne<TData>({
144
+ resource,
145
+ id,
146
+ meta: options.meta,
147
+ });
148
+ },
149
+ enabled: (queryOptions?.enabled ?? true) && id != null,
150
+ staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
151
+ gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
152
+ };
153
+ });
154
+
155
+ const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
156
+
157
+ createLiveSubscription(() => ({
158
+ resource: options.resource ?? parsed.resource ?? '',
159
+ liveProvider: getLiveProvider(),
160
+ liveMode: options.liveMode ?? adminOptions.liveMode,
161
+ onLiveEvent: (e) => {
162
+ options.onLiveEvent?.(e);
163
+ adminOptions.onLiveEvent?.(e);
143
164
  },
144
- enabled: (queryOptions?.enabled ?? true) && id != null,
145
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
146
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
165
+ liveParams: options.liveParams,
166
+ enabled: (options.queryOptions?.enabled ?? true) && (options.id ?? parsed.id) != null,
147
167
  }));
148
168
 
149
- const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
169
+ let lastSuccess = $state(0);
170
+ let lastError = $state(0);
150
171
 
151
172
  $effect(() => {
152
- if (query.isSuccess && options.successNotification) {
173
+ if (query.isSuccess && options.successNotification && query.dataUpdatedAt > lastSuccess) {
174
+ lastSuccess = query.dataUpdatedAt;
175
+ const resource = options.resource ?? parsed.resource ?? '';
153
176
  fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
154
- } else if (query.isError) {
177
+ } else if (query.isError && query.errorUpdatedAt > lastError) {
178
+ lastError = query.errorUpdatedAt;
155
179
  fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
156
180
  }
157
181
  });
@@ -193,29 +217,51 @@ export interface UseManyOptions<TData extends BaseRecord = BaseRecord, TError =
193
217
  }
194
218
 
195
219
  export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseManyOptions<TData, TError>) {
196
- const { resource, ids, meta, dataProviderName, queryOptions } = options;
197
220
  const adminOptions = getAdminOptions();
198
- const provider = getDataProviderForResource(resource, dataProviderName);
199
-
200
- const query = createQuery<GetManyResult<TData>, TError>(() => ({
201
- queryKey: [resource, 'many', ids, meta],
202
- queryFn: async () => {
203
- if (!ids.length) return { data: [] };
204
- if (provider.getMany) {
205
- return provider.getMany<TData>({ resource, ids, meta });
206
- }
207
- const results = await Promise.all(ids.map(id => provider.getOne<TData>({ resource, id, meta })));
208
- return { data: results.map(r => r.data) };
221
+
222
+ const query = createQuery<GetManyResult<TData>, TError>(() => {
223
+ const { resource, ids, meta, dataProviderName, queryOptions } = options;
224
+ const provider = getDataProviderForResource(resource, dataProviderName);
225
+ return {
226
+ queryKey: [resource, 'many', ids, meta],
227
+ queryFn: async () => {
228
+ if (!ids.length) return { data: [] };
229
+ if (provider.getMany) {
230
+ return provider.getMany<TData>({ resource, ids, meta });
231
+ }
232
+ const results = await Promise.all(ids.map(id => provider.getOne<TData>({ resource, id, meta })));
233
+ return { data: results.map(r => r.data) };
234
+ },
235
+ enabled: (queryOptions?.enabled ?? true) && ids.length > 0,
236
+ staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
237
+ };
238
+ });
239
+
240
+ const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
241
+
242
+ createLiveSubscription(() => ({
243
+ resource: options.resource,
244
+ liveProvider: getLiveProvider(),
245
+ liveMode: options.liveMode ?? adminOptions.liveMode,
246
+ onLiveEvent: (e) => {
247
+ options.onLiveEvent?.(e);
248
+ adminOptions.onLiveEvent?.(e);
209
249
  },
210
- enabled: (queryOptions?.enabled ?? true) && ids.length > 0,
211
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
250
+ liveParams: options.liveParams,
251
+ enabled: (options.queryOptions?.enabled ?? true) && options.ids?.length > 0,
212
252
  }));
213
253
 
214
- const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
254
+ let lastSuccess = $state(0);
255
+ let lastError = $state(0);
215
256
 
216
257
  $effect(() => {
217
- if (query.isSuccess && options.successNotification) fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
218
- else if (query.isError) fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
258
+ if (query.isSuccess && options.successNotification && query.dataUpdatedAt > lastSuccess) {
259
+ lastSuccess = query.dataUpdatedAt;
260
+ fireSuccessNotification(options.successNotification, '', query.data, undefined, options.resource);
261
+ } else if (query.isError && query.errorUpdatedAt > lastError) {
262
+ lastError = query.errorUpdatedAt;
263
+ fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
264
+ }
219
265
  });
220
266
 
221
267
  return extendQuery(query, () => ({ overtime }));
@@ -18,6 +18,8 @@ export interface RouterProvider {
18
18
  params: Record<string, string>;
19
19
  pathname: string;
20
20
  };
21
+ /** Formats an internal path into an href string for <a> tags */
22
+ formatLink?: (path: string) => string;
21
23
  }
22
24
 
23
25
  // ─── Hash Router (default) ──────────────────────────────────
@@ -25,21 +27,29 @@ export interface RouterProvider {
25
27
  export function createHashRouterProvider(): RouterProvider {
26
28
  return {
27
29
  go({ to, query, type = 'push' }) {
28
- let url = `#${to}`;
30
+ if (typeof window === 'undefined') return;
31
+ let url = to;
29
32
  if (query) {
30
33
  const params = new URLSearchParams(query).toString();
31
34
  if (params) url += `?${params}`;
32
35
  }
33
36
  if (type === 'replace') {
34
- window.location.replace(url);
37
+ const urlObj = new URL(window.location.href);
38
+ urlObj.hash = url;
39
+ window.history.replaceState(null, '', urlObj.href);
40
+ window.dispatchEvent(new HashChangeEvent('hashchange'));
35
41
  } else {
36
- window.location.hash = to;
42
+ window.location.hash = url;
37
43
  }
38
44
  },
39
45
  back() {
46
+ if (typeof window === 'undefined') return;
40
47
  history.back();
41
48
  },
42
49
  parse() {
50
+ if (typeof window === 'undefined') {
51
+ return { resource: undefined, action: undefined, id: undefined, params: {}, pathname: '/' };
52
+ }
43
53
  const hash = window.location.hash.slice(1) || '/';
44
54
  const [pathname, queryString] = hash.split('?');
45
55
  const params: Record<string, string> = {};
@@ -57,6 +67,9 @@ export function createHashRouterProvider(): RouterProvider {
57
67
  pathname,
58
68
  };
59
69
  },
70
+ formatLink(path) {
71
+ return '#' + path.replace(/^#/, '');
72
+ },
60
73
  };
61
74
  }
62
75
 
@@ -65,6 +78,7 @@ export function createHashRouterProvider(): RouterProvider {
65
78
  export function createHistoryRouterProvider(basePath = ''): RouterProvider {
66
79
  return {
67
80
  go({ to, query, type = 'push' }) {
81
+ if (typeof window === 'undefined') return;
68
82
  let url = `${basePath}${to}`;
69
83
  if (query) {
70
84
  const params = new URLSearchParams(query).toString();
@@ -79,9 +93,13 @@ export function createHistoryRouterProvider(basePath = ''): RouterProvider {
79
93
  window.dispatchEvent(new PopStateEvent('popstate'));
80
94
  },
81
95
  back() {
96
+ if (typeof window === 'undefined') return;
82
97
  history.back();
83
98
  },
84
99
  parse() {
100
+ if (typeof window === 'undefined') {
101
+ return { resource: undefined, action: undefined, id: undefined, params: {}, pathname: '/' };
102
+ }
85
103
  const pathname = window.location.pathname.replace(basePath, '') || '/';
86
104
  const params: Record<string, string> = {};
87
105
  for (const [k, v] of new URLSearchParams(window.location.search).entries()) {
@@ -96,5 +114,8 @@ export function createHistoryRouterProvider(basePath = ''): RouterProvider {
96
114
  pathname,
97
115
  };
98
116
  },
117
+ formatLink(path) {
118
+ return basePath + path;
119
+ },
99
120
  };
100
121
  }
package/src/router.ts CHANGED
@@ -50,21 +50,34 @@ export function matchRoute(
50
50
  return null;
51
51
  }
52
52
 
53
- /**
54
- * Navigate to a path. Uses RouterProvider if available, falls back to hash.
55
- */
56
53
  export function navigate(path: string, options?: { replaceState?: boolean }): void {
57
54
  if (_routerProvider) {
58
55
  _routerProvider.go({ to: path, type: options?.replaceState ? 'replace' : 'push' });
59
- } else {
56
+ } else if (typeof window !== 'undefined') {
60
57
  window.location.hash = '#' + path.replace(/^#/, '');
61
58
  }
62
59
  }
63
60
 
61
+ /**
62
+ * Format a path into an href suitable for <a> tags.
63
+ */
64
+ export function formatLink(path: string): string {
65
+ if (_routerProvider?.formatLink) {
66
+ return _routerProvider.formatLink(path);
67
+ }
68
+ return '#' + path.replace(/^#/, '');
69
+ }
70
+
64
71
  export function currentPath(): string {
65
72
  if (_routerProvider) {
66
73
  const parsed = _routerProvider.parse();
67
- return parsed.pathname || '/';
74
+ const pathname = parsed.pathname || '/';
75
+ let qs = '';
76
+ if (parsed.params && Object.keys(parsed.params).length > 0) {
77
+ qs = '?' + new URLSearchParams(parsed.params).toString();
78
+ }
79
+ return pathname + qs;
68
80
  }
81
+ if (typeof window === 'undefined') return '/';
69
82
  return window.location.hash.replace(/^#/, '') || '/';
70
83
  }
@@ -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 === 'clone' ? 'edit' : options.action, id: options.id });
34
+ targetUrl = getToPath({ resource: options.resource, action: options.action, id: options.id });
34
35
  }
35
36
  if (!targetUrl) targetUrl = '/';
36
37
 
@@ -115,9 +115,9 @@ export function useTable<
115
115
  writeURLState({
116
116
  page: pagination.current,
117
117
  pageSize: pagination.pageSize,
118
- sortField: effectiveSorters[0]?.field,
119
- sortOrder: effectiveSorters[0]?.order,
120
- filters: effectiveFilters,
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 (effectiveSorters[0]) {
130
- params.set('sortField', effectiveSorters[0].field);
131
- params.set('sortOrder', effectiveSorters[0].order);
129
+ if (currentSorters[0]) {
130
+ params.set('sort', currentSorters[0].field);
131
+ params.set('order', currentSorters[0].order);
132
132
  }
133
- if (effectiveFilters.length > 0) {
134
- params.set('filters', JSON.stringify(effectiveFilters));
133
+ if (currentFilters.length > 0) {
134
+ params.set('filters', JSON.stringify(currentFilters));
135
135
  }
136
136
  return `?${params.toString()}`;
137
137
  }
@@ -94,12 +94,13 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
94
94
  this.currentFilters = initFilters;
95
95
 
96
96
  // Create query
97
+ const _this = this;
97
98
  const { meta, syncWithLocation: _s, dataProviderName, pagination: _p, sorters: _sr, filters: _f, ...restOptions } = options;
98
99
  this._queryResult = useList<TData, TError>({
99
100
  resource,
100
- pagination: this.queryPagination,
101
- sorters: this.effectiveSorters,
102
- filters: this.effectiveFilters,
101
+ get pagination() { return _this.queryPagination; },
102
+ get sorters() { return _this.effectiveSorters; },
103
+ get filters() { return _this.effectiveFilters; },
103
104
  meta,
104
105
  dataProviderName,
105
106
  ...restOptions,
@@ -111,9 +112,9 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
111
112
  writeURLState({
112
113
  page: this.pagination.current,
113
114
  pageSize: this.pagination.pageSize,
114
- sortField: this.effectiveSorters[0]?.field,
115
- sortOrder: this.effectiveSorters[0]?.order,
116
- filters: this.effectiveFilters,
115
+ sortField: this.currentSorters[0]?.field,
116
+ sortOrder: this.currentSorters[0]?.order,
117
+ filters: this.currentFilters,
117
118
  });
118
119
  });
119
120
  }