From 142e8494730bd215b752b159a6441813268476ec Mon Sep 17 00:00:00 2001 From: codebude Date: Fri, 12 Jun 2026 22:45:09 +0200 Subject: [PATCH 1/3] Pre-select field to be changed on data hygiene page in case if only one field is selected --- frontend/src/routes/data-hygiene/+page.svelte | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/frontend/src/routes/data-hygiene/+page.svelte b/frontend/src/routes/data-hygiene/+page.svelte index 6c85d276..d144eb6b 100644 --- a/frontend/src/routes/data-hygiene/+page.svelte +++ b/frontend/src/routes/data-hygiene/+page.svelte @@ -32,6 +32,7 @@ let selectedBookIds = $state>(new Set()); let batchField = $state(null); let batchValue = $state(''); + let batchFieldWasAutoSelected = $state(false); let showBatchConfirm = $state(false); let batchUpdating = $state(false); let dataLoaded = $state(false); @@ -177,6 +178,25 @@ } return [...set]; }); + + $effect(() => { + if (missingAttrsOfSelected.length === 1) { + batchField = missingAttrsOfSelected[0]; + batchFieldWasAutoSelected = true; + return; + } + + if (missingAttrsOfSelected.length > 1 && batchFieldWasAutoSelected) { + batchField = null; + batchFieldWasAutoSelected = false; + return; + } + + if (batchField && !missingAttrsOfSelected.includes(batchField)) { + batchField = null; + batchFieldWasAutoSelected = false; + } + });
@@ -325,6 +345,7 @@ (batchFieldWasAutoSelected = false)} aria-label={$_('dataHygiene.batchFieldLabel')} From f4a92f720c4b6ab202b3de9ec6cf6bccc9902b92 Mon Sep 17 00:00:00 2001 From: codebude Date: Fri, 12 Jun 2026 23:05:36 +0200 Subject: [PATCH 3/3] Added book details and cover action buttons to data hygiene page --- frontend/src/lib/i18n/locales/de.json | 7 + frontend/src/lib/i18n/locales/en.json | 7 + frontend/src/lib/i18n/locales/es.json | 7 + frontend/src/lib/i18n/locales/fr.json | 7 + frontend/src/lib/i18n/locales/zh.json | 7 + frontend/src/routes/data-hygiene/+page.svelte | 140 ++++++++++++++++-- 6 files changed, 166 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json index 66b80150..fa79860f 100644 --- a/frontend/src/lib/i18n/locales/de.json +++ b/frontend/src/lib/i18n/locales/de.json @@ -605,6 +605,13 @@ "allSet": "Deine Bibliothek ist in großartiger Verfassung! Alle Bücher haben vollständige Metadaten.", "allSetFiltered": "Deine Bibliothek ist in großartiger Verfassung! Alle Bücher haben vollständige Metadaten für die ausgewählten Attribute.", "tableHeaderMissing": "Fehlend", + "actions": "Aktionen", + "openDetails": "Details öffnen", + "detailsShort": "Details", + "viewCover": "Cover ansehen", + "coverShort": "Cover", + "noCover": "Kein Cover", + "loadBookDetailsFailed": "Buchdetails konnten nicht geladen werden.", "remaining": "übrig", "andXMore": "...und {count} weitere" }, diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index cd1162ad..4575a935 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -605,6 +605,13 @@ "allSet": "Your library is in great shape! All books have complete metadata.", "allSetFiltered": "Your library is in great shape! All books have complete metadata for the selected attributes.", "tableHeaderMissing": "Missing", + "actions": "Actions", + "openDetails": "Open details", + "detailsShort": "Details", + "viewCover": "View cover", + "coverShort": "Cover", + "noCover": "No cover", + "loadBookDetailsFailed": "Failed to load book details.", "remaining": "remaining", "andXMore": "...and {count} more" }, diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json index d51595c3..24c6d705 100644 --- a/frontend/src/lib/i18n/locales/es.json +++ b/frontend/src/lib/i18n/locales/es.json @@ -605,6 +605,13 @@ "allSet": "¡Tu biblioteca está en excelente estado! Todos los libros tienen metadatos completos.", "allSetFiltered": "¡Tu biblioteca está en excelente estado! Todos los libros tienen metadatos completos para los atributos seleccionados.", "tableHeaderMissing": "Faltante", + "actions": "Acciones", + "openDetails": "Abrir detalles", + "detailsShort": "Detalles", + "viewCover": "Ver portada", + "coverShort": "Portada", + "noCover": "Sin portada", + "loadBookDetailsFailed": "No se pudieron cargar los detalles del libro.", "remaining": "restantes", "andXMore": "...y {count} más" }, diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json index cfbc08c5..6cdd3610 100644 --- a/frontend/src/lib/i18n/locales/fr.json +++ b/frontend/src/lib/i18n/locales/fr.json @@ -605,6 +605,13 @@ "allSet": "Ta bibliothèque est en pleine forme ! Tous les livres ont des métadonnées complètes.", "allSetFiltered": "Ta bibliothèque est en pleine forme ! Tous les livres ont des métadonnées complètes pour les attributs sélectionnés.", "tableHeaderMissing": "Manquant", + "actions": "Actions", + "openDetails": "Ouvrir les détails", + "detailsShort": "Détails", + "viewCover": "Voir la couverture", + "coverShort": "Couverture", + "noCover": "Pas de couverture", + "loadBookDetailsFailed": "Impossible de charger les détails du livre.", "remaining": "restants", "andXMore": "...et {count} autres" }, diff --git a/frontend/src/lib/i18n/locales/zh.json b/frontend/src/lib/i18n/locales/zh.json index 6b3a8809..d47d6c47 100644 --- a/frontend/src/lib/i18n/locales/zh.json +++ b/frontend/src/lib/i18n/locales/zh.json @@ -605,6 +605,13 @@ "allSet": "你的书库状态良好!所有图书元数据完整。", "allSetFiltered": "你的书库状态良好!所选属性的所有图书元数据完整。", "tableHeaderMissing": "缺失", + "actions": "操作", + "openDetails": "打开详情", + "detailsShort": "详情", + "viewCover": "查看封面", + "coverShort": "封面", + "noCover": "无封面", + "loadBookDetailsFailed": "加载图书详情失败。", "remaining": "剩余", "andXMore": "...还有 {count} 个" }, diff --git a/frontend/src/routes/data-hygiene/+page.svelte b/frontend/src/routes/data-hygiene/+page.svelte index 1aa0bdd4..31e7dfe8 100644 --- a/frontend/src/routes/data-hygiene/+page.svelte +++ b/frontend/src/routes/data-hygiene/+page.svelte @@ -5,8 +5,10 @@ import { toasts } from '$lib/toasts'; import { localizeError } from '$lib/errors'; import Alert from '$lib/components/Alert.svelte'; - import { LoaderCircle } from '@lucide/svelte'; - import type { HygieneAttribute, HygieneMissingBook } from '$lib/types'; + import BookDetailDialog from '$lib/components/BookDetailDialog.svelte'; + import BookDrawer from '$lib/components/BookDrawer.svelte'; + import { LoaderCircle, X } from '@lucide/svelte'; + import type { Book, HygieneAttribute, HygieneMissingBook } from '$lib/types'; const ATTRIBUTES: { key: HygieneAttribute; labelKey: string }[] = [ { key: 'author', labelKey: 'dataHygiene.attributes.author' }, @@ -37,6 +39,11 @@ let batchUpdating = $state(false); let dataLoaded = $state(false); let hasMore = $state(false); + let selectedBook = $state(null); + let detailOpen = $state(false); + let drawerOpen = $state(false); + let detailLoadingBookId = $state(null); + let coverViewer = $state<{ title: string; coverUrl: string } | null>(null); const effectiveAttributes = $derived( selectedAttributes.length > 0 ? selectedAttributes : ATTRIBUTES.map(a => a.key) @@ -164,6 +171,51 @@ void loadData(false); } + async function openBookDetails(book: HygieneMissingBook) { + detailLoadingBookId = book.id; + try { + selectedBook = await api.books.get(book.id); + detailOpen = true; + drawerOpen = false; + } catch (e: unknown) { + toasts.add(localizeError(e, $_, $_('dataHygiene.loadBookDetailsFailed')), 'error'); + } finally { + detailLoadingBookId = null; + } + } + + function openEditFromDetail(book: Book) { + selectedBook = book; + detailOpen = false; + drawerOpen = true; + } + + async function handleSave(updated: Book) { + selectedBook = updated; + detailOpen = false; + drawerOpen = false; + await loadData(true); + } + + function handleDelete(id: number) { + detailOpen = false; + drawerOpen = false; + selectedBookIds = new Set([...selectedBookIds].filter(bookId => bookId !== id)); + void loadData(true); + } + + function openCoverViewer(book: HygieneMissingBook) { + if (!book.cover_url) return; + coverViewer = { + title: book.title, + coverUrl: book.cover_url, + }; + } + + function closeCoverViewer() { + coverViewer = null; + } + const allComplete = $derived( dataLoaded && total === 0 && Object.values(totalMissingPerAttribute).every(c => c === 0) ); @@ -262,7 +314,7 @@
{$_('dataHygiene.sectionResults')}
-
+
@@ -276,10 +328,11 @@ /> - - - + + + + @@ -297,9 +350,9 @@ - - - + + + + {/each} @@ -385,6 +473,37 @@ {/if} +{#if coverViewer} +
e.key === 'Escape' && closeCoverViewer()} + >
+
+
+
+ +
{coverViewer.title}
+
+ {$_('book.coverOf', +
+
+
+
+{/if} + + + +
{$_('book.title')} {$_('dataHygiene.tableHeaderMissing')}{$_('dataHygiene.actions')}
{book.title}
{#each book.missing_attributes as attr} @@ -307,6 +360,41 @@ {/each}
+
+ + +
+