@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.
@@ -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
- const defaultOptions: AdminOptions = {
58
- mutationMode: 'pessimistic',
59
- warnWhenUnsavedChanges: false,
60
- syncWithLocation: false,
61
- liveMode: 'auto',
62
- disableServerSideValidation: false,
63
- undoableTimeout: 5000,
64
- textTransformers: defaultTextTransformers,
65
- redirect: { afterCreate: 'list', afterEdit: 'list', afterClone: 'list' },
66
- breadcrumb: 'default',
67
- disableRouteChangeHandler: false,
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>(defaultOptions);
72
+ let adminOptions = $state<AdminOptions>(createDefaultOptions());
71
73
 
72
- export function setAdminOptions(options: AdminOptions): void {
73
- adminOptions = {
74
- ...defaultOptions,
75
- ...options,
76
- textTransformers: { ...defaultTextTransformers, ...options.textTransformers },
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();
@@ -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: () => E,
26
+ extensions: E,
26
27
  ): Q & E {
27
28
  return new Proxy(query, {
28
29
  get(target, prop) {
29
- if (typeof prop === 'string') {
30
- const ext = extensions();
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[prop as keyof Q];
33
+ return Reflect.get(target, prop);
34
34
  },
35
35
  has(target, prop) {
36
- if (typeof prop === 'string' && prop in extensions()) return true;
37
- return prop in target;
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>(options: UseListOptions<TData, TError> = {}) {
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
- const provider = getDataProviderForResource(resource, options.dataProviderName);
65
-
66
- const queryOptions = options.queryOptions;
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
- queryKey: [resource, 'list', options.pagination, options.sorters, options.filters, options.meta],
70
- queryFn: async () => {
71
- const result = await provider.getList<TData>({
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: options.pagination,
74
- sorters: options.sorters,
75
- filters: options.filters,
76
- meta: options.meta,
77
- });
78
- return result;
79
- },
80
- enabled: queryOptions?.enabled ?? true,
81
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
82
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
83
- refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
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, options.overtimeOptions ?? adminOptions.overtime);
91
+ const overtime = createOvertimeTracker(() => query.isLoading, typeof optionsOrGetter === 'function' ? optionsOrGetter().overtimeOptions : optionsOrGetter.overtimeOptions ?? adminOptions.overtime);
87
92
 
88
- createLiveSubscription((): LiveSubscriptionParams => ({
89
- resource,
90
- liveProvider: getLiveProvider(),
91
- liveMode: options.liveMode ?? adminOptions.liveMode,
92
- onLiveEvent: (e: LiveEvent) => {
93
- options.onLiveEvent?.(e);
94
- adminOptions.onLiveEvent?.(e);
95
- },
96
- liveParams: options.liveParams,
97
- enabled: queryOptions?.enabled ?? true,
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
- if (query.isSuccess && options.successNotification) {
102
- fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
103
- } else if (query.isError) {
104
- fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
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, () => ({ overtime }));
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>(options: UseOneOptions<TData, TError> = {}) {
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
- const provider = getDataProviderForResource(resource, options.dataProviderName);
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 queryOptions = options.queryOptions;
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
- const query = createQuery<GetOneResult<TData>, TError>(() => ({
137
- queryKey: [resource, 'one', id, options.meta],
138
- queryFn: async () => {
139
- if (id == null) throw new Error('useOne requires an id');
140
- const result = await provider.getOne<TData>({
141
- resource,
142
- id,
143
- meta: options.meta,
144
- });
145
- return result;
146
- },
147
- enabled: (queryOptions?.enabled ?? true) && id != null,
148
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
149
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
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, options.overtimeOptions ?? adminOptions.overtime);
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
- if (query.isSuccess && options.successNotification) {
156
- fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
157
- } else if (query.isError) {
158
- fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
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, () => ({ overtime }));
207
+ return extendQuery(query, { overtime });
163
208
  }
164
209
 
165
210
  // ─── useShow ──────────────────────────────────────────────────
166
211
  export function useShow<TData extends BaseRecord = BaseRecord, TError = HttpError>(
167
- options: UseOneOptions<TData, TError> = {}
212
+ optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}
168
213
  ) {
169
214
  const parsed = useParsed();
170
- let showId = $state<string | number | undefined>(options.id ?? parsed.id);
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) { showId = id; }
222
+ function setShowId(id: string | number) { _showId = id; }
173
223
 
174
- const result = useOne<TData, TError>({
175
- ...options,
176
- get id() { return showId; },
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, () => ({ showId, setShowId }));
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>(options: UseManyOptions<TData, TError>) {
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 provider = getDataProviderForResource(resource, dataProviderName);
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 overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
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
- if (query.isSuccess && options.successNotification) fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
221
- else if (query.isError) fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
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, () => ({ overtime }));
306
+ return extendQuery(query, { overtime });
225
307
  }
226
308
 
227
309
  export function useApiUrl(dataProviderName?: string): string {
@@ -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.replace(basePath, '') || '/';
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
- // Store a reference to the active RouterProvider (set by router-state.svelte.ts)
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
- let path = `/${resource}`;
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
- const targetResource = resourceName ?? parsed.resource;
74
- if (!targetResource) {
75
- return { resource: undefined, resources: getResources() };
76
- }
77
- try {
78
- const resource = getResource(targetResource);
79
- return { resource, resources: getResources() };
80
- } catch {
81
- return { resource: undefined, resources: getResources() };
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(),