- 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.
229 lines
6.3 KiB
TypeScript
229 lines
6.3 KiB
TypeScript
import { Alpine } from "./alpine";
|
|
import { showToast } from "./toast";
|
|
|
|
async function refreshConflicts(): Promise<void> {
|
|
const token = localStorage.getItem("token");
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch("/api/conflicts", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data: ConflictListResponse = await response.json();
|
|
renderConflicts(data.conflicts);
|
|
updateConflictStats(data);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to refresh conflicts:", error);
|
|
}
|
|
}
|
|
|
|
async function resolveConflict(
|
|
conflictId: string,
|
|
winner: string,
|
|
manualData?: Record<string, unknown>,
|
|
): Promise<void> {
|
|
const token = localStorage.getItem("token");
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/conflicts/${conflictId}/resolve`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ winner, manual_data: manualData }),
|
|
});
|
|
|
|
if (response.ok) {
|
|
showToast("Conflict resolved", "success");
|
|
refreshConflicts();
|
|
} else {
|
|
const error = await response.json();
|
|
showToast(error.error || "Failed to resolve conflict", "error");
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to resolve conflict:", error);
|
|
showToast("Failed to resolve conflict", "error");
|
|
}
|
|
}
|
|
|
|
async function bulkResolve(
|
|
strategy: "most_recent" | "highest_progress",
|
|
conflictIds: string[],
|
|
): Promise<void> {
|
|
const token = localStorage.getItem("token");
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch("/api/conflicts/bulk-resolve", {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ conflict_ids: conflictIds, strategy }),
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data: BulkResolveResponse = await response.json();
|
|
showToast(`Resolved ${data.success} conflicts`, "success");
|
|
refreshConflicts();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to bulk resolve:", error);
|
|
showToast("Failed to bulk resolve conflicts", "error");
|
|
}
|
|
}
|
|
|
|
async function bulkDismiss(conflictIds: string[]): Promise<void> {
|
|
const token = localStorage.getItem("token");
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch("/api/conflicts/bulk-dismiss", {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ conflict_ids: conflictIds }),
|
|
});
|
|
|
|
if (response.ok) {
|
|
showToast("Conflicts dismissed", "success");
|
|
refreshConflicts();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to dismiss conflicts:", error);
|
|
showToast("Failed to dismiss conflicts", "error");
|
|
}
|
|
}
|
|
|
|
async function dismissAllResolved(): Promise<void> {
|
|
const token = localStorage.getItem("token");
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch("/api/conflicts/dismiss-resolved", {
|
|
method: "POST",
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
if (response.ok) {
|
|
showToast("Resolved conflicts dismissed", "success");
|
|
refreshConflicts();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to dismiss resolved:", error);
|
|
showToast("Failed to dismiss resolved conflicts", "error");
|
|
}
|
|
}
|
|
|
|
function renderConflicts(conflicts: ConflictDetailResponse[]): void {
|
|
const container = document.getElementById("conflicts-list");
|
|
if (!container) return;
|
|
|
|
if (conflicts.length === 0) {
|
|
container.innerHTML =
|
|
'<p class="text-center p-4" style="color: var(--text-secondary)">No conflicts found</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = conflicts
|
|
.map(
|
|
(conflict) => `
|
|
<div class="p-4 rounded-lg border mb-2" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h3 class="font-medium" style="color: var(--text-primary)">${conflict.media_item_title}</h3>
|
|
<p class="text-sm" style="color: var(--text-secondary)">${conflict.conflict_type} - ${conflict.resolution_status}</p>
|
|
</div>
|
|
${
|
|
conflict.resolution_status === "unresolved"
|
|
? `
|
|
<div class="flex space-x-2">
|
|
<button onclick="window.showResolveModal('${conflict.id}')" class="btn-primary px-3 py-1 rounded text-sm">Resolve</button>
|
|
</div>
|
|
`
|
|
: ""
|
|
}
|
|
</div>
|
|
</div>
|
|
`,
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function updateConflictStats(data: ConflictListResponse): void {
|
|
const totalEl = document.getElementById("conflicts-total");
|
|
const unresolvedEl = document.getElementById("conflicts-unresolved");
|
|
|
|
if (totalEl) totalEl.textContent = String(data.total);
|
|
if (unresolvedEl) unresolvedEl.textContent = String(data.unresolved);
|
|
}
|
|
|
|
function showResolveModal(conflictId: string): void {
|
|
const modal = document.getElementById("resolve-modal");
|
|
const conflictIdInput = document.getElementById(
|
|
"resolve-conflict-id",
|
|
) as HTMLInputElement;
|
|
|
|
if (modal && conflictIdInput) {
|
|
conflictIdInput.value = conflictId;
|
|
modal.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
function hideResolveModal(): void {
|
|
const modal = document.getElementById("resolve-modal");
|
|
if (modal) {
|
|
modal.classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
function handleResolveSubmit(event: Event): void {
|
|
event.preventDefault();
|
|
|
|
const form = event.target as HTMLFormElement;
|
|
const conflictId = (
|
|
form.querySelector("#resolve-conflict-id") as HTMLInputElement
|
|
)?.value;
|
|
const winner = (
|
|
form.querySelector('input[name="winner"]:checked') as HTMLInputElement
|
|
)?.value;
|
|
|
|
if (!conflictId || !winner) {
|
|
showToast("Please select a winner", "error");
|
|
return;
|
|
}
|
|
|
|
resolveConflict(conflictId, winner);
|
|
hideResolveModal();
|
|
}
|
|
|
|
export {
|
|
bulkDismiss,
|
|
bulkResolve,
|
|
dismissAllResolved,
|
|
handleResolveSubmit,
|
|
hideResolveModal,
|
|
refreshConflicts,
|
|
resolveConflict,
|
|
showResolveModal,
|
|
};
|
|
|
|
Alpine.data("conflicts", () => ({
|
|
bulkDismiss,
|
|
bulkResolve,
|
|
dismissAllResolved,
|
|
handleResolveSubmit,
|
|
hideResolveModal,
|
|
refreshConflicts,
|
|
resolveConflict,
|
|
showResolveModal,
|
|
}));
|