@svadmin/ui 0.40.5 → 0.41.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.
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { captureAdminContext, getResources, getChatProvider, toggleTheme } from '@svadmin/core';
2
+ import { captureAdminContext, toggleTheme } from '@svadmin/core';
3
3
  import { useTranslation } from '@svadmin/core/i18n';
4
4
  import { Command } from './ui/command/index.js';
5
5
  import * as Dialog from './ui/dialog/index.js';
@@ -12,24 +12,42 @@
12
12
  const adminContext = captureAdminContext();
13
13
  let searchValue = $state('');
14
14
 
15
- const resources = getResources();
16
- const provider = $derived(getChatProvider());
15
+ const resources = $derived(adminContext.resources);
16
+ const provider = $derived(adminContext.chatProvider);
17
17
 
18
18
  // AI State
19
19
  let aiMode = $state(false);
20
20
  let aiResponse = $state('');
21
21
  let isPredicting = $state(false);
22
22
  let abortController: AbortController | null = null;
23
+ let requestEpoch = 0;
24
+
25
+ $effect(() => {
26
+ void provider;
27
+ void adminContext.tenantCacheKey?.__svadminTenant;
28
+ void open;
29
+ clearAIConversation();
30
+
31
+ return cancelAIRequest;
32
+ });
23
33
 
24
34
  function close() {
25
35
  open = false;
36
+ clearAIConversation();
37
+ }
38
+
39
+ function cancelAIRequest() {
40
+ requestEpoch += 1;
41
+ abortController?.abort();
42
+ abortController = null;
43
+ }
44
+
45
+ function clearAIConversation() {
46
+ cancelAIRequest();
26
47
  searchValue = '';
27
48
  aiMode = false;
28
49
  aiResponse = '';
29
- if (abortController) {
30
- abortController.abort();
31
- abortController = null;
32
- }
50
+ isPredicting = false;
33
51
  }
34
52
 
35
53
  function act(fn: () => void) {
@@ -38,45 +56,50 @@
38
56
  }
39
57
 
40
58
  async function askAI() {
41
- if (!provider || !searchValue.trim()) return;
42
-
59
+ const scopedProvider = provider;
60
+ const prompt = searchValue.trim();
61
+ if (!scopedProvider || !prompt) return;
62
+
63
+ cancelAIRequest();
64
+ const epoch = requestEpoch;
65
+ const controller = new AbortController();
66
+ abortController = controller;
43
67
  aiMode = true;
44
68
  isPredicting = true;
45
69
  aiResponse = '';
46
-
47
- if (abortController) abortController.abort();
48
- abortController = new AbortController();
49
70
 
50
71
  try {
51
- const result = provider.sendMessage(
52
- [{ id: 'cmd', role: 'user', content: searchValue, timestamp: Date.now() }],
53
- { signal: abortController.signal }
72
+ const result = scopedProvider.sendMessage(
73
+ [{ id: 'cmd', role: 'user', content: prompt, timestamp: Date.now() }],
74
+ { signal: controller.signal }
54
75
  );
55
76
 
56
77
  if (Symbol.asyncIterator in result) {
57
78
  for await (const chunk of result as AsyncIterable<string>) {
79
+ if (epoch !== requestEpoch) return;
58
80
  aiResponse += chunk;
59
81
  }
60
82
  } else {
61
- aiResponse = await result;
83
+ const response = await result;
84
+ if (epoch === requestEpoch) aiResponse = response;
62
85
  }
63
86
  } catch (err: unknown) {
64
- if (err instanceof Error && err.name !== 'AbortError') {
87
+ if (epoch === requestEpoch && err instanceof Error && err.name !== 'AbortError') {
65
88
  aiResponse = 'Failed to get response from AI.';
66
89
  }
67
90
  } finally {
68
- isPredicting = false;
91
+ if (epoch === requestEpoch) {
92
+ isPredicting = false;
93
+ if (abortController === controller) abortController = null;
94
+ }
69
95
  }
70
96
  }
71
97
 
72
98
  function handleKeyDown(e: KeyboardEvent) {
73
99
  if (e.key === 'Enter' && searchValue.trim() && !aiMode) {
74
- // If pressing enter on an active search, cmdk usually handles selection.
75
- // But if there's no selection or we want to force AI, we can intercept.
76
- // For simplicity, we trigger AI on Ctrl+Enter
77
100
  if (e.ctrlKey || e.metaKey) {
78
101
  e.preventDefault();
79
- askAI();
102
+ void askAI();
80
103
  }
81
104
  }
82
105
  }
@@ -170,7 +193,7 @@
170
193
  <div class="p-2 border-t flex justify-end gap-2 bg-muted/10">
171
194
  <button
172
195
  class="text-xs px-3 py-1.5 rounded-md hover:bg-muted transition-colors text-muted-foreground"
173
- onclick={() => { aiMode = false; searchValue = ''; }}
196
+ onclick={clearAIConversation}
174
197
  >
175
198
  Clear & Return
176
199
  </button>
@@ -1,15 +1,16 @@
1
1
  <script lang="ts">
2
2
  import { useTranslation } from '@svadmin/core/i18n';
3
- import { getResources, getDataProviderNames } from '@svadmin/core';
3
+ import { captureAdminContext } from '@svadmin/core';
4
4
  import { getResolvedTheme, getColorTheme } from '@svadmin/core';
5
5
  import * as Card from './ui/card/index.js';
6
6
  import { Badge } from './ui/badge/index.js';
7
7
  import { Info, Database, Globe, Palette, Layers } from '@lucide/svelte';
8
8
 
9
9
  const i18n = useTranslation();
10
+ const adminContext = captureAdminContext();
10
11
 
11
- const resources = getResources();
12
- const providerNames = getDataProviderNames();
12
+ const resources = $derived(adminContext.resources);
13
+ const providerNames = $derived(adminContext.getDataProviderNames());
13
14
 
14
15
  const version = '__SVADMIN_VERSION__'; // replaced at build time or shown as-is
15
16
  </script>
@@ -1,18 +1,3 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const AboutSettings: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type AboutSettings = InstanceType<typeof AboutSettings>;
1
+ declare const AboutSettings: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type AboutSettings = ReturnType<typeof AboutSettings>;
18
3
  export default AboutSettings;
@@ -19,7 +19,26 @@
19
19
  // Auto-inject design tokens so consumers don't need to manually import
20
20
  import '../app.css';
21
21
  import { onDestroy, onMount, untrack, type Component, type Snippet } from 'svelte';
22
- import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem, TaskProvider, I18nProvider } from '@svadmin/core';
22
+ import type {
23
+ AccessControlProvider,
24
+ AgentProvider,
25
+ AuditLogProvider,
26
+ AuthProvider,
27
+ ChatProvider,
28
+ DataProviderInput,
29
+ I18nProvider,
30
+ LiveProvider,
31
+ MenuItem,
32
+ NotificationProvider,
33
+ ProviderBundle,
34
+ ResourceDefinition,
35
+ RouterProvider,
36
+ TaskProvider,
37
+ TenantAdapter,
38
+ TenantContext,
39
+ ThemeConfig,
40
+ ThemeMode,
41
+ } from '@svadmin/core';
23
42
  import {
24
43
  captureAdminContext,
25
44
  createHashRouterProvider,
@@ -55,12 +74,27 @@
55
74
  import { Badge } from './ui/badge/index.js';
56
75
  import { Skeleton } from './ui/skeleton/index.js';
57
76
  import { provideRouterState } from '../router-state.svelte.js';
77
+ import type { AdminProviderBundle } from '../types.js';
58
78
 
59
79
  interface Props {
60
- dataProvider: DataProvider;
80
+ /** Single provider, or named providers such as `{ default, analytics }`. */
81
+ dataProvider?: DataProviderInput;
82
+ /** Core canonical provider bundle. Existing top-level props take precedence. */
83
+ providerBundle?: AdminProviderBundle;
84
+ /** Compatibility alias for providerBundle. */
85
+ providers?: AdminProviderBundle;
61
86
  authProvider?: AuthProvider;
87
+ accessControlProvider?: AccessControlProvider;
88
+ liveProvider?: LiveProvider;
89
+ auditLogProvider?: AuditLogProvider;
90
+ notificationProvider?: NotificationProvider;
91
+ chatProvider?: ChatProvider;
92
+ agentProvider?: AgentProvider;
62
93
  taskProvider?: TaskProvider;
63
94
  routerProvider?: RouterProvider;
95
+ tenantAdapter?: TenantAdapter;
96
+ /** Request/tree-scoped tenant. Tenant identifiers are never rendered by DevTools. */
97
+ tenant?: TenantContext;
64
98
  resources: ResourceDefinition[];
65
99
  /** Bindable tree-local locale; defaults to the provider locale or per-tree browser detection (English during SSR). */
66
100
  locale?: string;
@@ -102,9 +136,19 @@
102
136
 
103
137
  let {
104
138
  dataProvider,
139
+ providerBundle,
140
+ providers,
105
141
  authProvider,
142
+ accessControlProvider,
143
+ liveProvider,
144
+ auditLogProvider,
145
+ notificationProvider,
146
+ chatProvider,
147
+ agentProvider,
106
148
  taskProvider,
107
149
  routerProvider,
150
+ tenantAdapter,
151
+ tenant,
108
152
  resources,
109
153
  locale = $bindable(),
110
154
  i18nProvider,
@@ -134,10 +178,24 @@
134
178
  Skeleton: Skeleton as unknown as ComponentRegistry['Skeleton'],
135
179
  };
136
180
 
181
+ const resolvedProviderBundle = $derived(providerBundle ?? providers);
182
+ const resolvedDataProvider = $derived(dataProvider ?? resolvedProviderBundle?.dataProvider);
183
+ const resolvedAuthProvider = $derived(authProvider ?? resolvedProviderBundle?.authProvider ?? undefined);
184
+ const resolvedAccessControlProvider = $derived(accessControlProvider ?? resolvedProviderBundle?.accessControlProvider ?? undefined);
185
+ const resolvedLiveProvider = $derived(liveProvider ?? resolvedProviderBundle?.liveProvider);
186
+ const resolvedAuditLogProvider = $derived(auditLogProvider ?? resolvedProviderBundle?.auditLogProvider ?? undefined);
187
+ const resolvedNotificationProvider = $derived(notificationProvider ?? resolvedProviderBundle?.notificationProvider ?? undefined);
188
+ const resolvedChatProvider = $derived(chatProvider ?? resolvedProviderBundle?.chatProvider ?? undefined);
189
+ const resolvedAgentProvider = $derived(agentProvider ?? resolvedProviderBundle?.agentProvider ?? undefined);
190
+ const resolvedTaskProvider = $derived(taskProvider ?? resolvedProviderBundle?.taskProvider);
191
+ const resolvedRouterInput = $derived(routerProvider ?? resolvedProviderBundle?.routerProvider);
192
+ const resolvedTenantAdapter = $derived(tenantAdapter ?? resolvedProviderBundle?.tenantAdapter);
193
+ const resolvedI18nProvider = $derived(i18nProvider ?? resolvedProviderBundle?.i18nProvider);
194
+
137
195
  // Resolve router provider (default to hash)
138
196
  const mergedComponents = $derived.by((): ComponentRegistry => ({ ...defaultComponents, ...userComponents }));
139
- const resolvedRouter = $derived(routerProvider ?? createHashRouterProvider());
140
- const resolvedRouteMode = $derived(routeMode ?? (routerProvider ? 'auto' : 'hash'));
197
+ const resolvedRouter = $derived(resolvedRouterInput ?? createHashRouterProvider());
198
+ const resolvedRouteMode = $derived(routeMode ?? (resolvedRouterInput ? 'auto' : 'hash'));
141
199
 
142
200
  const routerState = provideRouterState(untrack(() => resolvedRouter));
143
201
  const scopedRouterProvider: RouterProvider = {
@@ -166,14 +224,14 @@
166
224
 
167
225
  const i18nScope = createI18nScope({
168
226
  locale: untrack(() => locale),
169
- provider: untrack(() => i18nProvider),
227
+ provider: untrack(() => resolvedI18nProvider),
170
228
  onLocaleChange: updateBoundLocale,
171
229
  });
172
230
  provideI18nScope(i18nScope);
173
231
  const translation = useTranslation();
174
232
 
175
233
  $effect.pre(() => {
176
- const ownerProvider = i18nProvider;
234
+ const ownerProvider = resolvedI18nProvider;
177
235
  const ownerLocale = locale;
178
236
  untrack(() => i18nScope.updateOwner({
179
237
  locale: ownerLocale,
@@ -210,11 +268,23 @@
210
268
  }
211
269
 
212
270
  provideAdminContext({
213
- get dataProvider() { return dataProvider; },
214
- get authProvider() { return authProvider ?? null; },
215
- get taskProvider() { return taskProvider; },
271
+ get providerBundle() { return resolvedProviderBundle as ProviderBundle | undefined; },
272
+ get dataProvider() {
273
+ if (!resolvedDataProvider) throw new Error('AdminApp requires a DataProvider.');
274
+ return resolvedDataProvider;
275
+ },
276
+ get authProvider() { return resolvedAuthProvider ?? null; },
277
+ get accessControlProvider() { return resolvedAccessControlProvider ?? null; },
278
+ get liveProvider() { return resolvedLiveProvider; },
279
+ get auditLogProvider() { return resolvedAuditLogProvider ?? null; },
280
+ get notificationProvider() { return resolvedNotificationProvider ?? null; },
281
+ get chatProvider() { return resolvedChatProvider ?? null; },
282
+ get agentProvider() { return resolvedAgentProvider ?? null; },
283
+ get taskProvider() { return resolvedTaskProvider; },
216
284
  get resources() { return resources; },
217
285
  get routerProvider() { return scopedRouterProvider; },
286
+ get tenantAdapter() { return resolvedTenantAdapter; },
287
+ get tenant() { return tenant; },
218
288
  });
219
289
  const adminContext = captureAdminContext();
220
290
 
@@ -244,8 +314,8 @@
244
314
  });
245
315
 
246
316
  // Auth check
247
- let isAuthenticated = $state(untrack(() => !authProvider));
248
- let authChecked = $state(untrack(() => !authProvider));
317
+ let isAuthenticated = $state(untrack(() => !resolvedAuthProvider));
318
+ let authChecked = $state(untrack(() => !resolvedAuthProvider));
249
319
 
250
320
  async function navigateWithinApp(path: string) {
251
321
  await adminContext.navigate(path);
@@ -263,7 +333,7 @@
263
333
  // Explicitly track route so auth is re-checked on navigation
264
334
  const _route = route;
265
335
 
266
- if (!authProvider) {
336
+ if (!resolvedAuthProvider) {
267
337
  if (!cancelled) {
268
338
  isAuthenticated = true;
269
339
  authChecked = true;
@@ -271,7 +341,7 @@
271
341
  return;
272
342
  }
273
343
  authChecked = false;
274
- authProvider.check().then(result => {
344
+ resolvedAuthProvider.check().then(result => {
275
345
  if (cancelled) return;
276
346
  isAuthenticated = result.authenticated;
277
347
  authChecked = true;
@@ -293,7 +363,9 @@
293
363
  </script>
294
364
 
295
365
  <QueryClientProvider client={queryClient}>
296
- {#if !authChecked}
366
+ {#if !resolvedDataProvider}
367
+ <ConfigErrorScreen title="{title} — DataProvider is required" />
368
+ {:else if !authChecked}
297
369
  <div class="flex h-screen items-center justify-center">
298
370
  <div class="space-y-4 text-center">
299
371
  <div class="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary mx-auto"></div>
@@ -302,7 +374,7 @@
302
374
  </div>
303
375
  {:else if route === '/login' && loginPage}
304
376
  {@render loginPage()}
305
- {:else if route === '/login' && authProvider}
377
+ {:else if route === '/login' && resolvedAuthProvider}
306
378
  <LoginPage
307
379
  {title}
308
380
  defaultIdentifier={loginDefaults?.identifier}
@@ -310,15 +382,15 @@
310
382
  loginHint={loginDefaults?.hint}
311
383
  onSuccess={() => { isAuthenticated = true; void navigateWithinApp('/'); }}
312
384
  />
313
- {:else if route === '/register' && authProvider?.register}
385
+ {:else if route === '/register' && resolvedAuthProvider?.register}
314
386
  <RegisterPage {title} />
315
- {:else if route === '/forgot-password' && authProvider?.forgotPassword}
387
+ {:else if route === '/forgot-password' && resolvedAuthProvider?.forgotPassword}
316
388
  <ForgotPasswordPage {title} />
317
- {:else if route === '/update-password' && authProvider?.updatePassword}
389
+ {:else if route === '/update-password' && resolvedAuthProvider?.updatePassword}
318
390
  <UpdatePasswordPage {title} />
319
391
  {:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
320
392
  <ConfigErrorScreen title="{title} — {translation.t('common.configRequired')}" />
321
- {:else if (isAuthenticated || !authProvider) && isStandaloneTwoFactor}
393
+ {:else if (isAuthenticated || !resolvedAuthProvider) && isStandaloneTwoFactor}
322
394
  <SystemPageShell {title}>
323
395
  <LazyPage loader={loadTwoFactorAuthPage} props={{}} />
324
396
  </SystemPageShell>
@@ -327,7 +399,7 @@
327
399
  {@const ErrorComp = mergedComponents.ErrorPage || ErrorPage}
328
400
  <ErrorComp status={standaloneErrorStatus} />
329
401
  </SystemPageShell>
330
- {:else if isAuthenticated || !authProvider}
402
+ {:else if isAuthenticated || !resolvedAuthProvider}
331
403
  <Layout {title} {menu} {siteUrl} routeMode={resolvedRouteMode}>
332
404
  {#key route + (params.resource ?? '') + (params.id ?? '') + (params.variant ?? '') + (params.columns ?? '')}
333
405
  <div class="svadmin-page-enter">
@@ -1,13 +1,28 @@
1
1
  import '../app.css';
2
2
  import { type Component, type Snippet } from 'svelte';
3
- import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem, TaskProvider, I18nProvider } from '@svadmin/core';
3
+ import type { AccessControlProvider, AgentProvider, AuditLogProvider, AuthProvider, ChatProvider, DataProviderInput, I18nProvider, LiveProvider, MenuItem, NotificationProvider, ResourceDefinition, RouterProvider, TaskProvider, TenantAdapter, TenantContext, ThemeConfig, ThemeMode } from '@svadmin/core';
4
4
  import { QueryClient, type DefaultOptions } from '@tanstack/svelte-query';
5
5
  import { type ComponentRegistry } from '../component-registry.svelte.js';
6
+ import type { AdminProviderBundle } from '../types.js';
6
7
  interface Props {
7
- dataProvider: DataProvider;
8
+ /** Single provider, or named providers such as `{ default, analytics }`. */
9
+ dataProvider?: DataProviderInput;
10
+ /** Core canonical provider bundle. Existing top-level props take precedence. */
11
+ providerBundle?: AdminProviderBundle;
12
+ /** Compatibility alias for providerBundle. */
13
+ providers?: AdminProviderBundle;
8
14
  authProvider?: AuthProvider;
15
+ accessControlProvider?: AccessControlProvider;
16
+ liveProvider?: LiveProvider;
17
+ auditLogProvider?: AuditLogProvider;
18
+ notificationProvider?: NotificationProvider;
19
+ chatProvider?: ChatProvider;
20
+ agentProvider?: AgentProvider;
9
21
  taskProvider?: TaskProvider;
10
22
  routerProvider?: RouterProvider;
23
+ tenantAdapter?: TenantAdapter;
24
+ /** Request/tree-scoped tenant. Tenant identifiers are never rendered by DevTools. */
25
+ tenant?: TenantContext;
11
26
  resources: ResourceDefinition[];
12
27
  /** Bindable tree-local locale; defaults to the provider locale or per-tree browser detection (English during SSR). */
13
28
  locale?: string;
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
- import { getAuditLogProvider, useTranslation } from '@svadmin/core';
2
+ import { captureAdminContext, useTranslation } from '@svadmin/core';
3
3
 
4
- import type { AuditEntry } from '@svadmin/core';
4
+ import type { AuditEntry, AuditLogProvider } from '@svadmin/core';
5
5
  import * as Sheet from './ui/sheet/index.js';
6
6
  import { History, Loader2 } from '@lucide/svelte';
7
7
 
8
8
  const i18n = useTranslation();
9
+ const adminContext = captureAdminContext();
9
10
 
10
11
  let { open = $bindable(false), resource, recordId } = $props<{
11
12
  open: boolean;
@@ -15,25 +16,53 @@
15
16
 
16
17
  let logs = $state<AuditEntry[]>([]);
17
18
  let isLoading = $state(false);
19
+ let requestEpoch = 0;
18
20
 
19
21
  $effect(() => {
20
- if (open) {
21
- const provider = getAuditLogProvider();
22
- if (provider) {
23
- isLoading = true;
24
- provider.get({ resource, meta: recordId ? { recordId } : undefined })
25
- .then((data) => {
26
- logs = data;
27
- isLoading = false;
28
- })
29
- .catch((err) => {
30
- console.error('[svadmin] Failed to fetch audit logs', err);
31
- isLoading = false;
32
- });
33
- }
22
+ const scopedProvider = adminContext.auditLogProvider;
23
+ const scopedResource = resource;
24
+ const scopedRecordId = recordId;
25
+ const shouldLoad = open;
26
+ const providerMeta = adminContext.getProviderMeta(
27
+ scopedResource,
28
+ scopedRecordId == null ? undefined : { recordId: scopedRecordId },
29
+ );
30
+
31
+ clearAuditScope();
32
+ if (shouldLoad && scopedProvider) {
33
+ void loadAuditLogs(scopedProvider, scopedResource, providerMeta);
34
34
  }
35
+
36
+ return cancelAuditRequest;
35
37
  });
36
38
 
39
+ function cancelAuditRequest() {
40
+ requestEpoch += 1;
41
+ }
42
+
43
+ function clearAuditScope() {
44
+ cancelAuditRequest();
45
+ logs = [];
46
+ isLoading = false;
47
+ }
48
+
49
+ async function loadAuditLogs(
50
+ scopedProvider: AuditLogProvider,
51
+ scopedResource: string,
52
+ providerMeta: Record<string, unknown> | undefined,
53
+ ) {
54
+ const epoch = requestEpoch;
55
+ isLoading = true;
56
+ try {
57
+ const entries = await scopedProvider.get({ resource: scopedResource, meta: providerMeta });
58
+ if (epoch === requestEpoch) logs = entries;
59
+ } catch (error) {
60
+ if (epoch === requestEpoch) console.error('[svadmin] Failed to fetch audit logs', error);
61
+ } finally {
62
+ if (epoch === requestEpoch) isLoading = false;
63
+ }
64
+ }
65
+
37
66
  // Action semantic color mapping
38
67
  function getActionColor(action: string) {
39
68
  switch (action) {
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { useTranslation } from '@svadmin/core/i18n';
3
3
 
4
- import { getAuthProvider } from '@svadmin/core';
5
- import type { AuditLog } from '@svadmin/core';
4
+ import { captureAdminContext } from '@svadmin/core';
5
+ import type { AuditLog, AuthProvider } from '@svadmin/core';
6
6
  import { toast } from '@svadmin/core/toast';
7
7
  import * as Table from './ui/table/index.js';
8
8
  import { Badge } from './ui/badge/index.js';
@@ -11,8 +11,8 @@
11
11
  import { Loader2, ChevronLeft, ChevronRight, Search, FileSearch, Eye, X } from '@lucide/svelte';
12
12
 
13
13
  const i18n = useTranslation();
14
-
15
- const authProvider = getAuthProvider({ optional: true });
14
+ const adminContext = captureAdminContext();
15
+ const authProvider = $derived(adminContext.authProvider);
16
16
 
17
17
  let logs = $state<AuditLog[]>([]);
18
18
  let total = $state(0);
@@ -28,17 +28,31 @@
28
28
 
29
29
  let requestId = 0;
30
30
 
31
- async function loadLogs() {
32
- if (!authProvider?.getAuditLogs) {
31
+ function closeSnapshot() {
32
+ drawerOpen = false;
33
+ drawerLog = null;
34
+ }
35
+
36
+ $effect(() => {
37
+ void authProvider;
38
+ void adminContext.tenantCacheKey?.__svadminTenant;
39
+ closeSnapshot();
40
+ });
41
+
42
+ async function loadLogs(scopedAuthProvider: AuthProvider | null, scopedPage: number) {
43
+ const currentId = ++requestId;
44
+ logs = [];
45
+ total = 0;
46
+ loading = true;
47
+ error = null;
48
+
49
+ if (!scopedAuthProvider?.getAuditLogs) {
33
50
  error = i18n.t('settings.auditNotSupported') ?? 'Audit logs are not supported by the current AuthProvider';
34
51
  loading = false;
35
52
  return;
36
53
  }
37
- const currentId = ++requestId;
38
- loading = true;
39
- error = null;
40
54
  try {
41
- const result = await authProvider.getAuditLogs({ page, pageSize });
55
+ const result = await scopedAuthProvider.getAuditLogs({ page: scopedPage, pageSize });
42
56
  if (currentId !== requestId) return;
43
57
  logs = result.data;
44
58
  total = result.total;
@@ -51,8 +65,12 @@
51
65
  }
52
66
 
53
67
  $effect(() => {
54
- void page;
55
- loadLogs();
68
+ void adminContext.tenantCacheKey?.__svadminTenant;
69
+ void loadLogs(authProvider, page);
70
+
71
+ return () => {
72
+ requestId += 1;
73
+ };
56
74
  });
57
75
 
58
76
  let totalPages = $derived(Math.ceil(total / pageSize) || 1);
@@ -193,7 +211,7 @@
193
211
  {#if drawerOpen && drawerLog}
194
212
  <!-- svelte-ignore a11y_no_static_element_interactions -->
195
213
  <!-- svelte-ignore a11y_click_events_have_key_events -->
196
- <div class="fixed inset-0 z-[100] flex justify-end bg-black/40 backdrop-blur-sm" onclick={() => drawerOpen = false}>
214
+ <div class="fixed inset-0 z-[100] flex justify-end bg-black/40 backdrop-blur-sm" onclick={closeSnapshot}>
197
215
  <!-- svelte-ignore a11y_no_static_element_interactions -->
198
216
  <!-- svelte-ignore a11y_click_events_have_key_events -->
199
217
  <div class="bg-card shadow-2xl w-full max-w-2xl h-full flex flex-col animate-in slide-in-from-right duration-300" onclick={(e) => e.stopPropagation()}>
@@ -206,7 +224,7 @@
206
224
  </h2>
207
225
  <p class="text-xs text-muted-foreground mt-1">{formatDate(drawerLog.createdAt)} · {drawerLog.userName ?? drawerLog.userId}</p>
208
226
  </div>
209
- <button onclick={() => drawerOpen = false} class="text-muted-foreground hover:text-foreground transition p-2 rounded-lg hover:bg-muted">
227
+ <button onclick={closeSnapshot} class="text-muted-foreground hover:text-foreground transition p-2 rounded-lg hover:bg-muted">
210
228
  <X class="w-5 h-5" />
211
229
  </button>
212
230
  </div>
@@ -34,7 +34,7 @@
34
34
  cell_getValue,
35
35
  } from '@tanstack/table-core/static-functions';
36
36
 
37
- import { captureAdminContext, useList, useDelete, useDeleteMany, getResource, notify } from '@svadmin/core';
37
+ import { captureAdminContext, useList, useDelete, useDeleteMany, getResource, notifyWithProvider } from '@svadmin/core';
38
38
  import type {
39
39
  BaseRecord,
40
40
  FieldDefinition,
@@ -43,7 +43,7 @@
43
43
  Pagination as PaginationState,
44
44
  Sort,
45
45
  } from '@svadmin/core';
46
- import { useCan, getAccessControlProvider } from '@svadmin/core';
46
+ import { useCan } from '@svadmin/core';
47
47
  import { readURLState, writeURLState } from '@svadmin/core';
48
48
  import { useTranslation } from '@svadmin/core/i18n';
49
49
 
@@ -259,7 +259,7 @@
259
259
  const deleteManyMutation = deleteManyResult.mutation;
260
260
 
261
261
  // ─── Permissions ──────────────────────────────────────────────
262
- const acEnabled = $derived(!!getAccessControlProvider());
262
+ const acEnabled = $derived(!!adminContext.accessControlProvider);
263
263
  const canCreatePerm = useCan(() => ({ resource: resourceName, action: 'create', queryOptions: { enabled: acEnabled } }));
264
264
  const canEditPerm = useCan(() => ({ resource: resourceName, action: 'edit', queryOptions: { enabled: acEnabled } }));
265
265
  const canDeletePerm = useCan(() => ({ resource: resourceName, action: 'delete', queryOptions: { enabled: acEnabled } }));
@@ -505,9 +505,15 @@
505
505
  confirmAction = async () => {
506
506
  try {
507
507
  await deleteManyMutation.mutateAsync({ ids, resource: resourceName });
508
- notify({ type: 'success', message: i18n.t('common.batchDeleteSuccess', { count: ids.length }) });
508
+ notifyWithProvider(
509
+ { type: 'success', message: i18n.t('common.batchDeleteSuccess', { count: ids.length }) },
510
+ adminContext.notificationProvider,
511
+ );
509
512
  } catch {
510
- notify({ type: 'error', message: i18n.t('common.batchDeletePartialFail', { failed: 1, total: ids.length }) });
513
+ notifyWithProvider(
514
+ { type: 'error', message: i18n.t('common.batchDeletePartialFail', { failed: 1, total: ids.length }) },
515
+ adminContext.notificationProvider,
516
+ );
511
517
  }
512
518
  rowSelectionAtom.set({});
513
519
  confirmOpen = false;