@svadmin/ui 0.57.0 → 0.59.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.
Files changed (40) hide show
  1. package/dist/components/AICommandBar.svelte +34 -10
  2. package/dist/components/AdminApp.svelte +16 -5
  3. package/dist/components/AutoForm.svelte +4 -4
  4. package/dist/components/AutoTable.svelte +884 -217
  5. package/dist/components/AutoTable.svelte.d.ts +1 -1
  6. package/dist/components/CommandPalette.svelte +40 -17
  7. package/dist/components/ConfirmDialog.svelte +4 -3
  8. package/dist/components/ConfirmDialog.svelte.d.ts +1 -0
  9. package/dist/components/CopilotPanel.svelte +3 -3
  10. package/dist/components/DraggableHeader.svelte +15 -36
  11. package/dist/components/DraggableHeader.svelte.d.ts +13 -4
  12. package/dist/components/EditPage.svelte +4 -4
  13. package/dist/components/EmptyState.svelte +4 -2
  14. package/dist/components/Layout.svelte +8 -1
  15. package/dist/components/ListPage.svelte +4 -12
  16. package/dist/components/ListPage.svelte.d.ts +1 -1
  17. package/dist/components/RecordDetailDrawer.svelte +121 -0
  18. package/dist/components/RecordDetailDrawer.svelte.d.ts +9 -0
  19. package/dist/components/ResourceAccessGuard.svelte +68 -0
  20. package/dist/components/ResourceAccessGuard.svelte.d.ts +12 -0
  21. package/dist/components/ResourceOperationsPage.svelte +8 -3
  22. package/dist/components/ShowPage.svelte +5 -6
  23. package/dist/components/StatsCard.svelte +5 -3
  24. package/dist/components/TaskQueueDrawer.svelte +10 -1
  25. package/dist/components/TenantSwitcher.svelte +1 -0
  26. package/dist/components/Toast.svelte +35 -1
  27. package/dist/components/UndoableNotification.svelte +12 -6
  28. package/dist/components/UndoableNotification.svelte.d.ts +2 -0
  29. package/dist/components/buttons/CloneButton.svelte +9 -2
  30. package/dist/components/buttons/CreateButton.svelte +9 -2
  31. package/dist/components/buttons/DeleteButton.svelte +9 -2
  32. package/dist/components/buttons/EditButton.svelte +9 -2
  33. package/dist/components/buttons/ListButton.svelte +1 -1
  34. package/dist/components/buttons/ShowButton.svelte +9 -2
  35. package/dist/components/saved-list-views.d.ts +35 -0
  36. package/dist/components/saved-list-views.js +168 -0
  37. package/dist/components/ui/input/input.svelte +2 -2
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +2 -0
  40. package/package.json +2 -2
@@ -1,15 +1,17 @@
1
1
  <script lang="ts">
2
- import { captureAdminContext, toggleTheme } from '@svadmin/core';
2
+ import { captureAdminContext, toggleTheme, useNavigation } 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';
6
6
  import { Search, LayoutDashboard, Plus, Sun, FileText, Sparkles, Loader2 } from '@lucide/svelte';
7
7
  import MarkdownRenderer from './MarkdownRenderer.svelte';
8
+ import CanAccess from './CanAccess.svelte';
8
9
 
9
10
  const i18n = useTranslation();
10
11
 
11
12
  let { open = $bindable(false) } = $props<{ open?: boolean }>();
12
13
  const adminContext = captureAdminContext();
14
+ const navigation = useNavigation();
13
15
  let searchValue = $state('');
14
16
 
15
17
  const resources = $derived(adminContext.resources);
@@ -109,6 +111,7 @@
109
111
 
110
112
  <Dialog.Dialog bind:open onOpenChange={(v: boolean) => !v && close()}>
111
113
  <Dialog.DialogContent class="overflow-hidden p-0 sm:max-w-[600px] border-none shadow-2xl">
114
+ <Dialog.DialogTitle class="sr-only">{i18n.t('common.actions')}</Dialog.DialogTitle>
112
115
  <Command.Root
113
116
  shouldFilter={!aiMode}
114
117
  class="flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground [&_[data-command-group-heading]]:text-muted-foreground [&_[data-command-group-heading]]:px-2 [&_[data-command-group-heading]]:py-1.5 [&_[data-command-group-heading]]:text-xs [&_[data-command-group-heading]]:font-semibold [&_[data-command-group-heading]]:uppercase [&_[data-command-group-heading]]:tracking-wider"
@@ -127,7 +130,8 @@
127
130
  />
128
131
  {#if searchValue.trim() && !aiMode}
129
132
  <button
130
- class="absolute right-3 flex items-center gap-1 text-[10px] font-medium text-warning bg-warning/10 hover:bg-warning/20 px-2 py-1 rounded transition-colors"
133
+ type="button"
134
+ class="absolute right-3 flex items-center gap-1 text-[10px] font-medium text-warning bg-warning/10 hover:bg-warning/20 px-2 py-1 rounded transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
131
135
  onclick={askAI}
132
136
  >
133
137
  <Sparkles class="h-3 w-3" />
@@ -149,10 +153,19 @@
149
153
  {i18n.t('common.home')}
150
154
  </Command.Item>
151
155
  {#each resources as r, _i (_i)}
152
- <Command.Item value={r.name} onSelect={() => act(() => adminContext.navigate(`/${r.name}`))} class={itemClass}>
153
- <FileText class="h-4 w-4 text-muted-foreground" />
154
- {r.label}
155
- </Command.Item>
156
+ {#if adminContext.accessControlProvider}
157
+ <CanAccess resource={r.name} action="list">
158
+ <Command.Item value={r.name} onSelect={() => act(() => navigation.list(r.name))} class={itemClass}>
159
+ <FileText class="h-4 w-4 text-muted-foreground" />
160
+ {r.label}
161
+ </Command.Item>
162
+ </CanAccess>
163
+ {:else}
164
+ <Command.Item value={r.name} onSelect={() => act(() => navigation.list(r.name))} class={itemClass}>
165
+ <FileText class="h-4 w-4 text-muted-foreground" />
166
+ {r.label}
167
+ </Command.Item>
168
+ {/if}
156
169
  {/each}
157
170
  </Command.Group>
158
171
 
@@ -161,10 +174,21 @@
161
174
  <!-- Actions -->
162
175
  <Command.Group heading={i18n.t('common.actions')}>
163
176
  {#each resources as r, _i (_i)}
164
- <Command.Item value={`create-${r.name}`} onSelect={() => act(() => adminContext.navigate(`/${r.name}/create`))} class={itemClass}>
165
- <Plus class="h-4 w-4 text-muted-foreground" />
166
- {i18n.t('common.create')} {r.label}
167
- </Command.Item>
177
+ {#if r.canCreate !== false}
178
+ {#if adminContext.accessControlProvider}
179
+ <CanAccess resource={r.name} action="create">
180
+ <Command.Item value={`create-${r.name}`} onSelect={() => act(() => navigation.create(r.name))} class={itemClass}>
181
+ <Plus class="h-4 w-4 text-muted-foreground" />
182
+ {i18n.t('common.create')} {r.label}
183
+ </Command.Item>
184
+ </CanAccess>
185
+ {:else}
186
+ <Command.Item value={`create-${r.name}`} onSelect={() => act(() => navigation.create(r.name))} class={itemClass}>
187
+ <Plus class="h-4 w-4 text-muted-foreground" />
188
+ {i18n.t('common.create')} {r.label}
189
+ </Command.Item>
190
+ {/if}
191
+ {/if}
168
192
  {/each}
169
193
  <Command.Item value="toggle-theme" onSelect={() => act(() => toggleTheme())} class={itemClass}>
170
194
  <Sun class="h-4 w-4 text-muted-foreground" />
@@ -73,6 +73,7 @@
73
73
  import ErrorPage from './ErrorPage.svelte';
74
74
  import SystemPageShell from './SystemPageShell.svelte';
75
75
  import LazyPage from './LazyPage.svelte';
76
+ import ResourceAccessGuard from './ResourceAccessGuard.svelte';
76
77
  import { Button } from './ui/button/index.js';
77
78
  import { Input } from './ui/input/index.js';
78
79
  import { Badge } from './ui/badge/index.js';
@@ -609,27 +610,37 @@
609
610
  {:else if (renderedRoute === '/:resource' || renderedRoute === '/:parent/:parentId/:resource') && renderedHasRouteResource}
610
611
  {#key renderedParams.resource}
611
612
  {@const Comp = renderedResourcePages?.list ?? mergedComponents.AutoTable}
612
- <Comp resourceName={renderedParams.resource} />
613
+ <ResourceAccessGuard resourceName={renderedParams.resource} action="list">
614
+ <Comp resourceName={renderedParams.resource} />
615
+ </ResourceAccessGuard>
613
616
  {/key}
614
617
  {:else if (renderedRoute === '/:resource/create' || renderedRoute === '/:parent/:parentId/:resource/create') && renderedHasRouteResource}
615
618
  {#key renderedParams.resource}
616
619
  {@const Comp = renderedResourcePages?.create ?? mergedComponents.AutoForm}
617
- <Comp resourceName={renderedParams.resource} mode="create" />
620
+ <ResourceAccessGuard resourceName={renderedParams.resource} action="create">
621
+ <Comp resourceName={renderedParams.resource} mode="create" />
622
+ </ResourceAccessGuard>
618
623
  {/key}
619
624
  {:else if (renderedRoute === '/:resource/edit/:id' || renderedRoute === '/:resource/:id/edit' || renderedRoute === '/:parent/:parentId/:resource/edit/:id') && renderedHasRouteResource}
620
625
  {#key `${renderedParams.resource}-${renderedParams.id}`}
621
626
  {@const Comp = renderedResourcePages?.edit ?? mergedComponents.AutoForm}
622
- <Comp resourceName={renderedParams.resource} mode="edit" id={renderedParams.id} />
627
+ <ResourceAccessGuard resourceName={renderedParams.resource} action="edit" id={renderedParams.id}>
628
+ <Comp resourceName={renderedParams.resource} mode="edit" id={renderedParams.id} />
629
+ </ResourceAccessGuard>
623
630
  {/key}
624
631
  {:else if (renderedRoute === '/:resource/show/:id' || renderedRoute === '/:resource/:id' || renderedRoute === '/:parent/:parentId/:resource/show/:id') && renderedHasRouteResource}
625
632
  {#key `${renderedParams.resource}-${renderedParams.id}`}
626
633
  {@const Comp = renderedResourcePages?.show ?? mergedComponents.ShowPage}
627
- <Comp resourceName={renderedParams.resource} id={renderedParams.id} />
634
+ <ResourceAccessGuard resourceName={renderedParams.resource} action="show" id={renderedParams.id}>
635
+ <Comp resourceName={renderedParams.resource} id={renderedParams.id} />
636
+ </ResourceAccessGuard>
628
637
  {/key}
629
638
  {:else if (renderedRoute === '/:resource/clone/:id' || renderedRoute === '/:parent/:parentId/:resource/clone/:id') && renderedHasRouteResource}
630
639
  {#key `${renderedParams.resource}-clone-${renderedParams.id}`}
631
640
  {@const Comp = renderedResourcePages?.clone ?? mergedComponents.AutoForm}
632
- <Comp resourceName={renderedParams.resource} mode="clone" id={renderedParams.id} />
641
+ <ResourceAccessGuard resourceName={renderedParams.resource} action="create" id={renderedParams.id} requireSourceRead>
642
+ <Comp resourceName={renderedParams.resource} mode="clone" id={renderedParams.id} />
643
+ </ResourceAccessGuard>
633
644
  {/key}
634
645
  {:else}
635
646
  {@const ErrorComp = mergedComponents.ErrorPage || ErrorPage}
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { captureAdminContext, useForm, getResource, deriveValidator } from '@svadmin/core';
2
+ import { useForm, getResource, deriveValidator, useNavigation } from '@svadmin/core';
3
3
  import { slide } from 'svelte/transition';
4
4
  import type { FieldDefinition } from '@svadmin/core';
5
5
  import { useTranslation } from '@svadmin/core/i18n';
@@ -45,7 +45,7 @@
45
45
  onSuccess,
46
46
  onNavigationGuardReady,
47
47
  }: Props = $props();
48
- const adminContext = captureAdminContext();
48
+ const navigation = useNavigation();
49
49
  const isReadonly = $derived(mode === 'show');
50
50
  const isCompact = $derived(density === 'compact');
51
51
 
@@ -196,7 +196,7 @@
196
196
  <div class="flex items-center gap-4">
197
197
  <TooltipButton
198
198
  tooltip={i18n.t('common.back')}
199
- onclick={() => guardNavigate(() => adminContext.navigate(`/${resourceName}`))}
199
+ onclick={() => guardNavigate(() => navigation.list(resourceName))}
200
200
  >
201
201
  <ArrowLeft class="h-5 w-5" />
202
202
  </TooltipButton>
@@ -311,7 +311,7 @@
311
311
  type="button"
312
312
  variant="outline"
313
313
  size={isCompact ? 'sm' : 'default'}
314
- onclick={() => guardNavigate(() => adminContext.navigate(`/${resourceName}`))}
314
+ onclick={() => guardNavigate(() => navigation.list(resourceName))}
315
315
  >
316
316
  {i18n.t('common.cancel')}
317
317
  </Button>