includio-cms 0.0.69 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/ROADMAP.md +24 -5
- package/dist/admin/client/account/lang.d.ts +0 -5
- package/dist/admin/client/account/lang.js +2 -12
- package/dist/admin/client/account/preferences-section.svelte +27 -24
- package/dist/admin/client/account/profile-section.svelte +11 -15
- package/dist/admin/client/account/security-section.svelte +0 -11
- package/dist/admin/client/account/sessions-section.svelte +2 -1
- package/dist/admin/client/collection/collection-entries.svelte +168 -43
- package/dist/admin/client/collection/collection-view.svelte.d.ts +5 -0
- package/dist/admin/client/collection/collection-view.svelte.js +22 -5
- package/dist/admin/client/collection/collection.svelte +2 -2
- package/dist/admin/client/collection/data-table.svelte +15 -3
- package/dist/admin/client/collection/data-table.svelte.d.ts +3 -0
- package/dist/admin/client/entry/entry.svelte +11 -7
- package/dist/admin/client/entry/header/status-badge.svelte +6 -3
- package/dist/admin/client/entry/header/version-history-sheet.svelte +6 -3
- package/dist/admin/client/form/form-submissions.svelte +3 -26
- package/dist/admin/components/dashboard/form-submissions-widget.svelte +2 -12
- package/dist/admin/components/dashboard/recent-activity.svelte +2 -12
- package/dist/admin/components/layout/header-actions.svelte +4 -3
- package/dist/admin/components/media/media-library.svelte +17 -6
- package/dist/admin/remote/entry.remote.d.ts +10 -1
- package/dist/admin/remote/entry.remote.js +16 -4
- package/dist/admin/state/interface-language.svelte.d.ts +4 -7
- package/dist/admin/state/interface-language.svelte.js +19 -18
- package/dist/admin/utils/formatDate.d.ts +1 -0
- package/dist/admin/utils/formatDate.js +5 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/core/server/entries/operations/get.d.ts +2 -0
- package/dist/core/server/entries/operations/get.js +6 -0
- package/dist/db-postgres/index.js +26 -1
- package/dist/sveltekit/components/hybrid-context.d.ts +4 -0
- package/dist/sveltekit/components/hybrid-context.js +9 -0
- package/dist/sveltekit/components/hybrid-target.svelte +4 -1
- package/dist/sveltekit/components/image.svelte +5 -2
- package/dist/sveltekit/components/preview.svelte +3 -0
- package/dist/sveltekit/components/video.svelte +4 -1
- package/dist/sveltekit/index.d.ts +1 -0
- package/dist/sveltekit/index.js +1 -0
- package/dist/types/adapters/db.d.ts +3 -1
- package/dist/types/entries.d.ts +10 -2
- package/dist/types/languages.d.ts +2 -1
- package/dist/updates/0.1.0/index.d.ts +2 -0
- package/dist/updates/0.1.0/index.js +25 -0
- package/dist/updates/index.js +2 -1
- package/package.json +1 -1
- package/dist/core/server/utils/sanitizeRichText.d.ts +0 -1
- package/dist/core/server/utils/sanitizeRichText.js +0 -67
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
All notable changes to includio-cms are documented here.
|
|
4
4
|
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
5
|
|
|
6
|
+
## 0.1.0 — 2026-02-17
|
|
7
|
+
|
|
8
|
+
Stabilization — pagination, language switcher, and more
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Server-side pagination for collection entries (limit/offset/orderBy)
|
|
12
|
+
- Persistent page index and active tab across navigation
|
|
13
|
+
- countEntries adapter method for total count queries
|
|
14
|
+
- Automatic fallback to client-side pagination for search and name sorting
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Language switcher now reactive globally — all UI updates instantly without reload
|
|
18
|
+
- Removed hardcoded en/pl — locale list derived from Paraglide config
|
|
19
|
+
- Browser language auto-detected as default interface language
|
|
20
|
+
- Deduplicated formatRelativeDate across dashboard and form components
|
|
21
|
+
- Account profile: fixed name input not responding to typing
|
|
22
|
+
- Account preferences: added aria-pressed to theme/language toggles
|
|
23
|
+
- Removed placeholder 2FA stub from security section
|
|
24
|
+
- Media tag counts now display correct values — TagSidebar and BulkActionBar receive all files
|
|
25
|
+
- Hybrid target: data-hybrid-path no longer rendered on public pages — only in preview iframe
|
|
26
|
+
|
|
27
|
+
### Breaking
|
|
28
|
+
- getRawEntries remote now returns { entries: RawEntry[], total: number } instead of RawEntry[]
|
|
29
|
+
|
|
6
30
|
## 0.0.69 — 2026-02-17
|
|
7
31
|
|
|
8
32
|
DnD array reordering, nav-search batch fetch
|
package/ROADMAP.md
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
## 0.1.0 — Stabilization
|
|
16
16
|
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
17
|
+
- [x] `[fix]` `[P0]` Collection table pagination — server-side, persistent page state, archived tab <!-- files: src/lib/admin/client/collection/collection-entries.svelte, src/lib/admin/client/collection/table-pagination.svelte -->
|
|
18
|
+
- [x] `[fix]` `[P0]` Language switcher — reactive globally without reload, remove hardcoded en/pl <!-- files: src/lib/admin/state/interface-language.svelte.ts, src/lib/admin/components/layout/header-actions.svelte -->
|
|
19
|
+
- [x] `[fix]` `[P0]` User account section — fix name input, aria-pressed on prefs, clean up 2FA stub (email change deferred to 0.1.2, avatar upload skipped) <!-- files: src/lib/admin/client/account/ -->
|
|
20
|
+
- [x] `[fix]` `[P0]` Media tag counts always 0 — pass actual files to TagSidebar <!-- files: src/lib/admin/components/media/media-library.svelte, src/lib/admin/components/media/tag-sidebar.svelte -->
|
|
21
|
+
- [x] `[fix]` `[P0]` Hybrid target — don't render `data-hybrid-path` for non-logged-in users <!-- files: src/lib/sveltekit/components/hybrid-target.svelte -->
|
|
22
22
|
|
|
23
23
|
## 0.1.1 — Input integrity
|
|
24
24
|
|
|
@@ -27,6 +27,25 @@
|
|
|
27
27
|
- [ ] `[feature]` `[P1]` Array field fixed length — fixed item count, no add/remove, reorder only
|
|
28
28
|
- [ ] `[feature]` `[P1]` Field constraint info display — show constraints before validation error (WCAG/ATAG)
|
|
29
29
|
|
|
30
|
+
## 0.1.2 — User management & RBAC
|
|
31
|
+
|
|
32
|
+
### Phase 1 — Core
|
|
33
|
+
|
|
34
|
+
- [ ] `[feature]` `[P0]` RBAC middleware — `requireRole()`, role check w `requireAuth()` <!-- files: src/lib/admin/remote/middleware/auth.ts -->
|
|
35
|
+
- [ ] `[feature]` `[P0]` Admin users page — list, search, pagination via `authClient.admin.listUsers` <!-- files: src/lib/admin/client/users/ -->
|
|
36
|
+
- [ ] `[feature]` `[P0]` Create user — dialog z email/password/name/role via `authClient.admin.createUser` <!-- files: src/lib/admin/client/users/create-user-dialog.svelte -->
|
|
37
|
+
- [ ] `[feature]` `[P0]` Edit user — name, email, role via `adminUpdateUser` + `setRole` <!-- files: src/lib/admin/client/users/edit-user-dialog.svelte -->
|
|
38
|
+
- [ ] `[feature]` `[P0]` Delete user — confirmation dialog via `removeUser` <!-- files: src/lib/admin/client/users/ -->
|
|
39
|
+
- [ ] `[feature]` `[P0]` Route/sidebar gating — ukryj Users nav + chroń `/admin/users` dla non-admin <!-- files: src/lib/admin/components/layout/nav-main.svelte, src/lib/sveltekit/server/handle.ts -->
|
|
40
|
+
- [ ] `[feature]` `[P0]` First user bootstrap — pierwszy utworzony user auto-gets role `admin` <!-- files: src/lib/server/auth.ts -->
|
|
41
|
+
|
|
42
|
+
### Phase 2 — Extended
|
|
43
|
+
|
|
44
|
+
- [ ] `[feature]` `[P1]` Ban/unban UI — reason + expiry, `banUser`/`unbanUser` <!-- files: src/lib/admin/client/users/ban-user-dialog.svelte -->
|
|
45
|
+
- [ ] `[feature]` `[P1]` Admin session mgmt — list/revoke sesji innych userów <!-- files: src/lib/admin/client/users/user-sessions-sheet.svelte -->
|
|
46
|
+
- [ ] `[feature]` `[P1]` Impersonation UI — impersonate/stop bar <!-- files: src/lib/admin/client/users/impersonation-bar.svelte -->
|
|
47
|
+
- [ ] `[feature]` `[P2]` Email invitation system — invite link generation (custom, nie w better-auth) <!-- files: src/lib/core/server/auth/invite.ts -->
|
|
48
|
+
|
|
30
49
|
## 0.2.0 — Plugin system
|
|
31
50
|
|
|
32
51
|
- [ ] `[feature]` `[P0]` Wire plugin hooks into CRUD operations (before/afterCreate, Update, Delete) <!-- files: src/lib/types/plugins.ts, src/lib/core/server/entries/operations/ -->
|
|
@@ -23,9 +23,6 @@ export declare const accountLang: Record<string, {
|
|
|
23
23
|
confirmPassword: string;
|
|
24
24
|
updatePassword: string;
|
|
25
25
|
passwordUpdated: string;
|
|
26
|
-
twoFactor: string;
|
|
27
|
-
twoFactorDesc: string;
|
|
28
|
-
comingSoon: string;
|
|
29
26
|
deleteAccount: string;
|
|
30
27
|
deleteAccountDesc: string;
|
|
31
28
|
deleteAccountButton: string;
|
|
@@ -42,8 +39,6 @@ export declare const accountLang: Record<string, {
|
|
|
42
39
|
themeDark: string;
|
|
43
40
|
themeSystem: string;
|
|
44
41
|
language: string;
|
|
45
|
-
languageEn: string;
|
|
46
|
-
languagePl: string;
|
|
47
42
|
};
|
|
48
43
|
sessions: {
|
|
49
44
|
title: string;
|
|
@@ -24,9 +24,6 @@ export const accountLang = {
|
|
|
24
24
|
confirmPassword: 'Potwierdź hasło',
|
|
25
25
|
updatePassword: 'Zmień hasło',
|
|
26
26
|
passwordUpdated: 'Hasło zostało zmienione',
|
|
27
|
-
twoFactor: 'Uwierzytelnianie dwuskładnikowe',
|
|
28
|
-
twoFactorDesc: 'Dodatkowa warstwa zabezpieczeń dla Twojego konta',
|
|
29
|
-
comingSoon: 'Wkrótce',
|
|
30
27
|
deleteAccount: 'Usuń konto',
|
|
31
28
|
deleteAccountDesc: 'Trwale usuń swoje konto i wszystkie dane',
|
|
32
29
|
deleteAccountButton: 'Usuń konto',
|
|
@@ -42,9 +39,7 @@ export const accountLang = {
|
|
|
42
39
|
themeLight: 'Jasny',
|
|
43
40
|
themeDark: 'Ciemny',
|
|
44
41
|
themeSystem: 'Systemowy',
|
|
45
|
-
language: 'Język'
|
|
46
|
-
languageEn: 'Angielski',
|
|
47
|
-
languagePl: 'Polski'
|
|
42
|
+
language: 'Język'
|
|
48
43
|
},
|
|
49
44
|
sessions: {
|
|
50
45
|
title: 'Aktywne sesje',
|
|
@@ -88,9 +83,6 @@ export const accountLang = {
|
|
|
88
83
|
confirmPassword: 'Confirm password',
|
|
89
84
|
updatePassword: 'Update password',
|
|
90
85
|
passwordUpdated: 'Password updated',
|
|
91
|
-
twoFactor: 'Two-factor authentication',
|
|
92
|
-
twoFactorDesc: 'Add an extra layer of security to your account',
|
|
93
|
-
comingSoon: 'Coming soon',
|
|
94
86
|
deleteAccount: 'Delete account',
|
|
95
87
|
deleteAccountDesc: 'Permanently delete your account and all data',
|
|
96
88
|
deleteAccountButton: 'Delete account',
|
|
@@ -106,9 +98,7 @@ export const accountLang = {
|
|
|
106
98
|
themeLight: 'Light',
|
|
107
99
|
themeDark: 'Dark',
|
|
108
100
|
themeSystem: 'System',
|
|
109
|
-
language: 'Language'
|
|
110
|
-
languageEn: 'English',
|
|
111
|
-
languagePl: 'Polish'
|
|
101
|
+
language: 'Language'
|
|
112
102
|
},
|
|
113
103
|
sessions: {
|
|
114
104
|
title: 'Active sessions',
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import * as Card from '../../../components/ui/card/index.js';
|
|
3
3
|
import { accountLang } from './lang.js';
|
|
4
|
-
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
4
|
+
import { useInterfaceLanguage, locales } from '../../state/interface-language.svelte.js';
|
|
5
5
|
import { userPrefersMode, setMode } from 'mode-watcher';
|
|
6
6
|
import Sun from '@tabler/icons-svelte/icons/sun';
|
|
7
7
|
import Moon from '@tabler/icons-svelte/icons/moon';
|
|
8
8
|
import DeviceDesktop from '@tabler/icons-svelte/icons/device-desktop';
|
|
9
|
+
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
9
10
|
|
|
10
11
|
const interfaceLanguage = useInterfaceLanguage();
|
|
11
12
|
|
|
@@ -15,9 +16,15 @@
|
|
|
15
16
|
setMode(theme);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
function setLanguage(language:
|
|
19
|
+
function setLanguage(language: InterfaceLanguage) {
|
|
19
20
|
interfaceLanguage.current = language;
|
|
20
21
|
}
|
|
22
|
+
|
|
23
|
+
function getLanguageDisplayName(locale: string): string {
|
|
24
|
+
const name = new Intl.DisplayNames([interfaceLanguage.current], { type: 'language' }).of(locale);
|
|
25
|
+
if (!name) return locale.toUpperCase();
|
|
26
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
27
|
+
}
|
|
21
28
|
</script>
|
|
22
29
|
|
|
23
30
|
<div class="space-y-6">
|
|
@@ -26,9 +33,10 @@
|
|
|
26
33
|
<Card.Title>{lang.preferences.theme}</Card.Title>
|
|
27
34
|
</Card.Header>
|
|
28
35
|
<Card.Content>
|
|
29
|
-
<div class="flex gap-2">
|
|
36
|
+
<div class="flex gap-2" role="group" aria-label={lang.preferences.theme}>
|
|
30
37
|
<button
|
|
31
38
|
type="button"
|
|
39
|
+
aria-pressed={userPrefersMode.current === 'light'}
|
|
32
40
|
onclick={() => setTheme('light')}
|
|
33
41
|
class="inline-flex h-10 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none {userPrefersMode.current ===
|
|
34
42
|
'light'
|
|
@@ -40,6 +48,7 @@
|
|
|
40
48
|
</button>
|
|
41
49
|
<button
|
|
42
50
|
type="button"
|
|
51
|
+
aria-pressed={userPrefersMode.current === 'dark'}
|
|
43
52
|
onclick={() => setTheme('dark')}
|
|
44
53
|
class="inline-flex h-10 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none {userPrefersMode.current ===
|
|
45
54
|
'dark'
|
|
@@ -51,6 +60,7 @@
|
|
|
51
60
|
</button>
|
|
52
61
|
<button
|
|
53
62
|
type="button"
|
|
63
|
+
aria-pressed={userPrefersMode.current === 'system'}
|
|
54
64
|
onclick={() => setTheme('system')}
|
|
55
65
|
class="inline-flex h-10 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none {userPrefersMode.current ===
|
|
56
66
|
'system'
|
|
@@ -69,27 +79,20 @@
|
|
|
69
79
|
<Card.Title>{lang.preferences.language}</Card.Title>
|
|
70
80
|
</Card.Header>
|
|
71
81
|
<Card.Content>
|
|
72
|
-
<div class="flex gap-2">
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class="inline-flex h-10 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none {interfaceLanguage.current ===
|
|
87
|
-
'pl'
|
|
88
|
-
? 'border-[#2D4A77] bg-[#2D4A77]/10 text-[#2D4A77]'
|
|
89
|
-
: 'border-input hover:bg-accent'}"
|
|
90
|
-
>
|
|
91
|
-
{lang.preferences.languagePl}
|
|
92
|
-
</button>
|
|
82
|
+
<div class="flex gap-2" role="group" aria-label={lang.preferences.language}>
|
|
83
|
+
{#each locales as locale}
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
aria-pressed={interfaceLanguage.current === locale}
|
|
87
|
+
onclick={() => setLanguage(locale)}
|
|
88
|
+
class="inline-flex h-10 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none {interfaceLanguage.current ===
|
|
89
|
+
locale
|
|
90
|
+
? 'border-[#2D4A77] bg-[#2D4A77]/10 text-[#2D4A77]'
|
|
91
|
+
: 'border-input hover:bg-accent'}"
|
|
92
|
+
>
|
|
93
|
+
{getLanguageDisplayName(locale)}
|
|
94
|
+
</button>
|
|
95
|
+
{/each}
|
|
93
96
|
</div>
|
|
94
97
|
</Card.Content>
|
|
95
98
|
</Card.Root>
|
|
@@ -9,21 +9,27 @@
|
|
|
9
9
|
import { profileSchema } from './schema.js';
|
|
10
10
|
import { accountLang } from './lang.js';
|
|
11
11
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
12
|
+
import { toLocaleCode } from '../../utils/formatDate.js';
|
|
12
13
|
import { authClient } from '../../auth-client.js';
|
|
13
14
|
import { toast } from 'svelte-sonner';
|
|
15
|
+
import { untrack } from 'svelte';
|
|
14
16
|
|
|
15
17
|
let session = authClient.useSession();
|
|
16
18
|
const interfaceLanguage = useInterfaceLanguage();
|
|
17
19
|
|
|
18
20
|
$effect(() => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const sessionName = $session.data?.user.name;
|
|
22
|
+
if (sessionName) {
|
|
23
|
+
untrack(() => {
|
|
24
|
+
$formData.name = sessionName;
|
|
25
|
+
});
|
|
21
26
|
}
|
|
22
27
|
});
|
|
23
28
|
|
|
24
29
|
const form = superForm(defaults(zod4(profileSchema)), {
|
|
25
30
|
validators: zod4Client(profileSchema),
|
|
26
31
|
SPA: true,
|
|
32
|
+
resetForm: false,
|
|
27
33
|
onUpdate: async ({ form }) => {
|
|
28
34
|
if (form.valid) {
|
|
29
35
|
await authClient.updateUser({
|
|
@@ -48,7 +54,7 @@
|
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
function formatDate(date: Date | string): string {
|
|
51
|
-
return new Date(date).toLocaleDateString(interfaceLanguage.current
|
|
57
|
+
return new Date(date).toLocaleDateString(toLocaleCode(interfaceLanguage.current), {
|
|
52
58
|
year: 'numeric',
|
|
53
59
|
month: 'long'
|
|
54
60
|
});
|
|
@@ -79,12 +85,7 @@
|
|
|
79
85
|
<Form.Control>
|
|
80
86
|
{#snippet children({ props })}
|
|
81
87
|
<Form.Label>{lang.profile.name}</Form.Label>
|
|
82
|
-
<Input
|
|
83
|
-
{...props}
|
|
84
|
-
bind:value={$formData.name}
|
|
85
|
-
type="text"
|
|
86
|
-
class="mt-1"
|
|
87
|
-
/>
|
|
88
|
+
<Input {...props} bind:value={$formData.name} type="text" class="mt-1" />
|
|
88
89
|
{/snippet}
|
|
89
90
|
</Form.Control>
|
|
90
91
|
<Form.FieldErrors class="text-sm text-red-500" />
|
|
@@ -92,12 +93,7 @@
|
|
|
92
93
|
|
|
93
94
|
<div class="space-y-2">
|
|
94
95
|
<label class="text-sm font-medium">{lang.profile.email}</label>
|
|
95
|
-
<Input
|
|
96
|
-
type="email"
|
|
97
|
-
value={$session.data.user.email}
|
|
98
|
-
disabled
|
|
99
|
-
class="mt-1"
|
|
100
|
-
/>
|
|
96
|
+
<Input type="email" value={$session.data.user.email} disabled class="mt-1" />
|
|
101
97
|
</div>
|
|
102
98
|
|
|
103
99
|
<div class="flex items-center gap-4">
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import Button from '../../../components/ui/button/button.svelte';
|
|
5
5
|
import * as Card from '../../../components/ui/card/index.js';
|
|
6
6
|
import * as AlertDialog from '../../../components/ui/alert-dialog/index.js';
|
|
7
|
-
import { Badge } from '../../../components/ui/badge/index.js';
|
|
8
7
|
import { defaults, superForm } from 'sveltekit-superforms';
|
|
9
8
|
import { zod4, zod4Client } from 'sveltekit-superforms/adapters';
|
|
10
9
|
import { passwordSchema, deleteAccountSchema } from './schema.js';
|
|
@@ -124,16 +123,6 @@
|
|
|
124
123
|
</Card.Content>
|
|
125
124
|
</Card.Root>
|
|
126
125
|
|
|
127
|
-
<Card.Root>
|
|
128
|
-
<Card.Header>
|
|
129
|
-
<Card.Title>{lang.security.twoFactor}</Card.Title>
|
|
130
|
-
<Card.Description>{lang.security.twoFactorDesc}</Card.Description>
|
|
131
|
-
</Card.Header>
|
|
132
|
-
<Card.Content>
|
|
133
|
-
<Badge variant="outline">{lang.security.comingSoon}</Badge>
|
|
134
|
-
</Card.Content>
|
|
135
|
-
</Card.Root>
|
|
136
|
-
|
|
137
126
|
<Card.Root class="border-destructive/50">
|
|
138
127
|
<Card.Header>
|
|
139
128
|
<Card.Title class="text-destructive">{lang.security.deleteAccount}</Card.Title>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Badge } from '../../../components/ui/badge/index.js';
|
|
4
4
|
import { accountLang } from './lang.js';
|
|
5
5
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
6
|
+
import { toLocaleCode } from '../../utils/formatDate.js';
|
|
6
7
|
import { authClient } from '../../auth-client.js';
|
|
7
8
|
import { toast } from 'svelte-sonner';
|
|
8
9
|
import DeviceDesktop from '@tabler/icons-svelte/icons/device-desktop';
|
|
@@ -86,7 +87,7 @@
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
function formatDate(date: Date | string): string {
|
|
89
|
-
return new Date(date).toLocaleString(interfaceLanguage.current
|
|
90
|
+
return new Date(date).toLocaleString(toLocaleCode(interfaceLanguage.current), {
|
|
90
91
|
year: 'numeric',
|
|
91
92
|
month: 'short',
|
|
92
93
|
day: 'numeric',
|
|
@@ -117,6 +117,46 @@
|
|
|
117
117
|
let deleteDialogOpen = $state(false);
|
|
118
118
|
let pendingDeleteId = $state<string | null>(null);
|
|
119
119
|
|
|
120
|
+
// Determine if we need fetch-all fallback (search active or sorting by name)
|
|
121
|
+
const isSortingByName = $derived(
|
|
122
|
+
viewState.sorting.length > 0 && viewState.sorting[0].id === 'name'
|
|
123
|
+
);
|
|
124
|
+
const useServerPagination = $derived(!searchQuery && !isSortingByName);
|
|
125
|
+
|
|
126
|
+
// Build orderBy from sorting state for server-side mode
|
|
127
|
+
const serverOrderBy = $derived.by(() => {
|
|
128
|
+
if (!viewState.sorting.length) return undefined;
|
|
129
|
+
const { id, desc } = viewState.sorting[0];
|
|
130
|
+
if (id === 'createdAt' || id === 'updatedAt') {
|
|
131
|
+
return { column: id as 'createdAt' | 'updatedAt', direction: (desc ? 'desc' : 'asc') as 'asc' | 'desc' };
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Server-side query params
|
|
137
|
+
const activeQueryParams = $derived(
|
|
138
|
+
useServerPagination
|
|
139
|
+
? {
|
|
140
|
+
slug: collection.slug,
|
|
141
|
+
limit: viewState.pageSize,
|
|
142
|
+
offset: viewState.pageIndex * viewState.pageSize,
|
|
143
|
+
orderBy: serverOrderBy
|
|
144
|
+
}
|
|
145
|
+
: { slug: collection.slug }
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const archivedQueryParams = $derived(
|
|
149
|
+
useServerPagination
|
|
150
|
+
? {
|
|
151
|
+
slug: collection.slug,
|
|
152
|
+
onlyArchived: true as const,
|
|
153
|
+
limit: viewState.pageSize,
|
|
154
|
+
offset: viewState.pageIndex * viewState.pageSize,
|
|
155
|
+
orderBy: serverOrderBy
|
|
156
|
+
}
|
|
157
|
+
: { slug: collection.slug, onlyArchived: true as const }
|
|
158
|
+
);
|
|
159
|
+
|
|
120
160
|
const columns: ColumnDef<CollectionDataTableRow>[] = $derived([
|
|
121
161
|
{
|
|
122
162
|
id: 'select',
|
|
@@ -232,13 +272,15 @@
|
|
|
232
272
|
}
|
|
233
273
|
]);
|
|
234
274
|
|
|
235
|
-
|
|
275
|
+
function refreshQueries() {
|
|
276
|
+
remotes.getRawEntries(activeQueryParams).refresh();
|
|
277
|
+
remotes.getRawEntries(archivedQueryParams).refresh();
|
|
278
|
+
}
|
|
236
279
|
|
|
237
280
|
async function handleRestore(id: string) {
|
|
238
281
|
await remotes.unarchiveEntryCommand(id);
|
|
239
282
|
toast.success(lang[interfaceLanguage.current].entryRestored);
|
|
240
|
-
|
|
241
|
-
remotes.getRawEntries({ slug: collection.slug }).refresh();
|
|
283
|
+
refreshQueries();
|
|
242
284
|
}
|
|
243
285
|
|
|
244
286
|
function handleDelete(id: string) {
|
|
@@ -250,7 +292,7 @@
|
|
|
250
292
|
if (!pendingDeleteId) return;
|
|
251
293
|
await remotes.deleteEntryCommand(pendingDeleteId);
|
|
252
294
|
toast.success(lang[interfaceLanguage.current].entryDeleted);
|
|
253
|
-
|
|
295
|
+
refreshQueries();
|
|
254
296
|
deleteDialogOpen = false;
|
|
255
297
|
pendingDeleteId = null;
|
|
256
298
|
}
|
|
@@ -319,12 +361,18 @@
|
|
|
319
361
|
}
|
|
320
362
|
toast.success(lang[interfaceLanguage.current].entriesArchived);
|
|
321
363
|
rowSelection = {};
|
|
322
|
-
|
|
323
|
-
remotes.getRawEntries({ slug: collection.slug, onlyArchived: true }).refresh();
|
|
364
|
+
refreshQueries();
|
|
324
365
|
}
|
|
366
|
+
|
|
325
367
|
</script>
|
|
326
368
|
|
|
327
|
-
<Tabs.Root
|
|
369
|
+
<Tabs.Root
|
|
370
|
+
value={viewState.activeTab}
|
|
371
|
+
onValueChange={(v) => {
|
|
372
|
+
if (v === 'active' || v === 'archived') viewState.activeTab = v;
|
|
373
|
+
}}
|
|
374
|
+
class="w-full"
|
|
375
|
+
>
|
|
328
376
|
<div class="flex items-center justify-between border-b border-slate-200/50 px-4 py-3 dark:border-white/10">
|
|
329
377
|
<Tabs.List class="h-auto gap-1 bg-transparent p-0">
|
|
330
378
|
<Tabs.Trigger value="active" class="rounded-lg px-3 py-1.5 text-sm font-medium data-[state=active]:bg-slate-100 data-[state=active]:text-slate-900 dark:data-[state=active]:bg-slate-800 dark:data-[state=active]:text-white">{lang[interfaceLanguage.current].active}</Tabs.Trigger>
|
|
@@ -336,57 +384,94 @@
|
|
|
336
384
|
</div>
|
|
337
385
|
|
|
338
386
|
<Tabs.Content value="active" class="mt-0">
|
|
339
|
-
{#await remotes.getRawEntries(
|
|
340
|
-
{@const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
387
|
+
{#await remotes.getRawEntries(activeQueryParams) then result}
|
|
388
|
+
{@const allRows = result.entries.map(mapEntryToRow)}
|
|
389
|
+
{@const items = useServerPagination
|
|
390
|
+
? allRows
|
|
391
|
+
: (() => {
|
|
392
|
+
let filtered = searchQuery
|
|
393
|
+
? allRows.filter((item) => item.searchText.includes(searchQuery.toLowerCase()))
|
|
394
|
+
: allRows;
|
|
395
|
+
return sortItems(filtered, viewState.sorting);
|
|
396
|
+
})()}
|
|
397
|
+
{@const totalItems = useServerPagination ? result.total : items.length}
|
|
347
398
|
{@const pageCount = Math.ceil(totalItems / viewState.pageSize)}
|
|
348
399
|
|
|
349
400
|
<TableToolbar
|
|
350
401
|
{searchQuery}
|
|
351
|
-
onSearchChange={(q) =>
|
|
402
|
+
onSearchChange={(q) => {
|
|
403
|
+
searchQuery = q;
|
|
404
|
+
viewState.pageIndex = 0;
|
|
405
|
+
}}
|
|
352
406
|
viewMode={viewState.viewMode}
|
|
353
407
|
onViewModeChange={(m) => (viewState.viewMode = m)}
|
|
354
408
|
dateFormat={viewState.dateFormat}
|
|
355
409
|
onDateFormatChange={(f) => (viewState.dateFormat = f)}
|
|
356
410
|
{selectedCount}
|
|
357
|
-
onBulkArchive={() => handleBulkArchive(
|
|
411
|
+
onBulkArchive={() => handleBulkArchive(items)}
|
|
358
412
|
/>
|
|
359
413
|
|
|
360
414
|
{#if viewState.viewMode === 'list'}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
415
|
+
{#if useServerPagination}
|
|
416
|
+
<DataTable
|
|
417
|
+
data={items}
|
|
418
|
+
{columns}
|
|
419
|
+
enableSorting
|
|
420
|
+
enableFiltering
|
|
421
|
+
enableSelection
|
|
422
|
+
enablePagination
|
|
423
|
+
manualPagination={true}
|
|
424
|
+
pageCount={pageCount}
|
|
425
|
+
rowCount={totalItems}
|
|
426
|
+
sorting={viewState.sorting}
|
|
427
|
+
onSortingChange={(s) => (viewState.sorting = s)}
|
|
428
|
+
{rowSelection}
|
|
429
|
+
onRowSelectionChange={(s) => (rowSelection = s)}
|
|
430
|
+
pagination={{ pageIndex: viewState.pageIndex, pageSize: viewState.pageSize }}
|
|
431
|
+
onPaginationChange={(p) => {
|
|
432
|
+
viewState.pageIndex = p.pageIndex;
|
|
433
|
+
}}
|
|
434
|
+
tableRef={(t) => (tableInstance = t)}
|
|
435
|
+
/>
|
|
436
|
+
{:else}
|
|
437
|
+
<DataTable
|
|
438
|
+
data={items}
|
|
439
|
+
{columns}
|
|
440
|
+
enableSorting
|
|
441
|
+
enableFiltering
|
|
442
|
+
enableSelection
|
|
443
|
+
enablePagination
|
|
444
|
+
sorting={viewState.sorting}
|
|
445
|
+
onSortingChange={(s) => (viewState.sorting = s)}
|
|
446
|
+
{rowSelection}
|
|
447
|
+
onRowSelectionChange={(s) => (rowSelection = s)}
|
|
448
|
+
pagination={{ pageIndex: viewState.pageIndex, pageSize: viewState.pageSize }}
|
|
449
|
+
onPaginationChange={(p) => {
|
|
450
|
+
viewState.pageIndex = p.pageIndex;
|
|
451
|
+
}}
|
|
452
|
+
tableRef={(t) => (tableInstance = t)}
|
|
453
|
+
/>
|
|
454
|
+
{/if}
|
|
375
455
|
|
|
376
456
|
<TablePagination
|
|
377
|
-
pageIndex={
|
|
457
|
+
pageIndex={viewState.pageIndex}
|
|
378
458
|
pageSize={viewState.pageSize}
|
|
379
459
|
{pageCount}
|
|
380
460
|
{totalItems}
|
|
381
|
-
onPageChange={(p) =>
|
|
461
|
+
onPageChange={(p) => (viewState.pageIndex = p)}
|
|
382
462
|
onPageSizeChange={(s) => {
|
|
383
463
|
viewState.pageSize = s;
|
|
384
|
-
tableInstance?.setPageSize(s);
|
|
385
464
|
}}
|
|
386
465
|
/>
|
|
387
466
|
{:else}
|
|
467
|
+
{@const displayItems = useServerPagination
|
|
468
|
+
? items
|
|
469
|
+
: items.slice(
|
|
470
|
+
viewState.pageIndex * viewState.pageSize,
|
|
471
|
+
(viewState.pageIndex + 1) * viewState.pageSize
|
|
472
|
+
)}
|
|
388
473
|
<GridView
|
|
389
|
-
items={
|
|
474
|
+
items={displayItems.map((item) => ({
|
|
390
475
|
id: item.id,
|
|
391
476
|
name: item.name,
|
|
392
477
|
status: item.status,
|
|
@@ -396,12 +481,11 @@
|
|
|
396
481
|
}))}
|
|
397
482
|
dateFormat={viewState.dateFormat}
|
|
398
483
|
enableSelection
|
|
399
|
-
selectedIds={new Set(selectedIndices.map((idx) =>
|
|
484
|
+
selectedIds={new Set(selectedIndices.map((idx) => items[idx]?.id).filter(Boolean))}
|
|
400
485
|
onSelectionChange={(ids) => {
|
|
401
|
-
const sorted = sortItems(filteredItems, viewState.sorting);
|
|
402
486
|
const newSelection: RowSelectionState = {};
|
|
403
487
|
for (const id of ids) {
|
|
404
|
-
const idx =
|
|
488
|
+
const idx = items.findIndex((i) => i.id === id);
|
|
405
489
|
if (idx !== -1) {
|
|
406
490
|
newSelection[idx] = true;
|
|
407
491
|
}
|
|
@@ -409,20 +493,61 @@
|
|
|
409
493
|
rowSelection = newSelection;
|
|
410
494
|
}}
|
|
411
495
|
/>
|
|
496
|
+
|
|
497
|
+
<TablePagination
|
|
498
|
+
pageIndex={viewState.pageIndex}
|
|
499
|
+
pageSize={viewState.pageSize}
|
|
500
|
+
{pageCount}
|
|
501
|
+
{totalItems}
|
|
502
|
+
onPageChange={(p) => (viewState.pageIndex = p)}
|
|
503
|
+
onPageSizeChange={(s) => {
|
|
504
|
+
viewState.pageSize = s;
|
|
505
|
+
}}
|
|
506
|
+
/>
|
|
412
507
|
{/if}
|
|
413
508
|
{/await}
|
|
414
509
|
</Tabs.Content>
|
|
415
510
|
|
|
416
511
|
<Tabs.Content value="archived" class="mt-0">
|
|
417
|
-
{#await remotes.getRawEntries(
|
|
418
|
-
{@const items = entries.map((entry) => ({
|
|
512
|
+
{#await remotes.getRawEntries(archivedQueryParams) then result}
|
|
513
|
+
{@const items = result.entries.map((entry) => ({
|
|
419
514
|
...mapEntryToRow(entry),
|
|
420
515
|
onRestore: () => handleRestore(entry.id),
|
|
421
516
|
onDelete: () => handleDelete(entry.id)
|
|
422
517
|
}))}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
518
|
+
{@const totalItems = result.total}
|
|
519
|
+
{@const pageCount = Math.ceil(totalItems / viewState.pageSize)}
|
|
520
|
+
|
|
521
|
+
{#if useServerPagination}
|
|
522
|
+
<div class="p-4">
|
|
523
|
+
<DataTable
|
|
524
|
+
data={items}
|
|
525
|
+
columns={archivedColumns}
|
|
526
|
+
manualPagination={true}
|
|
527
|
+
pageCount={pageCount}
|
|
528
|
+
rowCount={totalItems}
|
|
529
|
+
enablePagination
|
|
530
|
+
pagination={{ pageIndex: viewState.pageIndex, pageSize: viewState.pageSize }}
|
|
531
|
+
onPaginationChange={(p) => {
|
|
532
|
+
viewState.pageIndex = p.pageIndex;
|
|
533
|
+
}}
|
|
534
|
+
/>
|
|
535
|
+
</div>
|
|
536
|
+
<TablePagination
|
|
537
|
+
pageIndex={viewState.pageIndex}
|
|
538
|
+
pageSize={viewState.pageSize}
|
|
539
|
+
{pageCount}
|
|
540
|
+
{totalItems}
|
|
541
|
+
onPageChange={(p) => (viewState.pageIndex = p)}
|
|
542
|
+
onPageSizeChange={(s) => {
|
|
543
|
+
viewState.pageSize = s;
|
|
544
|
+
}}
|
|
545
|
+
/>
|
|
546
|
+
{:else}
|
|
547
|
+
<div class="p-4">
|
|
548
|
+
<DataTable data={items} columns={archivedColumns} />
|
|
549
|
+
</div>
|
|
550
|
+
{/if}
|
|
426
551
|
{/await}
|
|
427
552
|
</Tabs.Content>
|
|
428
553
|
</Tabs.Root>
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import type { SortingState } from '@tanstack/table-core';
|
|
2
2
|
export type ViewMode = 'list' | 'grid';
|
|
3
3
|
export type DateFormat = 'relative' | 'absolute';
|
|
4
|
+
export type ActiveTab = 'active' | 'archived';
|
|
4
5
|
export interface CollectionViewState {
|
|
5
6
|
viewMode: ViewMode;
|
|
6
7
|
dateFormat: DateFormat;
|
|
7
8
|
pageSize: number;
|
|
8
9
|
sorting: SortingState;
|
|
10
|
+
pageIndex: number;
|
|
11
|
+
activeTab: ActiveTab;
|
|
9
12
|
}
|
|
10
13
|
export declare function createCollectionViewState(collectionSlug: string): {
|
|
11
14
|
viewMode: ViewMode;
|
|
12
15
|
dateFormat: DateFormat;
|
|
13
16
|
pageSize: number;
|
|
14
17
|
sorting: SortingState;
|
|
18
|
+
pageIndex: number;
|
|
19
|
+
activeTab: ActiveTab;
|
|
15
20
|
};
|