fix(alpine): wrap all Alpine.data() callbacks in arrow functions for proper component initialization
- Wrap all Alpine.data() object literals in arrow functions (() => ({}))
- This fixes "n.bind is not a function" errors when Alpine initializes components
- Alpine.data() requires a factory function, not a plain object
- Ensures each component instance gets its own closure and proper this binding
Fixed 24 TypeScript files:
- admin.ts, analytics.ts, api.ts, api-explorer-docs.ts
- bookshelf.ts, collection-rules.ts, collections.ts, conflicts.ts
- device-management.ts, docs.ts, header.ts, index.ts
- library.ts, linking.ts, login.ts, password_validation.ts
- profile-modal.ts, profile.ts, queue.ts, register.ts
- search.ts, theme.ts, toast-error.ts, toast.ts, unlinked_books.ts
Before: Alpine.data("name", { method1, method2 })
After: Alpine.data("name", () => ({ method1, method2 }))
This is a critical fix for Alpine.js v3+ where components must be
registered as factory functions to ensure proper reactivity and
prevent binding errors during initialization.
This commit is contained in:
@@ -126,6 +126,6 @@ document.addEventListener("DOMContentLoaded", loadAnalytics);
|
||||
|
||||
export { loadAnalytics };
|
||||
|
||||
Alpine.store("analytics", {
|
||||
Alpine.data("analytics", () => ({
|
||||
loadAnalytics,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -99,7 +99,7 @@ export {
|
||||
handleError,
|
||||
};
|
||||
|
||||
Alpine.store("api", {
|
||||
Alpine.data("api", () => ({
|
||||
get: apiGet,
|
||||
post: apiPost,
|
||||
put: apiPut,
|
||||
@@ -108,4 +108,4 @@ Alpine.store("api", {
|
||||
handleResponse: handleResponse,
|
||||
handleVoidResponse: handleVoidResponse,
|
||||
handleError: handleError,
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -7,7 +7,9 @@ let mediaItems: unknown[] = [];
|
||||
function initBookshelf(): void {
|
||||
const savedLibrary = localStorage.getItem("selectedLibrary");
|
||||
if (savedLibrary) {
|
||||
const select = document.getElementById("library-select") as HTMLSelectElement;
|
||||
const select = document.getElementById(
|
||||
"library-select",
|
||||
) as HTMLSelectElement;
|
||||
if (select && select.value) {
|
||||
currentLibraryId = savedLibrary;
|
||||
loadBookshelf(savedLibrary);
|
||||
@@ -196,7 +198,7 @@ export {
|
||||
viewBook,
|
||||
};
|
||||
|
||||
Alpine.store("bookshelf", {
|
||||
Alpine.data("bookshelf", () => ({
|
||||
changePage,
|
||||
initBookshelf,
|
||||
loadBookshelf,
|
||||
@@ -204,4 +206,4 @@ Alpine.store("bookshelf", {
|
||||
selectLibrary,
|
||||
setupEventDelegation,
|
||||
viewBook,
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -251,7 +251,8 @@ async function testRule(): Promise<void> {
|
||||
'<p style="color: var(--text-secondary); font-size: 0.875rem;">Found ' +
|
||||
result.matches.length +
|
||||
" matching books:</p>";
|
||||
html += '<div style="display: flex; flex-direction: column; gap: 0.5rem;">';
|
||||
html +=
|
||||
'<div style="display: flex; flex-direction: column; gap: 0.5rem;">';
|
||||
|
||||
result.matches.slice(0, 20).forEach((book: TestRuleMatch) => {
|
||||
html +=
|
||||
@@ -408,7 +409,7 @@ export {
|
||||
toggleRule,
|
||||
};
|
||||
|
||||
Alpine.store("collectionRules", {
|
||||
Alpine.data("collectionRules", () => ({
|
||||
backToCollection,
|
||||
clearForm,
|
||||
deleteRule,
|
||||
@@ -422,4 +423,4 @@ Alpine.store("collectionRules", {
|
||||
setupEventDelegation,
|
||||
testRule,
|
||||
toggleRule,
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -216,7 +216,7 @@ export {
|
||||
showResolveModal,
|
||||
};
|
||||
|
||||
Alpine.store("conflicts", {
|
||||
Alpine.data("conflicts", () => ({
|
||||
bulkDismiss,
|
||||
bulkResolve,
|
||||
dismissAllResolved,
|
||||
@@ -225,4 +225,4 @@ Alpine.store("conflicts", {
|
||||
refreshConflicts,
|
||||
resolveConflict,
|
||||
showResolveModal,
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -534,7 +534,10 @@ async function handleSaveDeviceSettings(event: Event): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyToClipboard(text: string, description: string): Promise<void> {
|
||||
async function copyToClipboard(
|
||||
text: string,
|
||||
description: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
showToast(`${description} copied!`, "success");
|
||||
@@ -693,7 +696,7 @@ export {
|
||||
showShelfMappings,
|
||||
};
|
||||
|
||||
Alpine.store("devices", {
|
||||
Alpine.data("devices", () => ({
|
||||
approveDevice,
|
||||
clearSyncQueue,
|
||||
copyToClipboard,
|
||||
@@ -716,4 +719,4 @@ Alpine.store("devices", {
|
||||
showAddMappingModal,
|
||||
showDeviceSettings,
|
||||
showShelfMappings,
|
||||
});
|
||||
}));
|
||||
|
||||
+4
-4
@@ -93,14 +93,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
initializeDocsSearch();
|
||||
});
|
||||
|
||||
Alpine.store("docs", {
|
||||
Alpine.data("docs", () => ({
|
||||
toggleSidebar,
|
||||
initializeSearch: initializeDocsSearch,
|
||||
});
|
||||
}));
|
||||
|
||||
export { toggleSidebar, initializeDocsSearch };
|
||||
|
||||
Alpine.store("docs", {
|
||||
Alpine.data("docs", () => ({
|
||||
toggleSidebar,
|
||||
initializeSearch: initializeDocsSearch,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -10,6 +10,6 @@ const logout = (): void => {
|
||||
|
||||
export { logout };
|
||||
|
||||
Alpine.store("header", {
|
||||
Alpine.data("header", () => ({
|
||||
logout,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ async function checkAuthRedirect(): Promise<void> {
|
||||
|
||||
export { changeTheme, checkAuthRedirect, initIndexTheme };
|
||||
|
||||
Alpine.store("index", {
|
||||
Alpine.data("index", () => ({
|
||||
changeTheme,
|
||||
checkAuthRedirect,
|
||||
initIndexTheme,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -680,7 +680,7 @@ export {
|
||||
confirmDeleteLibrary,
|
||||
};
|
||||
|
||||
Alpine.store("library", {
|
||||
Alpine.data("library", () => ({
|
||||
deleteLibrary,
|
||||
showLibraryFolders,
|
||||
addLibraryFolder,
|
||||
@@ -696,4 +696,4 @@ Alpine.store("library", {
|
||||
showDeleteModal,
|
||||
hideDeleteModal,
|
||||
confirmDeleteLibrary,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -201,11 +201,11 @@ export {
|
||||
showSuggestionsModal,
|
||||
};
|
||||
|
||||
Alpine.store("linking", {
|
||||
Alpine.data("linking", () => ({
|
||||
autoLinkBooks,
|
||||
getSuggestions,
|
||||
hideMatchModal,
|
||||
linkBook,
|
||||
loadUnlinkedBooks,
|
||||
showSuggestionsModal,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ function changeTheme(): void {
|
||||
|
||||
export { changeTheme, initLoginTheme };
|
||||
|
||||
Alpine.store("login", {
|
||||
Alpine.data("login", () => ({
|
||||
changeTheme,
|
||||
initLoginTheme,
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -189,6 +189,6 @@ function initPasswordValidation(): void {
|
||||
// Export for use in template
|
||||
export { initPasswordValidation };
|
||||
|
||||
Alpine.store("validation", {
|
||||
Alpine.data("validation", () => ({
|
||||
initPasswordValidation,
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -26,7 +26,7 @@ function setupProfileModal(): void {
|
||||
|
||||
export { closeProfileModal, setupProfileModal };
|
||||
|
||||
Alpine.store("profileModal", {
|
||||
Alpine.data("profileModal", () => ({
|
||||
closeProfileModal,
|
||||
setupProfileModal,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -41,6 +41,6 @@ async function confirmDeleteAccount(): Promise<void> {
|
||||
|
||||
export { confirmDeleteAccount };
|
||||
|
||||
Alpine.store("profile", {
|
||||
Alpine.data("profile", () => ({
|
||||
confirmDeleteAccount,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -191,7 +191,7 @@ export {
|
||||
filterQueue,
|
||||
};
|
||||
|
||||
Alpine.store("queue", {
|
||||
Alpine.data("queue", () => ({
|
||||
clearAllItems,
|
||||
clearFailedItems,
|
||||
deleteQueueItem,
|
||||
@@ -201,4 +201,4 @@ Alpine.store("queue", {
|
||||
showQueueItemModal,
|
||||
hideQueueItemModal,
|
||||
filterQueue,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -11,6 +11,6 @@ function initRegisterTheme(): void {
|
||||
|
||||
export { initRegisterTheme };
|
||||
|
||||
Alpine.store("register", {
|
||||
Alpine.data("register", () => ({
|
||||
initRegisterTheme,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -307,6 +307,6 @@ document.addEventListener("DOMContentLoaded", initializeSearch);
|
||||
|
||||
export { selectLibraryAndBook };
|
||||
|
||||
Alpine.store("search", {
|
||||
Alpine.data("search", () => ({
|
||||
selectLibraryAndBook,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -217,10 +217,10 @@ if (typeof document !== "undefined") {
|
||||
}
|
||||
}
|
||||
|
||||
Alpine.store("theme", {
|
||||
Alpine.data("theme", () => ({
|
||||
changeTheme,
|
||||
changeWoodPaneling,
|
||||
});
|
||||
}));
|
||||
|
||||
export {
|
||||
applyTheme,
|
||||
|
||||
@@ -30,6 +30,6 @@ function showErrorToast(message: string): void {
|
||||
|
||||
export { showErrorToast };
|
||||
|
||||
Alpine.store("toastError", {
|
||||
Alpine.data("toastError", () => ({
|
||||
showErrorToast,
|
||||
});
|
||||
}));
|
||||
|
||||
+2
-2
@@ -226,14 +226,14 @@ if (typeof document !== "undefined") {
|
||||
}
|
||||
|
||||
// Export toast API for manual use
|
||||
Alpine.store("showToast", {
|
||||
Alpine.data("showToast", () => ({
|
||||
error: (message: string, duration?: number) =>
|
||||
showToast(message, "error", duration),
|
||||
success: (message: string, duration?: number) =>
|
||||
showToast(message, "success", duration),
|
||||
info: (message: string, duration?: number) =>
|
||||
showToast(message, "info", duration),
|
||||
});
|
||||
}));
|
||||
|
||||
export { showToast };
|
||||
export type { ToastType };
|
||||
|
||||
+99
-35
@@ -4,14 +4,19 @@ import { getToken } from "./storage";
|
||||
|
||||
let selectedMediaItem: string | null = null;
|
||||
|
||||
function searchMatches(progressId: string, sha256: string, title: string): void {
|
||||
function searchMatches(
|
||||
progressId: string,
|
||||
sha256: string,
|
||||
title: string,
|
||||
): void {
|
||||
const container = document.getElementById(`matches-${progressId}`);
|
||||
const matchesList = document.getElementById(`matches-list-${progressId}`);
|
||||
|
||||
if (!container || !matchesList) return;
|
||||
|
||||
container.classList.remove("hidden");
|
||||
matchesList.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Searching...</p>';
|
||||
matchesList.innerHTML =
|
||||
'<p class="text-sm" style="color: var(--text-secondary)">Searching...</p>';
|
||||
|
||||
const token = getToken();
|
||||
const url = sha256
|
||||
@@ -51,19 +56,27 @@ function searchMatches(progressId: string, sha256: string, title: string): void
|
||||
)
|
||||
.join("");
|
||||
} else {
|
||||
matchesList.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">No matches found. Try manual linking.</p>';
|
||||
matchesList.innerHTML =
|
||||
'<p class="text-sm" style="color: var(--text-secondary)">No matches found. Try manual linking.</p>';
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to search", error);
|
||||
matchesList.innerHTML = '<p class="text-sm" style="color: var(--error)">Failed to search</p>';
|
||||
matchesList.innerHTML =
|
||||
'<p class="text-sm" style="color: var(--error)">Failed to search</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function autoLinkBook(progressId: string, mediaItemId: string, confidence: number): void {
|
||||
function autoLinkBook(
|
||||
progressId: string,
|
||||
mediaItemId: string,
|
||||
confidence: number,
|
||||
): void {
|
||||
if (
|
||||
!confirm(
|
||||
"Link this book? The confidence score is " + Math.round(confidence * 100) + "%",
|
||||
"Link this book? The confidence score is " +
|
||||
Math.round(confidence * 100) +
|
||||
"%",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -104,20 +117,28 @@ function autoLinkBook(progressId: string, mediaItemId: string, confidence: numbe
|
||||
|
||||
function showManualLinkModal(progressId: string, bookTitle: string): void {
|
||||
const modal = document.getElementById("manual-link-modal");
|
||||
const progressIdInput = document.getElementById("link-progress-id") as HTMLInputElement;
|
||||
const bookTitleInput = document.getElementById("link-book-title") as HTMLInputElement;
|
||||
const progressIdInput = document.getElementById(
|
||||
"link-progress-id",
|
||||
) as HTMLInputElement;
|
||||
const bookTitleInput = document.getElementById(
|
||||
"link-book-title",
|
||||
) as HTMLInputElement;
|
||||
const searchResults = document.getElementById("link-search-results");
|
||||
|
||||
if (modal) modal.classList.remove("hidden");
|
||||
if (progressIdInput) progressIdInput.value = progressId;
|
||||
if (bookTitleInput) bookTitleInput.value = bookTitle;
|
||||
if (searchResults) searchResults.innerHTML = '<p style="color: var(--text-secondary)">Search for books to link</p>';
|
||||
if (searchResults)
|
||||
searchResults.innerHTML =
|
||||
'<p style="color: var(--text-secondary)">Search for books to link</p>';
|
||||
selectedMediaItem = null;
|
||||
}
|
||||
|
||||
function hideManualLinkModal(): void {
|
||||
const modal = document.getElementById("manual-link-modal");
|
||||
const searchInput = document.getElementById("link-search-input") as HTMLInputElement;
|
||||
const searchInput = document.getElementById(
|
||||
"link-search-input",
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (modal) modal.classList.add("hidden");
|
||||
if (searchInput) searchInput.value = "";
|
||||
@@ -125,7 +146,9 @@ function hideManualLinkModal(): void {
|
||||
}
|
||||
|
||||
function searchBooksForLink(): void {
|
||||
const searchInput = document.getElementById("link-search-input") as HTMLInputElement;
|
||||
const searchInput = document.getElementById(
|
||||
"link-search-input",
|
||||
) as HTMLInputElement;
|
||||
const resultsContainer = document.getElementById("link-search-results");
|
||||
|
||||
if (!searchInput || !resultsContainer) return;
|
||||
@@ -133,11 +156,13 @@ function searchBooksForLink(): void {
|
||||
const searchTerm = searchInput.value;
|
||||
|
||||
if (searchTerm.length < 2) {
|
||||
resultsContainer.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Enter at least 2 characters</p>';
|
||||
resultsContainer.innerHTML =
|
||||
'<p class="text-sm" style="color: var(--text-secondary)">Enter at least 2 characters</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
resultsContainer.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Searching...</p>';
|
||||
resultsContainer.innerHTML =
|
||||
'<p class="text-sm" style="color: var(--text-secondary)">Searching...</p>';
|
||||
|
||||
const token = getToken();
|
||||
fetch(`/api/books/match?title=${encodeURIComponent(searchTerm)}`, {
|
||||
@@ -168,16 +193,22 @@ function searchBooksForLink(): void {
|
||||
)
|
||||
.join("");
|
||||
} else {
|
||||
resultsContainer.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">No matches found</p>';
|
||||
resultsContainer.innerHTML =
|
||||
'<p class="text-sm" style="color: var(--text-secondary)">No matches found</p>';
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to search", error);
|
||||
resultsContainer.innerHTML = '<p class="text-sm" style="color: var(--error)">Failed to search</p>';
|
||||
resultsContainer.innerHTML =
|
||||
'<p class="text-sm" style="color: var(--error)">Failed to search</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function selectBookForLink(mediaItemId: string, title: string, _coverPath: string): void {
|
||||
function selectBookForLink(
|
||||
mediaItemId: string,
|
||||
title: string,
|
||||
_coverPath: string,
|
||||
): void {
|
||||
selectedMediaItem = mediaItemId;
|
||||
const resultsContainer = document.getElementById("link-search-results");
|
||||
if (!resultsContainer) return;
|
||||
@@ -197,10 +228,18 @@ function confirmManualLink(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const progressIdInput = document.getElementById("link-progress-id") as HTMLInputElement;
|
||||
const confidenceInput = document.getElementById("link-confidence") as HTMLInputElement;
|
||||
const bookTitleInput = document.getElementById("link-book-title") as HTMLInputElement;
|
||||
const sha256Input = document.getElementById("link-book-sha256") as HTMLInputElement;
|
||||
const progressIdInput = document.getElementById(
|
||||
"link-progress-id",
|
||||
) as HTMLInputElement;
|
||||
const confidenceInput = document.getElementById(
|
||||
"link-confidence",
|
||||
) as HTMLInputElement;
|
||||
const bookTitleInput = document.getElementById(
|
||||
"link-book-title",
|
||||
) as HTMLInputElement;
|
||||
const sha256Input = document.getElementById(
|
||||
"link-book-sha256",
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (!progressIdInput) return;
|
||||
|
||||
@@ -240,7 +279,9 @@ function confirmManualLink(): void {
|
||||
}
|
||||
|
||||
function toggleAllUnlinked(): void {
|
||||
const selectAll = document.getElementById("select-all-unlinked") as HTMLInputElement;
|
||||
const selectAll = document.getElementById(
|
||||
"select-all-unlinked",
|
||||
) as HTMLInputElement;
|
||||
if (!selectAll) return;
|
||||
|
||||
document.querySelectorAll(".unlinked-checkbox").forEach((cb) => {
|
||||
@@ -250,7 +291,9 @@ function toggleAllUnlinked(): void {
|
||||
}
|
||||
|
||||
function getSelectedUnlinked(): { progressId: string; title: string }[] {
|
||||
return Array.from(document.querySelectorAll(".unlinked-checkbox:checked")).map((cb) => ({
|
||||
return Array.from(
|
||||
document.querySelectorAll(".unlinked-checkbox:checked"),
|
||||
).map((cb) => ({
|
||||
progressId: cb.getAttribute("data-progress-id") || "",
|
||||
title: cb.getAttribute("data-title") || "",
|
||||
}));
|
||||
@@ -271,7 +314,11 @@ async function bulkAutoLink(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Auto-link ${selected.length} books with high confidence matches (≥80%)?`)) {
|
||||
if (
|
||||
!confirm(
|
||||
`Auto-link ${selected.length} books with high confidence matches (≥80%)?`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -290,7 +337,10 @@ async function bulkAutoLink(): Promise<void> {
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
showToast(`Auto-linked ${result.auto_linked} books successfully`, "success");
|
||||
showToast(
|
||||
`Auto-linked ${result.auto_linked} books successfully`,
|
||||
"success",
|
||||
);
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} catch (error) {
|
||||
console.error("Auto-link failed", error);
|
||||
@@ -308,11 +358,14 @@ async function bulkGetSuggestions(): Promise<void> {
|
||||
const token = getToken();
|
||||
for (const book of selected) {
|
||||
try {
|
||||
const response = await fetch(`/sync/unlinked-books/${book.progressId}/suggestions`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
const response = await fetch(
|
||||
`/sync/unlinked-books/${book.progressId}/suggestions`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
displaySuggestions(book.progressId, result.suggestions, result.action);
|
||||
@@ -322,7 +375,11 @@ async function bulkGetSuggestions(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function displaySuggestions(progressId: string, suggestions: any[], _action: string): void {
|
||||
function displaySuggestions(
|
||||
progressId: string,
|
||||
suggestions: any[],
|
||||
_action: string,
|
||||
): void {
|
||||
const container = document.getElementById(`matches-${progressId}`);
|
||||
if (!container) return;
|
||||
|
||||
@@ -333,13 +390,15 @@ function displaySuggestions(progressId: string, suggestions: any[], _action: str
|
||||
listContainer.innerHTML = "";
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
listContainer.innerHTML = '<p style="color: var(--text-secondary)">No matches found</p>';
|
||||
listContainer.innerHTML =
|
||||
'<p style="color: var(--text-secondary)">No matches found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
suggestions.forEach((match) => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "p-3 border rounded cursor-pointer hover:bg-opacity-80 transition-colors";
|
||||
div.className =
|
||||
"p-3 border rounded cursor-pointer hover:bg-opacity-80 transition-colors";
|
||||
div.style.cssText = `background-color: var(--bg-primary); border-color: var(--border);`;
|
||||
div.innerHTML = `
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -370,8 +429,13 @@ function showBulkManualLink(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(`Bulk manual link for ${selected.length} books - select target book in library`, "info");
|
||||
window.location.href = "/library?mode=link&unlinked=" + selected.map((s) => s.progressId).join(",");
|
||||
showToast(
|
||||
`Bulk manual link for ${selected.length} books - select target book in library`,
|
||||
"info",
|
||||
);
|
||||
window.location.href =
|
||||
"/library?mode=link&unlinked=" +
|
||||
selected.map((s) => s.progressId).join(",");
|
||||
}
|
||||
|
||||
function setupEventDelegation(): void {
|
||||
@@ -427,7 +491,7 @@ export {
|
||||
toggleAllUnlinked,
|
||||
};
|
||||
|
||||
Alpine.store("unlinkedBooks", {
|
||||
Alpine.data("unlinkedBooks", () => ({
|
||||
bulkAutoLink,
|
||||
bulkGetSuggestions,
|
||||
confirmManualLink,
|
||||
@@ -440,4 +504,4 @@ Alpine.store("unlinkedBooks", {
|
||||
showBulkManualLink,
|
||||
showManualLinkModal,
|
||||
toggleAllUnlinked,
|
||||
});
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user