includio-cms 0.0.56 → 0.0.58

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 (124) hide show
  1. package/dist/admin/client/admin/admin-after-login-layout-content.svelte +1 -1
  2. package/dist/admin/client/admin/dashboard-page.svelte +10 -47
  3. package/dist/admin/client/collection/archived-actions.svelte +14 -2
  4. package/dist/admin/client/collection/collection-entries.svelte +289 -56
  5. package/dist/admin/client/collection/collection-entries.svelte.d.ts +2 -0
  6. package/dist/admin/client/collection/collection-view.svelte.d.ts +15 -0
  7. package/dist/admin/client/collection/collection-view.svelte.js +66 -0
  8. package/dist/admin/client/collection/collection.svelte +14 -10
  9. package/dist/admin/client/collection/data-table.svelte +76 -5
  10. package/dist/admin/client/collection/data-table.svelte.d.ts +13 -1
  11. package/dist/admin/client/collection/date-cell.svelte +25 -0
  12. package/dist/admin/client/collection/date-cell.svelte.d.ts +7 -0
  13. package/dist/admin/client/collection/grid-view.svelte +100 -0
  14. package/dist/admin/client/collection/grid-view.svelte.d.ts +20 -0
  15. package/dist/admin/client/collection/selection-cell.svelte +20 -0
  16. package/dist/admin/client/collection/selection-cell.svelte.d.ts +9 -0
  17. package/dist/admin/client/collection/sortable-header.svelte +42 -0
  18. package/dist/admin/client/collection/sortable-header.svelte.d.ts +29 -0
  19. package/dist/admin/client/collection/status-badge.svelte +40 -0
  20. package/dist/admin/client/collection/status-badge.svelte.d.ts +7 -0
  21. package/dist/admin/client/collection/table-pagination.svelte +136 -0
  22. package/dist/admin/client/collection/table-pagination.svelte.d.ts +11 -0
  23. package/dist/admin/client/collection/table-toolbar.svelte +156 -0
  24. package/dist/admin/client/collection/table-toolbar.svelte.d.ts +16 -0
  25. package/dist/admin/client/entry/entry-header.svelte +17 -40
  26. package/dist/admin/client/entry/entry-header.svelte.d.ts +3 -1
  27. package/dist/admin/client/entry/entry-page-skeleton.svelte +36 -0
  28. package/dist/admin/client/entry/entry-page-skeleton.svelte.d.ts +18 -0
  29. package/dist/admin/client/entry/entry-page.svelte +4 -1
  30. package/dist/admin/client/entry/entry.svelte +99 -70
  31. package/dist/admin/client/entry/header/save-indicator.svelte +58 -0
  32. package/dist/admin/client/entry/header/save-indicator.svelte.d.ts +7 -0
  33. package/dist/admin/client/entry/header/schedule-popover.svelte +127 -0
  34. package/dist/admin/client/entry/header/schedule-popover.svelte.d.ts +6 -0
  35. package/dist/admin/client/entry/header/status-badge.svelte +150 -0
  36. package/dist/admin/client/entry/header/status-badge.svelte.d.ts +8 -0
  37. package/dist/admin/client/entry/header/version-history-sheet.svelte +225 -0
  38. package/dist/admin/client/entry/header/version-history-sheet.svelte.d.ts +8 -0
  39. package/dist/admin/client/entry/hybrid/hybrid-context.svelte.js +7 -1
  40. package/dist/admin/client/form/form-config-wrapper.svelte +32 -0
  41. package/dist/admin/client/form/form-config-wrapper.svelte.d.ts +7 -0
  42. package/dist/admin/client/form/form-page.svelte +8 -24
  43. package/dist/admin/client/form/form-submission/form-submission-page.svelte +80 -13
  44. package/dist/admin/client/form/form-submission/form-submission-page.svelte.d.ts +6 -1
  45. package/dist/admin/client/form/form-submission/form-submission.svelte +224 -6
  46. package/dist/admin/client/form/form-submission/submission-field.svelte +104 -0
  47. package/dist/admin/client/form/form-submission/submission-field.svelte.d.ts +8 -0
  48. package/dist/admin/client/form/form-submissions.svelte +257 -21
  49. package/dist/admin/client/form/submission-link.svelte +3 -2
  50. package/dist/admin/client/form/submission-link.svelte.d.ts +1 -0
  51. package/dist/admin/client/form/submission-status-badge.svelte +27 -0
  52. package/dist/admin/client/form/submission-status-badge.svelte.d.ts +6 -0
  53. package/dist/admin/components/dashboard/accessibility-hub.svelte +146 -0
  54. package/dist/admin/components/dashboard/accessibility-hub.svelte.d.ts +18 -0
  55. package/dist/admin/components/dashboard/form-submissions-widget.svelte +152 -0
  56. package/dist/admin/components/dashboard/form-submissions-widget.svelte.d.ts +18 -0
  57. package/dist/admin/components/dashboard/index.d.ts +4 -0
  58. package/dist/admin/components/dashboard/index.js +4 -0
  59. package/dist/admin/components/dashboard/recent-activity.svelte +112 -0
  60. package/dist/admin/components/dashboard/recent-activity.svelte.d.ts +18 -0
  61. package/dist/admin/components/dashboard/stat-card.svelte +43 -0
  62. package/dist/admin/components/dashboard/stat-card.svelte.d.ts +14 -0
  63. package/dist/admin/components/fields/array-field.svelte +32 -7
  64. package/dist/admin/components/fields/array-field.svelte.d.ts +1 -0
  65. package/dist/admin/components/fields/block-picker-modal.svelte +97 -0
  66. package/dist/admin/components/fields/block-picker-modal.svelte.d.ts +9 -0
  67. package/dist/admin/components/fields/field-renderer.svelte +4 -3
  68. package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -0
  69. package/dist/admin/components/fields/fields-form.svelte +50 -28
  70. package/dist/admin/components/fields/image-field.svelte +7 -3
  71. package/dist/admin/components/fields/object-field.svelte +18 -3
  72. package/dist/admin/components/fields/object-field.svelte.d.ts +1 -0
  73. package/dist/admin/components/fields/relation-field.svelte +59 -55
  74. package/dist/admin/components/fields/relation-field.svelte.d.ts +6 -6
  75. package/dist/admin/components/media/file/file-preview.svelte +44 -23
  76. package/dist/admin/components/media/media-selector.svelte +47 -31
  77. package/dist/admin/remote/entry.remote.d.ts +9 -1
  78. package/dist/admin/remote/entry.remote.js +51 -8
  79. package/dist/admin/remote/form.remote.d.ts +19 -0
  80. package/dist/admin/remote/form.remote.js +45 -1
  81. package/dist/admin/utils/entryThumbnail.d.ts +2 -0
  82. package/dist/admin/utils/entryThumbnail.js +79 -0
  83. package/dist/admin/utils/formatDate.d.ts +3 -0
  84. package/dist/admin/utils/formatDate.js +94 -0
  85. package/dist/components/ui/alert-dialog/alert-dialog-action.svelte +23 -0
  86. package/dist/components/ui/alert-dialog/alert-dialog-action.svelte.d.ts +9 -0
  87. package/dist/components/ui/alert-dialog/alert-dialog-cancel.svelte +23 -0
  88. package/dist/components/ui/alert-dialog/alert-dialog-cancel.svelte.d.ts +9 -0
  89. package/dist/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
  90. package/dist/components/ui/alert-dialog/alert-dialog-content.svelte.d.ts +10 -0
  91. package/dist/components/ui/alert-dialog/alert-dialog-description.svelte +22 -0
  92. package/dist/components/ui/alert-dialog/alert-dialog-description.svelte.d.ts +9 -0
  93. package/dist/components/ui/alert-dialog/alert-dialog-footer.svelte +19 -0
  94. package/dist/components/ui/alert-dialog/alert-dialog-footer.svelte.d.ts +5 -0
  95. package/dist/components/ui/alert-dialog/alert-dialog-title.svelte +22 -0
  96. package/dist/components/ui/alert-dialog/alert-dialog-title.svelte.d.ts +9 -0
  97. package/dist/components/ui/alert-dialog/index.d.ts +11 -0
  98. package/dist/components/ui/alert-dialog/index.js +13 -0
  99. package/dist/core/server/entries/operations/get.js +3 -1
  100. package/dist/core/server/entries/operations/update.d.ts +2 -1
  101. package/dist/core/server/entries/operations/update.js +17 -0
  102. package/dist/core/server/fields/resolveImageFields.js +2 -1
  103. package/dist/core/server/fields/resolveUrlFields.js +2 -1
  104. package/dist/core/server/forms/submissions/operations/create.d.ts +7 -1
  105. package/dist/core/server/forms/submissions/operations/create.js +8 -2
  106. package/dist/core/server/forms/submissions/operations/delete.d.ts +2 -0
  107. package/dist/core/server/forms/submissions/operations/delete.js +9 -0
  108. package/dist/core/server/forms/submissions/operations/export.d.ts +1 -0
  109. package/dist/core/server/forms/submissions/operations/export.js +40 -0
  110. package/dist/core/server/forms/submissions/operations/update.d.ts +2 -0
  111. package/dist/core/server/forms/submissions/operations/update.js +4 -0
  112. package/dist/core/server/generator/generator.js +7 -2
  113. package/dist/core/server/media/operations/uploadFile.js +5 -1
  114. package/dist/db-postgres/index.js +15 -2
  115. package/dist/db-postgres/schema/formSubmission.d.ts +51 -0
  116. package/dist/db-postgres/schema/formSubmission.js +5 -2
  117. package/dist/sveltekit/components/image.svelte +16 -12
  118. package/dist/types/adapters/db.d.ts +11 -1
  119. package/dist/types/entries.d.ts +1 -0
  120. package/dist/types/fields.d.ts +2 -0
  121. package/dist/types/forms.d.ts +3 -0
  122. package/package.json +1 -1
  123. package/dist/admin/client/form/data-table.svelte +0 -56
  124. package/dist/admin/client/form/data-table.svelte.d.ts +0 -29
@@ -21,7 +21,7 @@
21
21
 
22
22
  <Sidebar.Inset class="h-[calc(100vh-1rem)] overflow-hidden">
23
23
  <SiteHeader />
24
- <div class="grow overflow-auto">
24
+ <div class="flex grow flex-col overflow-auto">
25
25
  {@render children()}
26
26
  </div>
27
27
  </Sidebar.Inset>
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
- import AccessibilityOverview from '../../components/accessibility/accessibility-overview.svelte';
2
+ import AccessibilityHub from '../../components/dashboard/accessibility-hub.svelte';
3
+ import FormSubmissionsWidget from '../../components/dashboard/form-submissions-widget.svelte';
4
+ import RecentActivity from '../../components/dashboard/recent-activity.svelte';
3
5
  import { sidebarLang } from '../../components/layout/lang.js';
4
6
  import { getBreadcrumbs } from '../../state/breadcrumbs.svelte.js';
5
7
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
@@ -7,7 +9,6 @@
7
9
  import { Button } from '../../../components/ui/button/index.js';
8
10
 
9
11
  import type { InterfaceLanguage } from '../../../types/languages.js';
10
- import InfoCircleIcon from '@tabler/icons-svelte/icons/info-circle';
11
12
  import AlertTriangleIcon from '@tabler/icons-svelte/icons/alert-triangle';
12
13
  import TrashIcon from '@tabler/icons-svelte/icons/trash';
13
14
 
@@ -61,17 +62,6 @@
61
62
  ];
62
63
  });
63
64
 
64
- const alertLang: Record<InterfaceLanguage, { title: string; description: string }> = {
65
- pl: {
66
- title: 'Kokpit jest w trakcie budowy',
67
- description: 'Oczekuj na więcej funkcji w nadchodzących aktualizacjach.'
68
- },
69
- en: {
70
- title: 'The dashboard is under construction',
71
- description: 'Expect more features in upcoming updates.'
72
- }
73
- };
74
-
75
65
  const orphanedLang: Record<
76
66
  InterfaceLanguage,
77
67
  { title: string; description: string; deleteBtn: string; confirmDelete: string }
@@ -93,27 +83,12 @@
93
83
  };
94
84
  </script>
95
85
 
96
- <div class="relative min-h-[calc(100vh-var(--header-height))] p-4 md:p-6">
97
- <!-- Dot pattern background -->
98
- <div
99
- class="pointer-events-none absolute inset-0 opacity-30 dark:opacity-10"
100
- style="background-image: radial-gradient(circle, oklch(0.5 0.02 285) 1px, transparent 1px); background-size: 24px 24px;"
101
- ></div>
102
-
103
- <!-- Floating orbs -->
104
- <div
105
- class="animate-float-slow pointer-events-none absolute -top-20 right-10 h-64 w-64 rounded-full bg-primary/10 blur-3xl"
106
- ></div>
107
- <div
108
- class="animate-float-slow pointer-events-none absolute bottom-20 left-10 h-48 w-48 rounded-full bg-primary/15 blur-2xl"
109
- style="animation-delay: -5s;"
110
- ></div>
111
-
112
- <div class="relative z-10">
86
+ <div class="min-h-[calc(100vh-var(--header-height))] bg-slate-50 p-4 dark:bg-slate-950 md:p-6">
87
+ <div class="mx-auto max-w-7xl space-y-6">
113
88
  {#if !loadingOrphaned && orphanedEntries.length > 0}
114
89
  <Alert.Root
115
90
  variant="destructive"
116
- class="glass-panel mb-4 rounded-2xl border-orange-300/50 bg-orange-50/80 backdrop-blur-xl dark:border-orange-500/30 dark:bg-orange-950/60 md:mb-6"
91
+ class="rounded-2xl border-orange-300/50 bg-orange-50 dark:border-orange-500/30 dark:bg-orange-950/60"
117
92
  >
118
93
  <AlertTriangleIcon class="text-orange-600 dark:text-orange-400" />
119
94
  <Alert.Title class="text-orange-800 dark:text-orange-200">
@@ -142,23 +117,11 @@
142
117
  </Alert.Root>
143
118
  {/if}
144
119
 
145
- <Alert.Root
146
- class="glass-panel mb-4 rounded-2xl border-slate-200/50 bg-white/80 backdrop-blur-xl dark:border-white/10 dark:bg-slate-900/60 md:mb-6"
147
- >
148
- <InfoCircleIcon />
149
- <Alert.Title>{alertLang[interfaceLanguage.current].title}</Alert.Title>
150
- <Alert.Description>{alertLang[interfaceLanguage.current].description}</Alert.Description>
151
- </Alert.Root>
120
+ <AccessibilityHub />
152
121
 
153
- <div class="flex flex-wrap gap-4">
154
- <AccessibilityOverview />
155
- <a
156
- href="/admin/demo/hybrid-editor"
157
- class="glass-card rounded-2xl p-4 transition-all duration-300 hover:-translate-y-1 hover:shadow-xl"
158
- >
159
- <h3 class="font-medium">Hybrid Editor Demo</h3>
160
- <p class="text-muted-foreground text-sm">Try the visual editing experience</p>
161
- </a>
122
+ <div class="grid gap-6 lg:grid-cols-2">
123
+ <FormSubmissionsWidget />
124
+ <RecentActivity />
162
125
  </div>
163
126
  </div>
164
127
  </div>
@@ -9,9 +9,21 @@
9
9
  };
10
10
 
11
11
  let { restoreLabel, deleteLabel, onRestore, onDelete }: Props = $props();
12
+
13
+ function handleRestore(e: MouseEvent) {
14
+ e.preventDefault();
15
+ e.stopPropagation();
16
+ onRestore();
17
+ }
18
+
19
+ function handleDelete(e: MouseEvent) {
20
+ e.preventDefault();
21
+ e.stopPropagation();
22
+ onDelete();
23
+ }
12
24
  </script>
13
25
 
14
26
  <div class="flex gap-2">
15
- <Button size="sm" variant="outline" onclick={onRestore}>{restoreLabel}</Button>
16
- <Button size="sm" variant="destructive" onclick={onDelete}>{deleteLabel}</Button>
27
+ <Button size="sm" variant="outline" onclick={handleRestore}>{restoreLabel}</Button>
28
+ <Button size="sm" variant="destructive" onclick={handleDelete}>{deleteLabel}</Button>
17
29
  </div>
@@ -1,18 +1,31 @@
1
1
  <script lang="ts">
2
+ import Plus from '@tabler/icons-svelte/icons/plus';
3
+ import Button from '../../../components/ui/button/button.svelte';
2
4
  import { getRawCollectionEntryLabel } from '../../utils/entryLabel.js';
5
+ import { getEntryThumbnail } from '../../utils/entryThumbnail.js';
3
6
  import { getRemotes } from '../../../sveltekit/index.js';
4
7
  import type { CollectionConfigWithType } from '../../../types/collections.js';
5
8
  import DataTable from './data-table.svelte';
6
9
  import { getContentLanguage } from '../../state/content-language.svelte.js';
7
- import type { ColumnDef } from '@tanstack/table-core';
10
+ import type { ColumnDef, RowSelectionState, SortingState, Table } from '@tanstack/table-core';
8
11
  import { renderComponent } from '../../../components/ui/data-table/render-helpers.js';
9
12
  import EntryLink from './entry-link.svelte';
10
13
  import type { InterfaceLanguage } from '../../../types/languages.js';
11
14
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
12
15
  import { getEntryStatus } from '../entry/utils.js';
13
16
  import * as Tabs from '../../../components/ui/tabs/index.js';
17
+ import * as AlertDialog from '../../../components/ui/alert-dialog/index.js';
14
18
  import { toast } from 'svelte-sonner';
15
19
  import ArchivedActions from './archived-actions.svelte';
20
+ import StatusBadge from './status-badge.svelte';
21
+ import SortableHeader from './sortable-header.svelte';
22
+ import DateCell from './date-cell.svelte';
23
+ import SelectionCell from './selection-cell.svelte';
24
+ import TableToolbar from './table-toolbar.svelte';
25
+ import TablePagination from './table-pagination.svelte';
26
+ import GridView from './grid-view.svelte';
27
+ import { createCollectionViewState } from './collection-view.svelte.js';
28
+ import type { EntryStatus, RawEntry } from '../../../types/entries.js';
16
29
 
17
30
  const remotes = getRemotes();
18
31
  const contentLanguage = getContentLanguage();
@@ -31,6 +44,12 @@
31
44
  delete: string;
32
45
  entryRestored: string;
33
46
  entryDeleted: string;
47
+ selectAll: string;
48
+ selectRow: string;
49
+ entriesArchived: string;
50
+ deleteConfirmTitle: string;
51
+ deleteConfirmDescription: string;
52
+ cancel: string;
34
53
  }
35
54
  > = {
36
55
  en: {
@@ -43,7 +62,13 @@
43
62
  restore: 'Restore',
44
63
  delete: 'Delete',
45
64
  entryRestored: 'Entry restored',
46
- entryDeleted: 'Entry permanently deleted'
65
+ entryDeleted: 'Entry permanently deleted',
66
+ selectAll: 'Select all',
67
+ selectRow: 'Select row',
68
+ entriesArchived: 'Entries archived',
69
+ deleteConfirmTitle: 'Delete entry permanently?',
70
+ deleteConfirmDescription: 'This action cannot be undone. The entry will be permanently deleted.',
71
+ cancel: 'Cancel'
47
72
  },
48
73
  pl: {
49
74
  name: 'Nazwa',
@@ -55,23 +80,70 @@
55
80
  restore: 'Przywróć',
56
81
  delete: 'Usuń',
57
82
  entryRestored: 'Wpis przywrócony',
58
- entryDeleted: 'Wpis trwale usunięty'
83
+ entryDeleted: 'Wpis trwale usunięty',
84
+ selectAll: 'Zaznacz wszystkie',
85
+ selectRow: 'Zaznacz wiersz',
86
+ entriesArchived: 'Wpisy zarchiwizowane',
87
+ deleteConfirmTitle: 'Usunąć wpis na stałe?',
88
+ deleteConfirmDescription: 'Ta akcja jest nieodwracalna. Wpis zostanie trwale usunięty.',
89
+ cancel: 'Anuluj'
59
90
  }
60
91
  };
61
92
 
62
93
  interface CollectionDataTableRow {
63
94
  id: string;
64
95
  name: string;
65
- status: string;
96
+ status: EntryStatus;
66
97
  createdAt: Date;
67
98
  updatedAt: Date;
68
99
  url: string;
100
+ thumbnail: string | null;
101
+ searchText: string;
69
102
  }
70
103
 
104
+ type Props = {
105
+ collection: CollectionConfigWithType;
106
+ onCreateEntry: () => void;
107
+ addLabel: string;
108
+ };
109
+
110
+ let { collection, onCreateEntry, addLabel }: Props = $props();
111
+
112
+ const viewState = createCollectionViewState(collection.slug);
113
+
114
+ let searchQuery = $state('');
115
+ let rowSelection = $state<RowSelectionState>({});
116
+ let tableInstance = $state<Table<CollectionDataTableRow> | null>(null);
117
+ let deleteDialogOpen = $state(false);
118
+ let pendingDeleteId = $state<string | null>(null);
119
+
71
120
  const columns: ColumnDef<CollectionDataTableRow>[] = $derived([
121
+ {
122
+ id: 'select',
123
+ header: ({ table }) =>
124
+ renderComponent(SelectionCell, {
125
+ checked: table.getIsAllPageRowsSelected(),
126
+ indeterminate: table.getIsSomePageRowsSelected(),
127
+ onCheckedChange: (value: boolean) => table.toggleAllPageRowsSelected(value),
128
+ ariaLabel: lang[interfaceLanguage.current].selectAll
129
+ }),
130
+ cell: ({ row }) =>
131
+ renderComponent(SelectionCell, {
132
+ checked: row.getIsSelected(),
133
+ onCheckedChange: (value: boolean) => row.toggleSelected(value),
134
+ ariaLabel: lang[interfaceLanguage.current].selectRow
135
+ }),
136
+ enableSorting: false,
137
+ enableHiding: false
138
+ },
72
139
  {
73
140
  accessorKey: 'name',
74
- header: lang[interfaceLanguage.current].name,
141
+ header: ({ column }) =>
142
+ renderComponent(SortableHeader<CollectionDataTableRow>, {
143
+ column,
144
+ label: lang[interfaceLanguage.current].name,
145
+ sorting: viewState.sorting
146
+ }),
75
147
  cell: (info) => {
76
148
  return renderComponent(EntryLink, {
77
149
  name: info.row.original.name,
@@ -81,21 +153,40 @@
81
153
  },
82
154
  {
83
155
  accessorKey: 'status',
84
- header: 'Status'
156
+ header: 'Status',
157
+ cell: (info) =>
158
+ renderComponent(StatusBadge, {
159
+ status: info.row.original.status
160
+ }),
161
+ enableSorting: false
85
162
  },
86
163
  {
87
164
  accessorKey: 'createdAt',
88
- header: lang[interfaceLanguage.current].createdAt,
89
- cell: (info) => {
90
- return new Date(info.row.original.createdAt).toLocaleString('pl');
91
- }
165
+ header: ({ column }) =>
166
+ renderComponent(SortableHeader<CollectionDataTableRow>, {
167
+ column,
168
+ label: lang[interfaceLanguage.current].createdAt,
169
+ sorting: viewState.sorting
170
+ }),
171
+ cell: (info) =>
172
+ renderComponent(DateCell, {
173
+ date: info.row.original.createdAt,
174
+ format: viewState.dateFormat
175
+ })
92
176
  },
93
177
  {
94
178
  accessorKey: 'updatedAt',
95
- header: lang[interfaceLanguage.current].updatedAt,
96
- cell: (info) => {
97
- return new Date(info.row.original.updatedAt).toLocaleString('pl');
98
- }
179
+ header: ({ column }) =>
180
+ renderComponent(SortableHeader<CollectionDataTableRow>, {
181
+ column,
182
+ label: lang[interfaceLanguage.current].updatedAt,
183
+ sorting: viewState.sorting
184
+ }),
185
+ cell: (info) =>
186
+ renderComponent(DateCell, {
187
+ date: info.row.original.updatedAt,
188
+ format: viewState.dateFormat
189
+ })
99
190
  }
100
191
  ]);
101
192
 
@@ -112,16 +203,20 @@
112
203
  {
113
204
  accessorKey: 'createdAt',
114
205
  header: lang[interfaceLanguage.current].createdAt,
115
- cell: (info) => {
116
- return new Date(info.row.original.createdAt).toLocaleString('pl');
117
- }
206
+ cell: (info) =>
207
+ renderComponent(DateCell, {
208
+ date: info.row.original.createdAt,
209
+ format: viewState.dateFormat
210
+ })
118
211
  },
119
212
  {
120
213
  accessorKey: 'updatedAt',
121
214
  header: lang[interfaceLanguage.current].updatedAt,
122
- cell: (info) => {
123
- return new Date(info.row.original.updatedAt).toLocaleString('pl');
124
- }
215
+ cell: (info) =>
216
+ renderComponent(DateCell, {
217
+ date: info.row.original.updatedAt,
218
+ format: viewState.dateFormat
219
+ })
125
220
  },
126
221
  {
127
222
  id: 'actions',
@@ -137,12 +232,6 @@
137
232
  }
138
233
  ]);
139
234
 
140
- type Props = {
141
- collection: CollectionConfigWithType;
142
- };
143
-
144
- let { collection }: Props = $props();
145
-
146
235
  let activeTab = $state('active');
147
236
 
148
237
  async function handleRestore(id: string) {
@@ -152,55 +241,199 @@
152
241
  remotes.getRawEntries({ slug: collection.slug }).refresh();
153
242
  }
154
243
 
155
- async function handleDelete(id: string) {
156
- if (!confirm('Are you sure? This cannot be undone.')) return;
157
- await remotes.deleteEntryCommand(id);
244
+ function handleDelete(id: string) {
245
+ pendingDeleteId = id;
246
+ deleteDialogOpen = true;
247
+ }
248
+
249
+ async function confirmDelete() {
250
+ if (!pendingDeleteId) return;
251
+ await remotes.deleteEntryCommand(pendingDeleteId);
158
252
  toast.success(lang[interfaceLanguage.current].entryDeleted);
159
253
  remotes.getRawEntries({ slug: collection.slug, onlyArchived: true }).refresh();
254
+ deleteDialogOpen = false;
255
+ pendingDeleteId = null;
256
+ }
257
+
258
+ function getSearchText(entry: RawEntry): string {
259
+ const data = entry.draftVersion?.data || entry.publishedVersion?.data;
260
+ if (!data) return '';
261
+ return extractTextValues(data).join(' ').toLowerCase();
262
+ }
263
+
264
+ function extractTextValues(obj: unknown): string[] {
265
+ const values: string[] = [];
266
+
267
+ if (typeof obj === 'string') {
268
+ values.push(obj);
269
+ } else if (Array.isArray(obj)) {
270
+ for (const item of obj) {
271
+ values.push(...extractTextValues(item));
272
+ }
273
+ } else if (obj && typeof obj === 'object') {
274
+ for (const value of Object.values(obj)) {
275
+ values.push(...extractTextValues(value));
276
+ }
277
+ }
278
+
279
+ return values;
280
+ }
281
+
282
+ function mapEntryToRow(entry: RawEntry): CollectionDataTableRow {
283
+ const data = entry.draftVersion?.data || entry.publishedVersion?.data || {};
284
+ return {
285
+ id: entry.id,
286
+ name: getRawCollectionEntryLabel(entry, collection, contentLanguage.current),
287
+ url: `/admin/entries/${entry.id}`,
288
+ status: getEntryStatus(entry),
289
+ createdAt: new Date(entry.createdAt),
290
+ updatedAt: new Date(entry.updatedAt),
291
+ thumbnail: getEntryThumbnail(data as Record<string, unknown>, collection),
292
+ searchText: getSearchText(entry)
293
+ };
294
+ }
295
+
296
+ const selectedIndices = $derived(Object.keys(rowSelection).filter((k) => rowSelection[k]).map(Number));
297
+ const selectedCount = $derived(selectedIndices.length);
298
+
299
+ function sortItems<T extends CollectionDataTableRow>(items: T[], sorting: SortingState): T[] {
300
+ if (!sorting.length) return items;
301
+ const { id, desc } = sorting[0];
302
+ return [...items].sort((a, b) => {
303
+ const aVal = a[id as keyof CollectionDataTableRow];
304
+ const bVal = b[id as keyof CollectionDataTableRow];
305
+ if (aVal instanceof Date && bVal instanceof Date) {
306
+ return desc ? bVal.getTime() - aVal.getTime() : aVal.getTime() - bVal.getTime();
307
+ }
308
+ if (typeof aVal === 'string' && typeof bVal === 'string') {
309
+ return desc ? bVal.localeCompare(aVal) : aVal.localeCompare(bVal);
310
+ }
311
+ return 0;
312
+ });
313
+ }
314
+
315
+ async function handleBulkArchive(items: CollectionDataTableRow[]) {
316
+ const idsToArchive = selectedIndices.map((idx) => items[idx]?.id).filter(Boolean);
317
+ for (const id of idsToArchive) {
318
+ await remotes.archiveEntryCommand(id);
319
+ }
320
+ toast.success(lang[interfaceLanguage.current].entriesArchived);
321
+ rowSelection = {};
322
+ remotes.getRawEntries({ slug: collection.slug }).refresh();
323
+ remotes.getRawEntries({ slug: collection.slug, onlyArchived: true }).refresh();
160
324
  }
161
325
  </script>
162
326
 
163
327
  <Tabs.Root bind:value={activeTab} class="w-full">
164
- <Tabs.List class="mb-4">
165
- <Tabs.Trigger value="active">{lang[interfaceLanguage.current].active}</Tabs.Trigger>
166
- <Tabs.Trigger value="archived">{lang[interfaceLanguage.current].archived}</Tabs.Trigger>
167
- </Tabs.List>
328
+ <div class="flex items-center justify-between border-b border-slate-200/50 px-4 py-3 dark:border-white/10">
329
+ <Tabs.List class="h-auto gap-1 bg-transparent p-0">
330
+ <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>
331
+ <Tabs.Trigger value="archived" 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].archived}</Tabs.Trigger>
332
+ </Tabs.List>
333
+ <Button type="button" onclick={onCreateEntry} class="btn-gradient" size="sm">
334
+ <Plus class="size-4" /> {addLabel}
335
+ </Button>
336
+ </div>
168
337
 
169
- <Tabs.Content value="active">
338
+ <Tabs.Content value="active" class="mt-0">
170
339
  {#await remotes.getRawEntries({ slug: collection.slug }) then entries}
171
340
  {@const items = entries
172
341
  .filter((entry) => entry.archivedAt === null)
173
- .map((entry) => ({
174
- id: entry.id,
175
- collection: entry.slug,
176
- name: getRawCollectionEntryLabel(entry, collection, contentLanguage.current),
177
- url: `/admin/entries/${entry.id}`,
178
- status: getEntryStatus(entry),
179
- createdAt: entry.createdAt,
180
- updatedAt: entry.updatedAt
181
- }))}
182
- {#key interfaceLanguage.current}
183
- <DataTable data={items} {columns} />
184
- {/key}
342
+ .map(mapEntryToRow)}
343
+ {@const filteredItems = searchQuery
344
+ ? items.filter((item) => item.searchText.includes(searchQuery.toLowerCase()))
345
+ : items}
346
+ {@const totalItems = filteredItems.length}
347
+ {@const pageCount = Math.ceil(totalItems / viewState.pageSize)}
348
+
349
+ <TableToolbar
350
+ {searchQuery}
351
+ onSearchChange={(q) => (searchQuery = q)}
352
+ viewMode={viewState.viewMode}
353
+ onViewModeChange={(m) => (viewState.viewMode = m)}
354
+ dateFormat={viewState.dateFormat}
355
+ onDateFormatChange={(f) => (viewState.dateFormat = f)}
356
+ {selectedCount}
357
+ onBulkArchive={() => handleBulkArchive(sortItems(filteredItems, viewState.sorting))}
358
+ />
359
+
360
+ {#if viewState.viewMode === 'list'}
361
+ <DataTable
362
+ data={sortItems(filteredItems, viewState.sorting)}
363
+ {columns}
364
+ enableSorting
365
+ enableFiltering
366
+ enableSelection
367
+ enablePagination
368
+ sorting={viewState.sorting}
369
+ onSortingChange={(s) => (viewState.sorting = s)}
370
+ {rowSelection}
371
+ onRowSelectionChange={(s) => (rowSelection = s)}
372
+ pagination={{ pageIndex: 0, pageSize: viewState.pageSize }}
373
+ tableRef={(t) => (tableInstance = t)}
374
+ />
375
+
376
+ <TablePagination
377
+ pageIndex={tableInstance?.getState().pagination.pageIndex ?? 0}
378
+ pageSize={viewState.pageSize}
379
+ {pageCount}
380
+ {totalItems}
381
+ onPageChange={(p) => tableInstance?.setPageIndex(p)}
382
+ onPageSizeChange={(s) => {
383
+ viewState.pageSize = s;
384
+ tableInstance?.setPageSize(s);
385
+ }}
386
+ />
387
+ {:else}
388
+ <GridView
389
+ items={sortItems(filteredItems, viewState.sorting).map((item) => ({
390
+ id: item.id,
391
+ name: item.name,
392
+ status: item.status,
393
+ url: item.url,
394
+ thumbnail: item.thumbnail,
395
+ updatedAt: item.updatedAt
396
+ }))}
397
+ dateFormat={viewState.dateFormat}
398
+ enableSelection
399
+ selectedIds={new Set(selectedIndices.map((idx) => sortItems(filteredItems, viewState.sorting)[idx]?.id).filter(Boolean))}
400
+ onSelectionChange={(ids) => {
401
+ const sorted = sortItems(filteredItems, viewState.sorting);
402
+ const newSelection: RowSelectionState = {};
403
+ for (const id of ids) {
404
+ const idx = sorted.findIndex((i) => i.id === id);
405
+ if (idx !== -1) {
406
+ newSelection[idx] = true;
407
+ }
408
+ }
409
+ rowSelection = newSelection;
410
+ }}
411
+ />
412
+ {/if}
185
413
  {/await}
186
414
  </Tabs.Content>
187
415
 
188
- <Tabs.Content value="archived">
416
+ <Tabs.Content value="archived" class="mt-0">
189
417
  {#await remotes.getRawEntries({ slug: collection.slug, onlyArchived: true }) then entries}
190
418
  {@const items = entries.map((entry) => ({
191
- id: entry.id,
192
- collection: entry.slug,
193
- name: getRawCollectionEntryLabel(entry, collection, contentLanguage.current),
194
- url: `/admin/entries/${entry.id}`,
195
- status: getEntryStatus(entry),
196
- createdAt: entry.createdAt,
197
- updatedAt: entry.updatedAt,
419
+ ...mapEntryToRow(entry),
198
420
  onRestore: () => handleRestore(entry.id),
199
421
  onDelete: () => handleDelete(entry.id)
200
422
  }))}
201
- {#key interfaceLanguage.current}
423
+ <div class="p-4">
202
424
  <DataTable data={items} columns={archivedColumns} />
203
- {/key}
425
+ </div>
204
426
  {/await}
205
427
  </Tabs.Content>
206
428
  </Tabs.Root>
429
+
430
+ <AlertDialog.Root bind:open={deleteDialogOpen}>
431
+ <AlertDialog.Content>
432
+ <AlertDialog.Title>{lang[interfaceLanguage.current].deleteConfirmTitle}</AlertDialog.Title>
433
+ <AlertDialog.Description>{lang[interfaceLanguage.current].deleteConfirmDescription}</AlertDialog.Description>
434
+ <AlertDialog.Footer>
435
+ <AlertDialog.Cancel>{lang[interfaceLanguage.current].cancel}</AlertDialog.Cancel>
436
+ <AlertDialog.Action onclick={confirmDelete}>{lang[interfaceLanguage.current].delete}</AlertDialog.Action>
437
+ </AlertDialog.Footer>
438
+ </AlertDialog.Content>
439
+ </AlertDialog.Root>
@@ -1,6 +1,8 @@
1
1
  import type { CollectionConfigWithType } from '../../../types/collections.js';
2
2
  type Props = {
3
3
  collection: CollectionConfigWithType;
4
+ onCreateEntry: () => void;
5
+ addLabel: string;
4
6
  };
5
7
  declare const CollectionEntries: import("svelte").Component<Props, {}, "">;
6
8
  type CollectionEntries = ReturnType<typeof CollectionEntries>;
@@ -0,0 +1,15 @@
1
+ import type { SortingState } from '@tanstack/table-core';
2
+ export type ViewMode = 'list' | 'grid';
3
+ export type DateFormat = 'relative' | 'absolute';
4
+ export interface CollectionViewState {
5
+ viewMode: ViewMode;
6
+ dateFormat: DateFormat;
7
+ pageSize: number;
8
+ sorting: SortingState;
9
+ }
10
+ export declare function createCollectionViewState(collectionSlug: string): {
11
+ viewMode: ViewMode;
12
+ dateFormat: DateFormat;
13
+ pageSize: number;
14
+ sorting: SortingState;
15
+ };
@@ -0,0 +1,66 @@
1
+ const DEFAULT_STATE = {
2
+ viewMode: 'list',
3
+ dateFormat: 'relative',
4
+ pageSize: 10,
5
+ sorting: []
6
+ };
7
+ function getStorageKey(collectionSlug) {
8
+ return `includio-collection-${collectionSlug}-view`;
9
+ }
10
+ function loadState(collectionSlug) {
11
+ if (typeof window === 'undefined')
12
+ return DEFAULT_STATE;
13
+ try {
14
+ const stored = localStorage.getItem(getStorageKey(collectionSlug));
15
+ if (stored) {
16
+ const parsed = JSON.parse(stored);
17
+ return { ...DEFAULT_STATE, ...parsed };
18
+ }
19
+ }
20
+ catch {
21
+ // ignore
22
+ }
23
+ return DEFAULT_STATE;
24
+ }
25
+ function saveState(collectionSlug, state) {
26
+ if (typeof window === 'undefined')
27
+ return;
28
+ try {
29
+ localStorage.setItem(getStorageKey(collectionSlug), JSON.stringify(state));
30
+ }
31
+ catch {
32
+ // ignore
33
+ }
34
+ }
35
+ export function createCollectionViewState(collectionSlug) {
36
+ let state = $state(loadState(collectionSlug));
37
+ $effect(() => {
38
+ saveState(collectionSlug, state);
39
+ });
40
+ return {
41
+ get viewMode() {
42
+ return state.viewMode;
43
+ },
44
+ set viewMode(value) {
45
+ state = { ...state, viewMode: value };
46
+ },
47
+ get dateFormat() {
48
+ return state.dateFormat;
49
+ },
50
+ set dateFormat(value) {
51
+ state = { ...state, dateFormat: value };
52
+ },
53
+ get pageSize() {
54
+ return state.pageSize;
55
+ },
56
+ set pageSize(value) {
57
+ state = { ...state, pageSize: value };
58
+ },
59
+ get sorting() {
60
+ return state.sorting;
61
+ },
62
+ set sorting(value) {
63
+ state = { ...state, sorting: value };
64
+ }
65
+ };
66
+ }