feat: merge books from the duplicates screen and the toolbar
A workbench with the folded records in a rail, the survivor's fields live and editable beside them, and per-field actions chosen by what kind of field it is. Fields the records agree on stay out of the way. Reachable from a duplicate group and from the selection toolbar, which is the only way to merge a pair the detector never proposed.
This commit is contained in:
+13
-5
@@ -1,10 +1,18 @@
|
||||
import { listDuplicateBooks } from '$lib/api/book.remote.js';
|
||||
import { listBooks, listDuplicateBooks } from '$lib/api/book.remote.js';
|
||||
|
||||
export async function load({ params, depends }) {
|
||||
// Dismissing a group re-runs this, so the card leaves the screen.
|
||||
// Dismissing or merging a group re-runs this, so the card leaves the screen.
|
||||
depends('app:duplicate-books');
|
||||
|
||||
return {
|
||||
groups: await listDuplicateBooks(params.libraryId)
|
||||
};
|
||||
const groups = await listDuplicateBooks(params.libraryId);
|
||||
|
||||
// The groups carry only enough to render a card. Merging needs the whole record —
|
||||
// identifiers, description, publisher — so fetch them in one go rather than per
|
||||
// card, and let the dialog pick out the books for its own group.
|
||||
const ids = [...new Set(groups.flatMap((group) => group.books.map((book) => book.book_id)))];
|
||||
const books = ids.length
|
||||
? await listBooks({ ids, pageSize: ids.length })
|
||||
: { items: [] };
|
||||
|
||||
return { groups, books: books.items };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { resolve } from '$app/paths';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -10,9 +11,20 @@
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import BookImage from '$lib/components/view/book-image.svelte';
|
||||
import { dismissDuplicateBooks } from '$lib/api/book.remote';
|
||||
import type { DuplicateBookGroup } from '$lib/schema';
|
||||
import MergeBooks from '$lib/components/forms/merge-books/merge-books.svelte';
|
||||
import type { Book, DuplicateBookGroup } from '$lib/schema';
|
||||
|
||||
let { data }: { data: { groups: DuplicateBookGroup[] } } = $props();
|
||||
let { data }: { data: { groups: DuplicateBookGroup[]; books: Book[] } } = $props();
|
||||
|
||||
// The group being merged, or null when the dialog is closed.
|
||||
let merging = $state<DuplicateBookGroup | null>(null);
|
||||
|
||||
/** The full records behind a group, in the order the group lists them. */
|
||||
function recordsFor(group: DuplicateBookGroup): Book[] {
|
||||
return group.books
|
||||
.map(({ book_id }) => data.books.find((book) => book.id === book_id))
|
||||
.filter((book): book is Book => book !== undefined);
|
||||
}
|
||||
|
||||
// Which groups are being dismissed, so a slow round trip cannot be started twice.
|
||||
let dismissing = $state<number[]>([]);
|
||||
@@ -86,7 +98,7 @@
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{group.books.length} books look like the same book</Card.Title>
|
||||
<Card.Action>
|
||||
<Card.Action class="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -95,6 +107,15 @@
|
||||
>
|
||||
Not duplicates
|
||||
</Button>
|
||||
<!-- Disabled until every record in the group came back, so the
|
||||
dialog can never open on a partial group. -->
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={busy || recordsFor(group).length !== group.books.length}
|
||||
onclick={() => (merging = group)}
|
||||
>
|
||||
Merge…
|
||||
</Button>
|
||||
</Card.Action>
|
||||
</Card.Header>
|
||||
|
||||
@@ -147,3 +168,18 @@
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Keyed on the group so a second merge starts from that group's records rather
|
||||
than the previous one's draft, the same way the edit dialog keys on the book.
|
||||
-->
|
||||
{#if merging}
|
||||
{#key merging.books[0].book_id}
|
||||
<MergeBooks
|
||||
books={recordsFor(merging)}
|
||||
libraryId={page.params.libraryId!}
|
||||
open={true}
|
||||
onmerged={() => (merging = null)}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user