includio-cms 0.5.3 → 0.5.7

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 (109) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/ROADMAP.md +33 -0
  3. package/dist/admin/api/rest/handler.d.ts +7 -0
  4. package/dist/admin/api/rest/handler.js +116 -0
  5. package/dist/admin/api/rest/middleware/apiKey.d.ts +6 -0
  6. package/dist/admin/api/rest/middleware/apiKey.js +45 -0
  7. package/dist/admin/api/rest/routes/collections.d.ts +5 -0
  8. package/dist/admin/api/rest/routes/collections.js +104 -0
  9. package/dist/admin/api/rest/routes/entries.d.ts +2 -0
  10. package/dist/admin/api/rest/routes/entries.js +37 -0
  11. package/dist/admin/api/rest/routes/languages.d.ts +1 -0
  12. package/dist/admin/api/rest/routes/languages.js +5 -0
  13. package/dist/admin/api/rest/routes/schema.d.ts +2 -0
  14. package/dist/admin/api/rest/routes/schema.js +78 -0
  15. package/dist/admin/api/rest/routes/singletons.d.ts +3 -0
  16. package/dist/admin/api/rest/routes/singletons.js +60 -0
  17. package/dist/admin/auth-client.d.ts +48 -48
  18. package/dist/admin/client/admin/admin-after-login-layout.svelte +5 -3
  19. package/dist/admin/client/admin/admin-layout.svelte +0 -2
  20. package/dist/admin/client/collection/bulk-actions-bar.svelte +3 -1
  21. package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +1 -1
  22. package/dist/admin/client/collection/collection-entries.svelte +334 -166
  23. package/dist/admin/client/collection/collection-page.svelte +6 -3
  24. package/dist/admin/client/collection/data-table.svelte +127 -18
  25. package/dist/admin/client/collection/data-table.svelte.d.ts +2 -0
  26. package/dist/admin/client/collection/row-actions.svelte +3 -1
  27. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -1
  28. package/dist/admin/client/entry/entry-page.svelte +5 -4
  29. package/dist/admin/client/form/form-config-wrapper.svelte +5 -3
  30. package/dist/admin/client/form/form-page.svelte +5 -5
  31. package/dist/admin/client/form/form-submission/form-submission-page.svelte +30 -32
  32. package/dist/admin/components/accessibility/accessibility-overview.svelte +7 -5
  33. package/dist/admin/components/accessibility/accessibility-overview.svelte.d.ts +2 -17
  34. package/dist/admin/components/dashboard/a11y-gauge.svelte +7 -4
  35. package/dist/admin/components/dashboard/a11y-gauge.svelte.d.ts +2 -17
  36. package/dist/admin/components/dashboard/accessibility-hub.svelte +7 -4
  37. package/dist/admin/components/dashboard/accessibility-hub.svelte.d.ts +2 -17
  38. package/dist/admin/components/dashboard/form-submissions-widget.svelte +8 -3
  39. package/dist/admin/components/dashboard/form-submissions-widget.svelte.d.ts +2 -17
  40. package/dist/admin/components/dashboard/recent-activity.svelte +8 -3
  41. package/dist/admin/components/dashboard/recent-activity.svelte.d.ts +2 -17
  42. package/dist/admin/components/dashboard/recent-entries.svelte +8 -3
  43. package/dist/admin/components/dashboard/recent-entries.svelte.d.ts +2 -17
  44. package/dist/admin/components/fields/image-field.svelte +21 -20
  45. package/dist/admin/components/fields/media-field.svelte +45 -44
  46. package/dist/admin/components/fields/relation-field.svelte +428 -149
  47. package/dist/admin/components/fields/relation-picker-dialog.svelte +112 -0
  48. package/dist/admin/components/fields/relation-picker-dialog.svelte.d.ts +22 -0
  49. package/dist/admin/components/fields/seo-field.svelte +66 -10
  50. package/dist/admin/components/icons/icon-map.d.ts +4 -0
  51. package/dist/admin/components/icons/icon-map.js +143 -0
  52. package/dist/admin/components/layout/header-actions.svelte +0 -10
  53. package/dist/admin/components/layout/nav-collections.svelte +55 -57
  54. package/dist/admin/components/layout/nav-collections.svelte.d.ts +2 -17
  55. package/dist/admin/components/layout/nav-forms.svelte +49 -49
  56. package/dist/admin/components/layout/nav-forms.svelte.d.ts +2 -17
  57. package/dist/admin/components/layout/nav-search.svelte +15 -22
  58. package/dist/admin/components/layout/nav-singletons.svelte +35 -38
  59. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +2 -17
  60. package/dist/admin/components/media/file/file-preview.svelte +92 -91
  61. package/dist/admin/components/media/file-preview.svelte +17 -18
  62. package/dist/admin/components/media/media-library.svelte +39 -41
  63. package/dist/admin/components/media/media-selector.svelte +8 -8
  64. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +6 -8
  65. package/dist/admin/components/tiptap/content-editor.svelte +2 -2
  66. package/dist/admin/components/tiptap/editor-toolbar.svelte +1 -1
  67. package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
  68. package/dist/admin/components/tiptap/inline-block-node.js +12 -1
  69. package/dist/admin/components/tiptap/tiptap-editor.svelte +1 -1
  70. package/dist/admin/remote/entry.remote.d.ts +20 -1
  71. package/dist/admin/remote/entry.remote.js +24 -3
  72. package/dist/admin/remote/invite.d.ts +2 -2
  73. package/dist/cli/scaffold/admin.js +8 -0
  74. package/dist/cms/runtime/remote.d.ts +8 -0
  75. package/dist/cms/runtime/remote.js +7 -0
  76. package/dist/cms/runtime/types.d.ts +1 -1
  77. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  78. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  79. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  80. package/dist/components/ui/sonner/sonner.svelte +1 -2
  81. package/dist/core/cms.d.ts +2 -1
  82. package/dist/core/cms.js +2 -0
  83. package/dist/core/server/entries/operations/get.d.ts +11 -0
  84. package/dist/core/server/entries/operations/get.js +63 -1
  85. package/dist/core/server/entries/operations/update.d.ts +1 -0
  86. package/dist/core/server/entries/operations/update.js +5 -1
  87. package/dist/core/server/fields/resolveUrlFields.js +49 -18
  88. package/dist/db-postgres/schema/entry.d.ts +17 -0
  89. package/dist/db-postgres/schema/entry.js +4 -2
  90. package/dist/server/auth.d.ts +4 -4
  91. package/dist/sveltekit/components/structured-content.svelte +18 -5
  92. package/dist/sveltekit/server/handle.js +1 -0
  93. package/dist/types/cms.d.ts +7 -0
  94. package/dist/types/collections.d.ts +2 -0
  95. package/dist/types/config.d.ts +2 -2
  96. package/dist/types/entries.d.ts +7 -1
  97. package/dist/types/forms.d.ts +2 -0
  98. package/dist/types/index.d.ts +2 -1
  99. package/dist/types/index.js +1 -0
  100. package/dist/updates/0.5.4/index.d.ts +2 -0
  101. package/dist/updates/0.5.4/index.js +15 -0
  102. package/dist/updates/0.5.5/index.d.ts +2 -0
  103. package/dist/updates/0.5.5/index.js +20 -0
  104. package/dist/updates/0.5.6/index.d.ts +2 -0
  105. package/dist/updates/0.5.6/index.js +17 -0
  106. package/dist/updates/0.5.7/index.d.ts +2 -0
  107. package/dist/updates/0.5.7/index.js +14 -0
  108. package/dist/updates/index.js +5 -1
  109. package/package.json +6 -2
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { getRawCollectionEntryLabel } from '../../utils/entryLabel.js';
3
3
  import { getEntryThumbnail } from '../../utils/entryThumbnail.js';
4
+ import { arrayMove } from '../../utils/arrayMove.js';
4
5
  import { getRemotes } from '../../../sveltekit/index.js';
5
6
  import type { CollectionConfigWithType } from '../../../types/collections.js';
6
7
  import DataTable from './data-table.svelte';
@@ -11,6 +12,7 @@
11
12
  import type { InterfaceLanguage } from '../../../types/languages.js';
12
13
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
13
14
  import { getEntryStatus } from '../entry/utils.js';
15
+ import { getLocalizedLabel } from '../../utils/collectionLabel.js';
14
16
  import * as AlertDialog from '../../../components/ui/alert-dialog/index.js';
15
17
  import { toast } from 'svelte-sonner';
16
18
  import StatusBadge from './status-badge.svelte';
@@ -26,11 +28,13 @@
26
28
  import EmptyState from './empty-state.svelte';
27
29
  import { createCollectionViewState } from './collection-view.svelte.js';
28
30
  import type { EntryStatus as EntryStatusType, RawEntry } from '../../../types/entries.js';
31
+ import type { RelationField } from '../../../types/fields.js';
29
32
  import {
30
33
  validateA11y,
31
34
  a11yLangPl,
32
35
  a11yLangEn
33
36
  } from '../entry/header/a11y-validator.js';
37
+ import Skeleton from '../../../components/ui/skeleton/skeleton.svelte';
34
38
 
35
39
  const remotes = getRemotes();
36
40
  const contentLanguage = getContentLanguage();
@@ -110,6 +114,7 @@
110
114
  thumbnail: string | null;
111
115
  searchText: string;
112
116
  a11yWarnings: number;
117
+ customData: Record<string, unknown>;
113
118
  }
114
119
 
115
120
  type Props = {
@@ -128,6 +133,17 @@
128
133
  let deleteDialogOpen = $state(false);
129
134
  let pendingDeleteId = $state<string | null>(null);
130
135
 
136
+ // Relation fields in listColumns: detect and build lookup
137
+ const relationListFields = $derived(
138
+ (collection.listColumns ?? [])
139
+ .map((slug) => {
140
+ const field = collection.fields.find((f) => f.slug === slug);
141
+ return field?.type === 'relation' ? (field as RelationField) : null;
142
+ })
143
+ .filter((f): f is RelationField => f !== null)
144
+ );
145
+
146
+
131
147
  // Find seo field for slug extraction
132
148
  const seoField = $derived(collection.fields.find((f) => f.type === 'seo'));
133
149
 
@@ -140,8 +156,14 @@
140
156
  // Is the current filter for archived entries?
141
157
  const isArchivedFilter = $derived(viewState.statusFilter === 'archived');
142
158
 
159
+ const isOrderable = $derived(!!collection.orderable);
160
+
143
161
  // Build orderBy from sorting state for server-side mode
144
162
  const serverOrderBy = $derived.by(() => {
163
+ // When orderable, always sort by sortOrder
164
+ if (isOrderable) {
165
+ return { column: 'sortOrder' as const, direction: 'asc' as const };
166
+ }
145
167
  if (!viewState.sorting.length) return undefined;
146
168
  const { id, desc } = viewState.sorting[0];
147
169
  if (id === 'createdAt' || id === 'updatedAt') {
@@ -169,6 +191,20 @@
169
191
  }
170
192
  );
171
193
 
194
+ // Reactive query
195
+ const entriesQuery = $derived(remotes.getRawEntries(queryParams));
196
+
197
+ // Track relation labels
198
+ let labelLookup = $state<Record<string, string>>({});
199
+
200
+ $effect(() => {
201
+ if (entriesQuery.current) {
202
+ fetchRelationLabels(entriesQuery.current.entries).then((l) => {
203
+ labelLookup = l;
204
+ });
205
+ }
206
+ });
207
+
172
208
  const columns: ColumnDef<CollectionDataTableRow>[] = $derived([
173
209
  {
174
210
  id: 'select',
@@ -240,6 +276,23 @@
240
276
  }),
241
277
  size: 140
242
278
  },
279
+ // Custom list columns from collection config
280
+ ...(collection.listColumns ?? []).map((fieldSlug) => {
281
+ const fieldConfig = collection.fields.find((f) => f.slug === fieldSlug);
282
+ const headerLabel = fieldConfig?.label
283
+ ? getLocalizedLabel(fieldConfig.label, interfaceLanguage.current)
284
+ : fieldSlug;
285
+ return {
286
+ id: `custom_${fieldSlug}`,
287
+ header: headerLabel,
288
+ cell: (info: { row: { original: CollectionDataTableRow } }) => {
289
+ const value = info.row.original.customData[fieldSlug];
290
+ return String(value ?? '');
291
+ },
292
+ enableSorting: false,
293
+ size: 120
294
+ } satisfies ColumnDef<CollectionDataTableRow>;
295
+ }),
243
296
  {
244
297
  id: 'actions',
245
298
  header: '',
@@ -248,28 +301,35 @@
248
301
  entryUrl: info.row.original.url,
249
302
  entryName: info.row.original.name,
250
303
  onArchive: () => handleArchiveSingle(info.row.original.id),
251
- onDelete: () => handleDelete(info.row.original.id),
252
- ...(isArchivedFilter ? { onRestore: () => handleRestoreSingle(info.row.original.id) } : {})
304
+ ...(isArchivedFilter ? { onDelete: () => handleDelete(info.row.original.id), onRestore: () => handleRestoreSingle(info.row.original.id) } : {})
253
305
  }),
254
306
  enableSorting: false,
255
307
  size: 50
256
308
  }
257
309
  ]);
258
310
 
259
- function refreshQueries() {
260
- remotes.getRawEntries(queryParams).refresh();
261
- }
262
-
263
311
  async function handleArchiveSingle(id: string) {
312
+ const override = entriesQuery.withOverride((old) => ({
313
+ ...old,
314
+ entries: old.entries.filter((e: RawEntry) => e.id !== id),
315
+ total: old.total - 1
316
+ }));
264
317
  await remotes.archiveEntryCommand(id);
265
318
  toast.success(lang[interfaceLanguage.current].entriesArchived);
266
- refreshQueries();
319
+ await entriesQuery.refresh();
320
+ override.release();
267
321
  }
268
322
 
269
323
  async function handleRestoreSingle(id: string) {
324
+ const override = entriesQuery.withOverride((old) => ({
325
+ ...old,
326
+ entries: old.entries.filter((e: RawEntry) => e.id !== id),
327
+ total: old.total - 1
328
+ }));
270
329
  await remotes.unarchiveEntryCommand(id);
271
330
  toast.success(lang[interfaceLanguage.current].entryRestored);
272
- refreshQueries();
331
+ await entriesQuery.refresh();
332
+ override.release();
273
333
  }
274
334
 
275
335
  function handleDelete(id: string) {
@@ -279,9 +339,16 @@
279
339
 
280
340
  async function confirmDelete() {
281
341
  if (!pendingDeleteId) return;
282
- await remotes.deleteEntryCommand(pendingDeleteId);
342
+ const id = pendingDeleteId;
343
+ const override = entriesQuery.withOverride((old) => ({
344
+ ...old,
345
+ entries: old.entries.filter((e: RawEntry) => e.id !== id),
346
+ total: old.total - 1
347
+ }));
348
+ await remotes.deleteEntryCommand(id);
283
349
  toast.success(lang[interfaceLanguage.current].entryDeleted);
284
- refreshQueries();
350
+ await entriesQuery.refresh();
351
+ override.release();
285
352
  deleteDialogOpen = false;
286
353
  pendingDeleteId = null;
287
354
  }
@@ -329,8 +396,68 @@
329
396
  return issues.filter((i) => i.type === 'warning').length;
330
397
  }
331
398
 
332
- function mapEntryToRow(entry: RawEntry): CollectionDataTableRow {
399
+ async function fetchRelationLabels(entries: RawEntry[]): Promise<Record<string, string>> {
400
+ if (relationListFields.length === 0) return {};
401
+
402
+ const uuidsByCollection = new Map<string, Set<string>>();
403
+ for (const field of relationListFields) {
404
+ uuidsByCollection.set(field.collection, new Set());
405
+ }
406
+ for (const entry of entries) {
407
+ const data = entry.publishedVersion?.data || entry.draftVersion?.data;
408
+ if (!data) continue;
409
+ for (const field of relationListFields) {
410
+ const raw = (data as Record<string, unknown>)[field.slug];
411
+ if (!raw) continue;
412
+ const set = uuidsByCollection.get(field.collection)!;
413
+ if (typeof raw === 'string') set.add(raw);
414
+ if (Array.isArray(raw)) raw.forEach((v) => typeof v === 'string' && set.add(v));
415
+ }
416
+ }
417
+
418
+ const lookup: Record<string, string> = {};
419
+ await Promise.all(
420
+ Array.from(uuidsByCollection.entries()).map(async ([slug, ids]) => {
421
+ if (ids.size === 0) return;
422
+ const labels = await remotes.getEntryLabels({ slug, ids: Array.from(ids) });
423
+ for (const { id, label } of labels) {
424
+ lookup[id] = label;
425
+ }
426
+ })
427
+ );
428
+ return lookup;
429
+ }
430
+
431
+ function mapEntryToRow(entry: RawEntry, lookup: Record<string, string> = {}): CollectionDataTableRow {
333
432
  const data = entry.draftVersion?.data || entry.publishedVersion?.data || {};
433
+ // For custom columns, prefer published data (complete) over draft (may be partial)
434
+ const columnData = entry.publishedVersion?.data || entry.draftVersion?.data || {};
435
+ const customData: Record<string, unknown> = {};
436
+ if (collection.listColumns) {
437
+ for (const fieldSlug of collection.listColumns) {
438
+ const fieldData = (columnData as Record<string, unknown>)[fieldSlug];
439
+ const isRelation = relationListFields.some((f) => f.slug === fieldSlug);
440
+ if (isRelation) {
441
+ // Resolve relation UUID(s) to labels
442
+ if (typeof fieldData === 'string') {
443
+ customData[fieldSlug] = lookup[fieldData] ?? '';
444
+ } else if (Array.isArray(fieldData)) {
445
+ customData[fieldSlug] = fieldData
446
+ .map((id) => (typeof id === 'string' ? lookup[id] ?? '' : ''))
447
+ .filter(Boolean)
448
+ .join(', ');
449
+ } else {
450
+ customData[fieldSlug] = '';
451
+ }
452
+ } else if (fieldData && typeof fieldData === 'object' && !Array.isArray(fieldData)) {
453
+ // For localized fields, extract current content language value
454
+ const localized = fieldData as Record<string, unknown>;
455
+ customData[fieldSlug] = localized[contentLanguage.current] ?? Object.values(localized)[0] ?? '';
456
+ } else {
457
+ customData[fieldSlug] = fieldData ?? '';
458
+ }
459
+ }
460
+ }
334
461
  return {
335
462
  id: entry.id,
336
463
  name: getRawCollectionEntryLabel(entry, collection, contentLanguage.current),
@@ -341,7 +468,8 @@
341
468
  updatedAt: new Date(entry.updatedAt),
342
469
  thumbnail: getEntryThumbnail(data as Record<string, unknown>, collection),
343
470
  searchText: getSearchText(entry),
344
- a11yWarnings: countA11yWarnings(entry)
471
+ a11yWarnings: countA11yWarnings(entry),
472
+ customData
345
473
  };
346
474
  }
347
475
 
@@ -386,190 +514,230 @@
386
514
 
387
515
  async function handleBulkArchive(items: CollectionDataTableRow[]) {
388
516
  const idsToArchive = selectedIndices.map((idx) => items[idx]?.id).filter(Boolean);
517
+ const override = entriesQuery.withOverride((old) => ({
518
+ ...old,
519
+ entries: old.entries.filter((e: RawEntry) => !idsToArchive.includes(e.id)),
520
+ total: old.total - idsToArchive.length
521
+ }));
389
522
  for (const id of idsToArchive) {
390
523
  await remotes.archiveEntryCommand(id);
391
524
  }
392
525
  toast.success(lang[interfaceLanguage.current].entriesArchived);
393
526
  rowSelection = {};
394
- refreshQueries();
527
+ await entriesQuery.refresh();
528
+ override.release();
395
529
  }
396
530
 
397
531
  async function handleBulkRestore(items: CollectionDataTableRow[]) {
398
532
  const idsToRestore = selectedIndices.map((idx) => items[idx]?.id).filter(Boolean);
533
+ const override = entriesQuery.withOverride((old) => ({
534
+ ...old,
535
+ entries: old.entries.filter((e: RawEntry) => !idsToRestore.includes(e.id)),
536
+ total: old.total - idsToRestore.length
537
+ }));
399
538
  for (const id of idsToRestore) {
400
539
  await remotes.unarchiveEntryCommand(id);
401
540
  }
402
541
  toast.success(lang[interfaceLanguage.current].entriesRestored);
403
542
  rowSelection = {};
404
- refreshQueries();
543
+ await entriesQuery.refresh();
544
+ override.release();
545
+ }
546
+
547
+ async function handleReorder(items: CollectionDataTableRow[], fromIndex: number, toIndex: number) {
548
+ if (!isOrderable || items.length === 0) return;
549
+ const reordered = arrayMove(items, fromIndex, toIndex);
550
+ const orderedIds = reordered.map((item) => item.id);
551
+ const override = entriesQuery.withOverride((old) => ({
552
+ ...old,
553
+ entries: orderedIds.map((id) => old.entries.find((e: RawEntry) => e.id === id)).filter(Boolean)
554
+ }));
555
+ await remotes.reorderEntriesCommand({ orderedIds });
556
+ await entriesQuery.refresh();
557
+ override.release();
405
558
  }
406
559
 
407
560
  async function handleBulkDelete(items: CollectionDataTableRow[]) {
408
561
  const idsToDelete = selectedIndices.map((idx) => items[idx]?.id).filter(Boolean);
562
+ const override = entriesQuery.withOverride((old) => ({
563
+ ...old,
564
+ entries: old.entries.filter((e: RawEntry) => !idsToDelete.includes(e.id)),
565
+ total: old.total - idsToDelete.length
566
+ }));
409
567
  for (const id of idsToDelete) {
410
568
  await remotes.deleteEntryCommand(id);
411
569
  }
412
570
  toast.success(lang[interfaceLanguage.current].entryDeleted);
413
571
  rowSelection = {};
414
- refreshQueries();
572
+ await entriesQuery.refresh();
573
+ override.release();
415
574
  }
416
575
  </script>
417
576
 
418
- {#key JSON.stringify(queryParams)}
419
- {#await remotes.getRawEntries(queryParams) then result}
420
- {@const allRows = result.entries.map(mapEntryToRow)}
421
- {@const filteredRows = (() => {
422
- let rows = allRows;
423
- // Client-side status filter (non-archived statuses)
424
- if (!isArchivedFilter) {
425
- rows = filterByStatus(rows);
426
- }
427
- // Client-side search
428
- if (searchQuery) {
429
- rows = rows.filter((item) => item.searchText.includes(searchQuery.toLowerCase()));
430
- }
431
- // Client-side sort (if sorting by name)
432
- if (!useServerPagination) {
433
- rows = sortItems(rows, viewState.sorting);
434
- }
435
- return rows;
436
- })()}
437
- {@const totalItems =
438
- useServerPagination && !viewState.statusFilter && viewState.statusFilter !== 'archived'
439
- ? result.total
440
- : filteredRows.length}
441
- {@const pageCount = Math.ceil(totalItems / viewState.pageSize)}
442
- {@const displayItems =
443
- useServerPagination && !viewState.statusFilter
444
- ? filteredRows
445
- : filteredRows.slice(
446
- viewState.pageIndex * viewState.pageSize,
447
- (viewState.pageIndex + 1) * viewState.pageSize
448
- )}
449
-
450
- <TableToolbar
451
- {searchQuery}
452
- onSearchChange={(q) => {
453
- searchQuery = q;
454
- viewState.pageIndex = 0;
577
+ {#if !entriesQuery.ready}
578
+ <div class="space-y-4 p-4">
579
+ <Skeleton class="h-10 w-full" />
580
+ <Skeleton class="h-64 w-full" />
581
+ </div>
582
+ {:else if entriesQuery.current}
583
+ {@const result = entriesQuery.current}
584
+ {@const allRows = result.entries.map((e) => mapEntryToRow(e, labelLookup))}
585
+ {@const filteredRows = (() => {
586
+ let rows = allRows;
587
+ // Client-side status filter (non-archived statuses)
588
+ if (!isArchivedFilter) {
589
+ rows = filterByStatus(rows);
590
+ }
591
+ // Client-side search
592
+ if (searchQuery) {
593
+ rows = rows.filter((item) => item.searchText.includes(searchQuery.toLowerCase()));
594
+ }
595
+ // Client-side sort (if sorting by name)
596
+ if (!useServerPagination) {
597
+ rows = sortItems(rows, viewState.sorting);
598
+ }
599
+ return rows;
600
+ })()}
601
+ {@const totalItems =
602
+ useServerPagination && !viewState.statusFilter && viewState.statusFilter !== 'archived'
603
+ ? result.total
604
+ : filteredRows.length}
605
+ {@const pageCount = Math.ceil(totalItems / viewState.pageSize)}
606
+ {@const displayItems = useServerPagination && !viewState.statusFilter
607
+ ? filteredRows
608
+ : filteredRows.slice(
609
+ viewState.pageIndex * viewState.pageSize,
610
+ (viewState.pageIndex + 1) * viewState.pageSize
611
+ )}
612
+
613
+ <div class:opacity-60={entriesQuery.loading} class="transition-opacity duration-200">
614
+ <TableToolbar
615
+ {searchQuery}
616
+ onSearchChange={(q) => {
617
+ searchQuery = q;
618
+ viewState.pageIndex = 0;
619
+ }}
620
+ statusFilter={viewState.statusFilter}
621
+ onStatusFilterChange={(f) => (viewState.statusFilter = f)}
622
+ viewMode={viewState.viewMode}
623
+ onViewModeChange={(m) => (viewState.viewMode = m)}
624
+ {onCreateEntry}
625
+ createLabel={addLabel}
626
+ />
627
+
628
+ {#if totalItems === 0 && !searchQuery}
629
+ <EmptyState
630
+ title={lang[interfaceLanguage.current].emptyTitle}
631
+ description={lang[interfaceLanguage.current].emptyDescription}
632
+ ctaLabel={addLabel}
633
+ onCta={onCreateEntry}
634
+ />
635
+ {:else if viewState.viewMode === 'list'}
636
+ <div class="bg-card overflow-hidden rounded-xl border shadow-sm">
637
+ {#if useServerPagination && !viewState.statusFilter}
638
+ <DataTable
639
+ data={displayItems}
640
+ {columns}
641
+ enableSorting={!isOrderable}
642
+ enableFiltering
643
+ enableSelection
644
+ enablePagination
645
+ manualPagination={true}
646
+ {pageCount}
647
+ rowCount={totalItems}
648
+ sorting={viewState.sorting}
649
+ onSortingChange={(s) => (viewState.sorting = s)}
650
+ {rowSelection}
651
+ onRowSelectionChange={(s) => (rowSelection = s)}
652
+ pagination={{ pageIndex: viewState.pageIndex, pageSize: viewState.pageSize }}
653
+ onPaginationChange={(p) => {
654
+ viewState.pageIndex = p.pageIndex;
655
+ }}
656
+ tableRef={(t) => (tableInstance = t)}
657
+ orderable={isOrderable}
658
+ onReorder={(from, to) => handleReorder(displayItems, from, to)}
659
+ />
660
+ {:else}
661
+ <DataTable
662
+ data={displayItems}
663
+ {columns}
664
+ enableSorting={!isOrderable}
665
+ enableFiltering
666
+ enableSelection
667
+ enablePagination
668
+ sorting={viewState.sorting}
669
+ onSortingChange={(s) => (viewState.sorting = s)}
670
+ {rowSelection}
671
+ onRowSelectionChange={(s) => (rowSelection = s)}
672
+ pagination={{ pageIndex: viewState.pageIndex, pageSize: viewState.pageSize }}
673
+ onPaginationChange={(p) => {
674
+ viewState.pageIndex = p.pageIndex;
675
+ }}
676
+ tableRef={(t) => (tableInstance = t)}
677
+ orderable={isOrderable}
678
+ onReorder={(from, to) => handleReorder(displayItems, from, to)}
679
+ />
680
+ {/if}
681
+ </div>
682
+
683
+ <TablePagination
684
+ pageIndex={viewState.pageIndex}
685
+ pageSize={viewState.pageSize}
686
+ {pageCount}
687
+ {totalItems}
688
+ onPageChange={(p) => (viewState.pageIndex = p)}
689
+ onPageSizeChange={(s) => {
690
+ viewState.pageSize = s;
455
691
  }}
456
- statusFilter={viewState.statusFilter}
457
- onStatusFilterChange={(f) => (viewState.statusFilter = f)}
458
- viewMode={viewState.viewMode}
459
- onViewModeChange={(m) => (viewState.viewMode = m)}
460
- {onCreateEntry}
461
- createLabel={addLabel}
462
692
  />
463
-
464
- {#if totalItems === 0 && !searchQuery}
465
- <EmptyState
466
- title={lang[interfaceLanguage.current].emptyTitle}
467
- description={lang[interfaceLanguage.current].emptyDescription}
468
- ctaLabel={addLabel}
469
- onCta={onCreateEntry}
470
- />
471
- {:else if viewState.viewMode === 'list'}
472
- <div class="bg-card overflow-hidden rounded-xl border shadow-sm">
473
- {#if useServerPagination && !viewState.statusFilter}
474
- <DataTable
475
- data={displayItems}
476
- {columns}
477
- enableSorting
478
- enableFiltering
479
- enableSelection
480
- enablePagination
481
- manualPagination={true}
482
- {pageCount}
483
- rowCount={totalItems}
484
- sorting={viewState.sorting}
485
- onSortingChange={(s) => (viewState.sorting = s)}
486
- {rowSelection}
487
- onRowSelectionChange={(s) => (rowSelection = s)}
488
- pagination={{ pageIndex: viewState.pageIndex, pageSize: viewState.pageSize }}
489
- onPaginationChange={(p) => {
490
- viewState.pageIndex = p.pageIndex;
491
- }}
492
- tableRef={(t) => (tableInstance = t)}
493
- />
494
- {:else}
495
- <DataTable
496
- data={displayItems}
497
- {columns}
498
- enableSorting
499
- enableFiltering
500
- enableSelection
501
- enablePagination
502
- sorting={viewState.sorting}
503
- onSortingChange={(s) => (viewState.sorting = s)}
504
- {rowSelection}
505
- onRowSelectionChange={(s) => (rowSelection = s)}
506
- pagination={{ pageIndex: viewState.pageIndex, pageSize: viewState.pageSize }}
507
- onPaginationChange={(p) => {
508
- viewState.pageIndex = p.pageIndex;
509
- }}
510
- tableRef={(t) => (tableInstance = t)}
511
- />
512
- {/if}
513
- </div>
514
-
515
- <TablePagination
516
- pageIndex={viewState.pageIndex}
517
- pageSize={viewState.pageSize}
518
- {pageCount}
519
- {totalItems}
520
- onPageChange={(p) => (viewState.pageIndex = p)}
521
- onPageSizeChange={(s) => {
522
- viewState.pageSize = s;
523
- }}
524
- />
525
- {:else}
526
- {@const gridItems = displayItems.map((item) => ({
527
- id: item.id,
528
- name: item.name,
529
- slug: item.slug,
530
- status: item.status,
531
- url: item.url,
532
- thumbnail: item.thumbnail,
533
- updatedAt: item.updatedAt
534
- }))}
535
-
536
- <GridView
537
- items={gridItems}
538
- enableSelection
539
- selectedIds={new Set(selectedIndices.map((idx) => displayItems[idx]?.id).filter(Boolean))}
540
- onSelectionChange={(ids) => {
541
- const newSelection: RowSelectionState = {};
542
- for (const id of ids) {
543
- const idx = displayItems.findIndex((i) => i.id === id);
544
- if (idx !== -1) {
545
- newSelection[idx] = true;
546
- }
693
+ {:else}
694
+ {@const gridItems = displayItems.map((item) => ({
695
+ id: item.id,
696
+ name: item.name,
697
+ slug: item.slug,
698
+ status: item.status,
699
+ url: item.url,
700
+ thumbnail: item.thumbnail,
701
+ updatedAt: item.updatedAt
702
+ }))}
703
+
704
+ <GridView
705
+ items={gridItems}
706
+ enableSelection
707
+ selectedIds={new Set(selectedIndices.map((idx) => displayItems[idx]?.id).filter(Boolean))}
708
+ onSelectionChange={(ids) => {
709
+ const newSelection: RowSelectionState = {};
710
+ for (const id of ids) {
711
+ const idx = displayItems.findIndex((i) => i.id === id);
712
+ if (idx !== -1) {
713
+ newSelection[idx] = true;
547
714
  }
548
- rowSelection = newSelection;
549
- }}
550
- />
551
-
552
- <TablePagination
553
- pageIndex={viewState.pageIndex}
554
- pageSize={viewState.pageSize}
555
- {pageCount}
556
- {totalItems}
557
- onPageChange={(p) => (viewState.pageIndex = p)}
558
- onPageSizeChange={(s) => {
559
- viewState.pageSize = s;
560
- }}
561
- />
562
- {/if}
563
-
564
- <BulkActionsBar
565
- {selectedCount}
566
- onArchive={() => handleBulkArchive(displayItems)}
567
- onDelete={() => handleBulkDelete(displayItems)}
568
- onClear={() => (rowSelection = {})}
569
- onRestore={isArchivedFilter ? () => handleBulkRestore(displayItems) : undefined}
715
+ }
716
+ rowSelection = newSelection;
717
+ }}
718
+ />
719
+
720
+ <TablePagination
721
+ pageIndex={viewState.pageIndex}
722
+ pageSize={viewState.pageSize}
723
+ {pageCount}
724
+ {totalItems}
725
+ onPageChange={(p) => (viewState.pageIndex = p)}
726
+ onPageSizeChange={(s) => {
727
+ viewState.pageSize = s;
728
+ }}
570
729
  />
571
- {/await}
572
- {/key}
730
+ {/if}
731
+
732
+ <BulkActionsBar
733
+ {selectedCount}
734
+ onArchive={() => handleBulkArchive(displayItems)}
735
+ onDelete={isArchivedFilter ? () => handleBulkDelete(displayItems) : undefined}
736
+ onClear={() => (rowSelection = {})}
737
+ onRestore={isArchivedFilter ? () => handleBulkRestore(displayItems) : undefined}
738
+ />
739
+ </div>
740
+ {/if}
573
741
 
574
742
  <AlertDialog.Root bind:open={deleteDialogOpen}>
575
743
  <AlertDialog.Content>
@@ -9,8 +9,11 @@
9
9
  let { data }: Props = $props();
10
10
 
11
11
  const collectionSlug = $derived(data?.collectionSlug || page.params.collection || '');
12
+ const query = $derived(remotes.getCollection(collectionSlug));
12
13
  </script>
13
14
 
14
- {#await remotes.getCollection(collectionSlug) then collection}
15
- <Collection {collection} />
16
- {/await}
15
+ {#if query.ready && query.current}
16
+ <div class:opacity-60={query.loading} class="transition-opacity duration-200">
17
+ <Collection collection={query.current} />
18
+ </div>
19
+ {/if}