This commit updates the web frontend TypeScript modules: Core modules: - admin.ts: Admin panel functionality and user management - analytics.ts: Analytics dashboard and data visualization - api-explorer.ts: Interactive API documentation explorer - api.ts: Core API client with request/response handling - collections.ts: Book collection management UI - conflicts.ts: Sync conflict resolution interface - custom-section-builder.ts: Dynamic section builder for UI - docs.ts: Documentation viewer and navigation - dom.ts: DOM manipulation utilities and helpers - header.ts: Application header with navigation - library.ts: Library view and book grid management - linking.ts: Device-book linking interface - password_validation.ts: Client-side password strength validation - queue.ts: Device sync queue management UI - search.ts: Full-text search with Lunr integration - storage.ts: Local storage and cache management - theme.ts: Theme management and CSS variable updates - themeDropdown.ts: Theme selector dropdown component - toast.ts: Toast notification system - woodPaneling.ts: Visual theme effects - woodPanelingInit.ts: Visual effects initialization Type definitions: - api.d.ts: Updated TypeScript definitions for API responses These updates enhance the frontend with improved functionality for book management, device synchronization, and user experience.
235 lines
6.9 KiB
TypeScript
235 lines
6.9 KiB
TypeScript
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) {
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success("Conflict resolved");
|
|
}
|
|
refreshConflicts();
|
|
} else {
|
|
const error = await response.json();
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error(
|
|
error.error || "Failed to resolve conflict",
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to resolve conflict:", error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error("Failed to resolve conflict");
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success(`Resolved ${data.success} conflicts`);
|
|
}
|
|
refreshConflicts();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to bulk resolve:", error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error("Failed to bulk resolve conflicts");
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success("Conflicts dismissed");
|
|
}
|
|
refreshConflicts();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to dismiss conflicts:", error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error("Failed to dismiss conflicts");
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success("Resolved conflicts dismissed");
|
|
}
|
|
refreshConflicts();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to dismiss resolved:", error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error("Failed to dismiss resolved conflicts");
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error("Please select a winner");
|
|
}
|
|
return;
|
|
}
|
|
|
|
resolveConflict(conflictId, winner);
|
|
hideResolveModal();
|
|
}
|
|
|
|
(window as any).refreshConflicts = refreshConflicts;
|
|
(window as any).resolveConflict = resolveConflict;
|
|
(window as any).bulkResolve = bulkResolve;
|
|
(window as any).bulkDismiss = bulkDismiss;
|
|
(window as any).dismissAllResolved = dismissAllResolved;
|
|
(window as any).showResolveModal = showResolveModal;
|
|
(window as any).hideResolveModal = hideResolveModal;
|
|
(window as any).handleResolveSubmit = handleResolveSubmit;
|