fix(ui): custom confirm dialog, picker visibility, remove wiring

Three fixes for the collection detail page:

1. Picker modal never showed because the outer overlay div had
   style="display:none" with no x-show binding — add x-show bound
   to $store.bookPicker.isOpen plus a backdrop and click-to-close.

2. Replace native confirm() with an in-page Alpine modal for UI
   continuity. Add confirm dialog state (showConfirm, confirmMessage,
   pendingAction) and methods (requestRemoveBook, requestBulkRemove,
   executeConfirmed, closeConfirm) to the collections component.
   The actual API calls (doRemoveBook/doBulkRemove) are triggered only
   when the user confirms.

3. Rename removeBook → requestRemoveBook and bulkRemove →
   requestBulkRemove in template + JS renderer so the dialog opens
   instead of navigating or failing silently.
This commit is contained in:
2026-08-06 10:55:37 -04:00
parent 816ee0ec80
commit ef3c05714a
3 changed files with 98 additions and 51 deletions
+36 -21
View File
@@ -159,7 +159,7 @@ function renderCollectionBooks(books: BookInfo[]): void {
</div>
</div>
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
<button @click="removeBook('${book.media_item_id}')" class="btn btn-secondary w-full">
<button @click="requestRemoveBook('${book.media_item_id}')" class="btn btn-secondary w-full">
<svg class="h-4 w-4 inline" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path></svg>
Remove from Collection
</button>
@@ -577,8 +577,7 @@ function toggleSelection(this: any, id: string): void {
}
}
async function removeBook(id: string): Promise<void> {
if (!confirm("Remove this book from the collection?")) return;
async function doRemoveBook(id: string): Promise<void> {
const token = localStorage.getItem("token");
if (!token) return;
try {
@@ -600,14 +599,7 @@ async function removeBook(id: string): Promise<void> {
}
}
async function bulkRemove(this: any): Promise<void> {
if (this.selectedBooks.length === 0) return;
if (
!confirm(
`Remove ${this.selectedBooks.length} book(s) from this collection?`,
)
)
return;
async function doBulkRemove(ids: string[]): Promise<void> {
const token = localStorage.getItem("token");
if (!token) return;
try {
@@ -619,15 +611,11 @@ async function bulkRemove(this: any): Promise<void> {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ book_ids: this.selectedBooks }),
body: JSON.stringify({ book_ids: ids }),
},
);
if (response.ok) {
showToast(
`Removed ${this.selectedBooks.length} book(s)`,
"success",
);
this.selectedBooks = [];
showToast(`Removed ${ids.length} book(s)`, "success");
setTimeout(() => location.reload(), 500);
} else {
showToast("Failed to remove books", "error");
@@ -655,7 +643,6 @@ function filterCollectionBooks(): void {
}
export {
bulkRemove,
closeCollectionModal,
createRule,
deleteRule,
@@ -668,7 +655,6 @@ export {
loadCollections,
navigateToCollection,
populateIconGrid,
removeBook,
selectColor,
selectIcon,
setupHTMXAuth,
@@ -691,7 +677,6 @@ Alpine.data("collections", () => ({
loadCollections,
navigateToCollection,
populateIconGrid,
removeBook,
selectColor,
selectIcon,
selectedBooks: [] as string[],
@@ -699,5 +684,35 @@ Alpine.data("collections", () => ({
showAllIcons,
testRule,
toggleSelection,
bulkRemove,
showConfirm: false,
confirmMessage: "",
confirmLabel: "Remove",
pendingAction: null as null | (() => void),
requestRemoveBook(this: any, id: string) {
this.confirmMessage = "Remove this book from the collection?";
this.pendingAction = () => doRemoveBook(id);
this.showConfirm = true;
},
requestBulkRemove(this: any) {
if (this.selectedBooks.length === 0) return;
this.confirmMessage = `Remove ${this.selectedBooks.length} book(s) from this collection?`;
const ids = [...this.selectedBooks];
this.pendingAction = () => doBulkRemove(ids);
this.showConfirm = true;
},
executeConfirmed(this: any) {
this.showConfirm = false;
const action = this.pendingAction;
this.pendingAction = null;
if (action) action();
},
closeConfirm(this: any) {
this.showConfirm = false;
this.pendingAction = null;
},
}));