CRITICAL FIX: initializeLibraryAdmin() was calling reloadLibraries()
which fetched data from the API and replaced the SSR-rendered library
list on page load, defeating the purpose of server-side rendering.
Changes in web/src/library.ts:
- Remove DOMContentLoaded listener (now uses Alpine x-init in template)
- Remove void reloadLibraries() call from initializeLibraryAdmin()
- Add comment explaining SSR provides initial data
- Add initializeLibraryAdmin to export statement
- Add initializeLibraryAdmin to Alpine.data() registration
- Keep reloadLibraries() as standalone function for use after CRUD ops
Rationale:
- SSR provides fast initial page load with library list
- x-init should ONLY setup event listeners, not fetch data
- reloadLibraries() is called after create/delete/update operations
- Follows SSR-first architecture: different pages have different
SSR/JS ratios (analytics is 80% JS, most pages are 80% SSR)
Documentation:
- Update COLLECTIONS_CLEANUP_GUIDE.md with SSR-first strategy
- Document page-by-page review status (dashboard ✓, collections 🔄)
- Fix template references (library.templ → admin_library.templ)
- Explain why analytics fetches data (intentional for dynamic page)
This ensures the admin library page maintains SSR benefits while
still providing interactive features via Alpine.js.
695 lines
20 KiB
TypeScript
695 lines
20 KiB
TypeScript
// Library management functionality for admin/library page
|
|
// Procedural/imperative style t
|
|
// nt
|
|
// OOP)
|
|
|
|
import { Alpine } from "./alpine";
|
|
import {
|
|
apiPut,
|
|
apiPost,
|
|
apiGet,
|
|
handleResponse,
|
|
handleError,
|
|
handleVoidResponse,
|
|
apiDelete,
|
|
} from "./api";
|
|
import { showToast } from "./toast";
|
|
|
|
interface Library {
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
library_type_id: string;
|
|
type_name: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
interface LibraryFolder {
|
|
id: string;
|
|
library_id: string;
|
|
folder_path: string;
|
|
created_at: string;
|
|
}
|
|
|
|
interface LibrariesResponse {
|
|
data: Library[];
|
|
}
|
|
|
|
// State (already SSR'd, used for updates)
|
|
let libraries: Library[] = [];
|
|
|
|
// Reload libraries from API (called after create/delete/update)
|
|
async function reloadLibraries(): Promise<void> {
|
|
try {
|
|
const response = await apiGet("/libraries");
|
|
const result = handleResponse(response) as unknown as LibrariesResponse;
|
|
libraries = result.data;
|
|
renderLibraries();
|
|
} catch (error) {
|
|
handleError(error, "Failed to load libraries");
|
|
}
|
|
}
|
|
|
|
// Render libraries list (replaces SSR content after updates)
|
|
function renderLibraries(): void {
|
|
const container = document.getElementById("libraries-list");
|
|
if (!container) return;
|
|
|
|
if (libraries.length === 0) {
|
|
container.innerHTML =
|
|
'<p style="color: var(--text-secondary)" class="text-center py-8">No libraries yet. Create your first library to get started.</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = libraries
|
|
.map(
|
|
(library) =>
|
|
'<div class="p-4 border rounded-lg" style="background-color: var(--bg-primary); border-color: var(--border)">' +
|
|
'<div class="flex justify-between items-start mb-2">' +
|
|
"<div>" +
|
|
`<h4 class="font-semibold" style="color: var(--text-primary)">${escapeHtmlLocal(library.name)}</h4>` +
|
|
(library.description
|
|
? `<p class="text-sm" style="color: var(--text-secondary)">${escapeHtmlLocal(library.description)}</p>`
|
|
: "") +
|
|
`<span class="inline-block px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)">${escapeHtmlLocal(library.type_name)}</span>` +
|
|
"</div>" +
|
|
'<div class="flex space-x-2">' +
|
|
`<button data-library-id="${library.id}" data-action="show-folders" class="text-xs px-2 py-1 rounded" style="background-color: var(--bg-secondary); color: var(--text-primary)">Folders</button>` +
|
|
`<button data-library-id="${library.id}" data-action="edit" class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: var(--bg-primary)">Edit</button>` +
|
|
`<button data-library-id="${library.id}" data-action="delete" class="text-xs px-2 py-1 rounded text-red-500">Delete</button>` +
|
|
"</div>" +
|
|
"</div>" +
|
|
`<div id="library-folders-${library.id}" class="hidden mt-3 space-y-2"></div>` +
|
|
"</div>",
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
// Load user's visible libraries for visibility management
|
|
async function loadUserVisibility(): Promise<void> {
|
|
const select = document.getElementById("user-select") as HTMLSelectElement;
|
|
const userId = select?.value;
|
|
if (!userId) {
|
|
const container = document.getElementById("user-libraries");
|
|
if (container) {
|
|
container.innerHTML =
|
|
'<p style="color: var(--text-secondary)">Please select a user</p>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await apiGet("/libraries/visible");
|
|
const visibleLibraries = (await handleResponse(
|
|
response,
|
|
)) as unknown as Library[];
|
|
const container = document.getElementById("user-libraries");
|
|
if (!container) return;
|
|
|
|
const visibleIds = new Set(visibleLibraries.map((lib: Library) => lib.id));
|
|
|
|
container.innerHTML = libraries
|
|
.map((library) => {
|
|
const isVisible = visibleIds.has(library.id);
|
|
return (
|
|
'<label class="flex items-center space-x-3 p-2 rounded" style="background-color: var(--bg-primary);">' +
|
|
`<input type="checkbox" ${isVisible ? "checked" : ""} ` +
|
|
`data-user-id="${userId}" data-library-id="${library.id}" ` +
|
|
`onchange="setLibraryVisibility('${userId}', '${library.id}', this.checked)" ` +
|
|
'class="w-4 h-4">' +
|
|
`<span style="color: var(--text-primary)">${escapeHtmlLocal(library.name)} (${escapeHtmlLocal(library.type_name)})</span>` +
|
|
"</label>"
|
|
);
|
|
})
|
|
.join("");
|
|
} catch (error) {
|
|
handleError(error, "Failed to load user libraries");
|
|
}
|
|
}
|
|
|
|
// Set library visibility for a user
|
|
async function setLibraryVisibility(
|
|
userId: string,
|
|
libraryId: string,
|
|
isVisible: boolean,
|
|
): Promise<void> {
|
|
// userId is used in the HTML onchange handler but the API gets user from JWT context
|
|
console.debug(
|
|
"Setting visibility for user:",
|
|
userId,
|
|
"library:",
|
|
libraryId,
|
|
"visible:",
|
|
isVisible,
|
|
);
|
|
|
|
try {
|
|
const response = await apiPost("/libraries/visibility", {
|
|
library_id: libraryId,
|
|
is_visible: isVisible,
|
|
});
|
|
await handleVoidResponse(response);
|
|
|
|
showToast("Library visibility updated", "success");
|
|
|
|
// Refresh visibility controls
|
|
void loadUserVisibility();
|
|
} catch (error) {
|
|
handleError(error, "Failed to update library visibility");
|
|
}
|
|
}
|
|
|
|
// Create library form handler
|
|
async function handleCreateLibrarySubmit(event: Event): Promise<void> {
|
|
event.preventDefault();
|
|
|
|
const form = event.target as HTMLFormElement;
|
|
const formData = new FormData(form);
|
|
|
|
const libraryId = (document.getElementById("library-id") as HTMLInputElement)
|
|
?.value;
|
|
const isEdit = !!libraryId;
|
|
|
|
const libraryData = {
|
|
name: formData.get("name") as string,
|
|
description: formData.get("description") as string,
|
|
type: formData.get("type") as string,
|
|
};
|
|
|
|
try {
|
|
const url = isEdit ? `/libraries/${libraryId}` : "/libraries";
|
|
const response = isEdit
|
|
? await apiPut(url, libraryData)
|
|
: await apiPost(url, libraryData);
|
|
|
|
if (isEdit) {
|
|
await handleVoidResponse(response);
|
|
} else {
|
|
(await handleResponse(response)) as unknown as { data: Library };
|
|
}
|
|
|
|
showToast(
|
|
isEdit ? "Library updated successfully" : "Library created successfully",
|
|
"success",
|
|
);
|
|
|
|
hideCreateLibraryModal();
|
|
form.reset();
|
|
|
|
const libraryIdInput = document.getElementById(
|
|
"library-id",
|
|
) as HTMLInputElement;
|
|
if (libraryIdInput) {
|
|
libraryIdInput.value = "";
|
|
}
|
|
|
|
void reloadLibraries();
|
|
} catch (error) {
|
|
handleError(
|
|
error,
|
|
isEdit ? "Failed to update library" : "Failed to create library",
|
|
);
|
|
}
|
|
}
|
|
|
|
// Delete library
|
|
async function deleteLibrary(libraryId: string): Promise<void> {
|
|
const library = libraries.find((l) => l.id === libraryId);
|
|
if (!library) return;
|
|
|
|
showDeleteModal(library);
|
|
}
|
|
|
|
// Show library folders
|
|
async function showLibraryFolders(libraryId: string): Promise<void> {
|
|
const container = document.getElementById(`library-folders-${libraryId}`);
|
|
if (!container) return;
|
|
|
|
try {
|
|
const response = await apiGet(`/libraries/${libraryId}/folders`);
|
|
const folders = (await handleResponse(
|
|
response,
|
|
)) as unknown as LibraryFolder[];
|
|
|
|
container.innerHTML = folders
|
|
.map(
|
|
(folder: LibraryFolder) =>
|
|
'<div class="flex justify-between items-center p-2 rounded" style="background-color: var(--bg-secondary); border-color: var(--border)">' +
|
|
`<span class="text-sm" style="color: var(--text-primary)">${escapeHtmlLocal(folder.folder_path)}</span>` +
|
|
`<button data-library-id="${libraryId}" data-folder-path="${escapeHtmlLocal(folder.folder_path)}" data-action="remove-folder" ` +
|
|
'class="text-xs text-red-500">Remove</button>' +
|
|
"</div>",
|
|
)
|
|
.join("");
|
|
|
|
container.innerHTML +=
|
|
'<div class="mt-2 flex space-x-2">' +
|
|
`<input type="text" id="folder-path-${libraryId}" placeholder="Add folder path" ` +
|
|
'class="flex-1 px-2 py-1 text-sm border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">' +
|
|
`<button data-action="browse-folder" data-input-id="folder-path-${libraryId}" ` +
|
|
'class="btn-secondary px-2 py-1 text-xs rounded">Browse</button>' +
|
|
`<button data-library-id="${libraryId}" data-action="add-folder" ` +
|
|
'class="btn-primary px-2 py-1 text-xs rounded">Add</button>' +
|
|
"</div>";
|
|
|
|
container.classList.remove("hidden");
|
|
} catch (error) {
|
|
handleError(error, "Failed to load folders");
|
|
}
|
|
}
|
|
|
|
// Add library folder
|
|
async function addLibraryFolder(libraryId: string): Promise<void> {
|
|
const input = document.getElementById(
|
|
`folder-path-${libraryId}`,
|
|
) as HTMLInputElement;
|
|
const folderPath = input?.value.trim();
|
|
|
|
if (!folderPath) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await apiPost(`/libraries/${libraryId}/folders`, {
|
|
folder_path: folderPath,
|
|
});
|
|
await handleVoidResponse(response);
|
|
|
|
showToast("Folder added successfully", "success");
|
|
|
|
if (input) {
|
|
input.value = "";
|
|
}
|
|
void showLibraryFolders(libraryId); // Refresh
|
|
} catch (error) {
|
|
handleError(error, "Failed to add folder");
|
|
}
|
|
}
|
|
|
|
// Remove library folder
|
|
async function removeLibraryFolder(
|
|
libraryId: string,
|
|
folderPath: string,
|
|
): Promise<void> {
|
|
if (!confirm(`Remove folder "${folderPath}" from the library?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await apiDelete(`/libraries/${libraryId}/folders`, {
|
|
folder_path: folderPath,
|
|
});
|
|
await handleVoidResponse(response);
|
|
|
|
showToast("Folder removed successfully", "success");
|
|
|
|
void showLibraryFolders(libraryId); // Refresh
|
|
} catch (error) {
|
|
handleError(error, "Failed to remove folder");
|
|
}
|
|
}
|
|
|
|
// Edit library (placeholder - opens modal or navigates to edit page)
|
|
function editLibrary(libraryId: string): void {
|
|
const library = libraries.find((l) => l.id === libraryId);
|
|
if (!library) {
|
|
showToast("Library not found", "error");
|
|
return;
|
|
}
|
|
|
|
const form = document.getElementById(
|
|
"create-library-form",
|
|
) as HTMLFormElement;
|
|
if (form) {
|
|
const nameInput = form.querySelector('[name="name"]') as HTMLInputElement;
|
|
const descInput = form.querySelector(
|
|
'[name="description"]',
|
|
) as HTMLTextAreaElement;
|
|
const typeInput = form.querySelector('[name="type"]') as HTMLSelectElement;
|
|
|
|
if (nameInput) nameInput.value = library.name;
|
|
if (descInput) descInput.value = library.description || "";
|
|
if (typeInput) typeInput.value = library.type_name;
|
|
}
|
|
|
|
const modalTitle = document.querySelector("#create-library-modal h2");
|
|
if (modalTitle) {
|
|
modalTitle.textContent = "Edit Library";
|
|
}
|
|
|
|
const libraryIdInput = document.getElementById(
|
|
"library-id",
|
|
) as HTMLInputElement;
|
|
if (libraryIdInput) {
|
|
libraryIdInput.value = libraryId;
|
|
}
|
|
|
|
showCreateLibraryModal();
|
|
}
|
|
|
|
// Modal controls
|
|
function showCreateLibraryModal(): void {
|
|
const modal = document.getElementById("create-library-modal") as HTMLElement;
|
|
if (modal) {
|
|
modal.classList.remove("hidden");
|
|
|
|
const modalTitle = document.querySelector("#create-library-modal h2");
|
|
if (modalTitle) {
|
|
modalTitle.textContent = "Create Library";
|
|
}
|
|
}
|
|
}
|
|
|
|
function hideCreateLibraryModal(): void {
|
|
const modal = document.getElementById("create-library-modal") as HTMLElement;
|
|
if (modal) {
|
|
modal.classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
// Delete modal state
|
|
let libraryToDelete: Library | null = null;
|
|
|
|
function showDeleteModal(library: Library): void {
|
|
libraryToDelete = library;
|
|
|
|
const modal = document.getElementById("delete-library-modal") as HTMLElement;
|
|
const content = document.getElementById(
|
|
"delete-modal-content",
|
|
) as HTMLElement;
|
|
|
|
if (modal && content) {
|
|
const message = `Are you sure you want to delete "<strong>${escapeHtmlLocal(library.name)}</strong>"?
|
|
|
|
This will remove:
|
|
• Library metadata from the database
|
|
• All folder references
|
|
• All book records from the database
|
|
|
|
⚠️ Book files on disk will NOT be deleted.
|
|
|
|
This action cannot be undone.`;
|
|
|
|
content.innerHTML = message.replace(/\n/g, "<br>");
|
|
modal.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
function hideDeleteModal(): void {
|
|
const modal = document.getElementById("delete-library-modal") as HTMLElement;
|
|
if (modal) {
|
|
modal.classList.add("hidden");
|
|
}
|
|
libraryToDelete = null;
|
|
}
|
|
|
|
async function confirmDeleteLibrary(): Promise<void> {
|
|
if (!libraryToDelete) return;
|
|
|
|
const libraryId = libraryToDelete.id;
|
|
hideDeleteModal();
|
|
|
|
try {
|
|
const response = await apiDelete(`/libraries/${libraryId}`);
|
|
await handleVoidResponse(response);
|
|
|
|
showToast("Library deleted successfully", "success");
|
|
|
|
await reloadLibraries();
|
|
|
|
const libraryIdInput = document.getElementById(
|
|
"library-id",
|
|
) as HTMLInputElement | null;
|
|
if (libraryIdInput) {
|
|
libraryIdInput.value = "";
|
|
}
|
|
} catch (error) {
|
|
handleError(error, "Failed to delete library");
|
|
}
|
|
}
|
|
|
|
// Local escape HTML helper
|
|
function escapeHtmlLocal(text: string): string {
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Event delegation for handling dynamic button clicks
|
|
function handleLibraryListClick(event: Event): void {
|
|
const target = event.target as HTMLElement;
|
|
const button = target.closest("button") as HTMLElement;
|
|
if (!button) return;
|
|
|
|
const action = button.dataset.action;
|
|
const libraryId = button.dataset.libraryId;
|
|
|
|
switch (action) {
|
|
case "show-folders":
|
|
if (libraryId) showLibraryFolders(libraryId);
|
|
break;
|
|
case "delete":
|
|
if (libraryId) deleteLibrary(libraryId);
|
|
break;
|
|
case "edit":
|
|
if (libraryId) editLibrary(libraryId);
|
|
break;
|
|
case "add-folder":
|
|
if (libraryId) addLibraryFolder(libraryId);
|
|
break;
|
|
case "remove-folder":
|
|
if (libraryId && button.dataset.folderPath) {
|
|
removeLibraryFolder(libraryId, button.dataset.folderPath);
|
|
}
|
|
break;
|
|
case "browse-folder":
|
|
if (button.dataset.inputId) showFolderBrowser(button.dataset.inputId);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function handleFolderBrowserClick(event: Event): void {
|
|
const target = event.target as HTMLElement;
|
|
const button = target.closest("button") as HTMLElement;
|
|
const div = target.closest("div[data-action]") as HTMLElement;
|
|
|
|
if (button) {
|
|
const action = button.dataset.action;
|
|
const path = button.dataset.path;
|
|
|
|
switch (action) {
|
|
case "browse-parent":
|
|
if (path) navigateFolderBrowser(path);
|
|
break;
|
|
case "browse-cancel":
|
|
hideFolderBrowser();
|
|
break;
|
|
case "browse-select":
|
|
if (path) selectBrowseFolder(path);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (div && div.dataset.action === "browse-navigate") {
|
|
const path = div.dataset.path;
|
|
if (path) navigateFolderBrowser(path);
|
|
}
|
|
}
|
|
|
|
function handleGlobalClick(event: Event): void {
|
|
const target = event.target as HTMLElement;
|
|
const button = target.closest("button") as HTMLElement;
|
|
if (!button) return;
|
|
|
|
const action = button.dataset.action;
|
|
|
|
switch (action) {
|
|
case "show-create-modal":
|
|
showCreateLibraryModal();
|
|
break;
|
|
case "hide-create-modal":
|
|
hideCreateLibraryModal();
|
|
break;
|
|
case "hide-delete-modal":
|
|
hideDeleteModal();
|
|
break;
|
|
case "confirm-delete":
|
|
void confirmDeleteLibrary();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Folder browser state
|
|
let currentBrowsePath = "";
|
|
let currentBrowseInputId = "";
|
|
|
|
// Show folder browser modal
|
|
function showFolderBrowser(inputId: string): void {
|
|
currentBrowseInputId = inputId;
|
|
currentBrowsePath = "/";
|
|
|
|
const modal = document.getElementById("folder-browser-modal") as HTMLElement;
|
|
if (modal) {
|
|
modal.classList.remove("hidden");
|
|
void loadBrowseDirectories(currentBrowsePath);
|
|
}
|
|
}
|
|
|
|
// Load directories for browsing
|
|
async function loadBrowseDirectories(path: string): Promise<void> {
|
|
try {
|
|
const response = await apiGet(
|
|
`/libraries/browse?path=${encodeURIComponent(path)}`,
|
|
);
|
|
const data = (await handleResponse(response)) as unknown as {
|
|
current_path: string;
|
|
parent_path: string;
|
|
directories: string[];
|
|
};
|
|
|
|
currentBrowsePath = data.current_path;
|
|
renderBrowseDirectories(data);
|
|
} catch (error) {
|
|
handleError(error, "Failed to load directories");
|
|
}
|
|
}
|
|
|
|
// Render browse directories (uses event delegation via data-action attributes)
|
|
function renderBrowseDirectories(data: {
|
|
current_path: string;
|
|
parent_path: string;
|
|
directories: string[];
|
|
}): void {
|
|
const container = document.getElementById("folder-browser-content");
|
|
if (!container) return;
|
|
|
|
let html = `
|
|
<div class="flex items-center gap-2 mb-4">
|
|
${
|
|
data.parent_path
|
|
? `<button type="button" data-action="browse-parent" data-path="${escapeHtmlLocal(data.parent_path)}" class="btn-secondary px-3 py-1 rounded">↑ Parent</button>`
|
|
: ""
|
|
}
|
|
<span class="text-sm" style="color: var(--text-secondary)">${escapeHtmlLocal(data.current_path)}</span>
|
|
</div>
|
|
<div class="max-h-64 overflow-y-auto space-y-1">
|
|
`;
|
|
|
|
if (data.directories.length === 0) {
|
|
html +=
|
|
'<p style="color: var(--text-secondary)" class="text-center py-4">No subdirectories</p>';
|
|
} else {
|
|
data.directories.forEach((dir) => {
|
|
const fullPath =
|
|
data.current_path === "/" ? `/${dir}` : `${data.current_path}/${dir}`;
|
|
html += `
|
|
<div class="p-2 rounded cursor-pointer hover:opacity-80"
|
|
style="background-color: var(--bg-secondary); color: var(--text-primary)"
|
|
data-action="browse-navigate"
|
|
data-path="${escapeHtmlLocal(fullPath)}">
|
|
📁 ${escapeHtmlLocal(dir)}
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
html += `
|
|
</div>
|
|
<div class="mt-4 flex justify-end gap-2">
|
|
<button type="button" data-action="browse-cancel" class="btn-secondary px-4 py-2 rounded">Cancel</button>
|
|
<button type="button" data-action="browse-select" data-path="${escapeHtmlLocal(data.current_path)}" class="btn-primary px-4 py-2 rounded">Select This Folder</button>
|
|
</div>
|
|
`;
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// Navigate to subdirectory
|
|
function navigateFolderBrowser(path: string): void {
|
|
void loadBrowseDirectories(path);
|
|
}
|
|
|
|
// Select folder and close browser
|
|
function selectBrowseFolder(path: string): void {
|
|
const input = document.getElementById(
|
|
currentBrowseInputId,
|
|
) as HTMLInputElement;
|
|
if (input) {
|
|
input.value = path;
|
|
}
|
|
hideFolderBrowser();
|
|
}
|
|
|
|
// Hide folder browser modal
|
|
function hideFolderBrowser(): void {
|
|
const modal = document.getElementById("folder-browser-modal") as HTMLElement;
|
|
if (modal) {
|
|
modal.classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
// Initialize page
|
|
function initializeLibraryAdmin(): void {
|
|
// Setup event listeners
|
|
const librariesList = document.getElementById("libraries-list");
|
|
if (librariesList) {
|
|
librariesList.addEventListener("click", handleLibraryListClick);
|
|
}
|
|
|
|
const folderBrowserModal = document.getElementById("folder-browser-modal");
|
|
if (folderBrowserModal) {
|
|
folderBrowserModal.addEventListener("click", handleFolderBrowserClick);
|
|
}
|
|
|
|
document.addEventListener("click", handleGlobalClick);
|
|
|
|
// Setup form submission
|
|
const createLibraryForm = document.getElementById("create-library-form");
|
|
if (createLibraryForm) {
|
|
createLibraryForm.addEventListener("submit", handleCreateLibrarySubmit);
|
|
}
|
|
|
|
// SSR provides initial library list - no need to fetch on page load
|
|
// reloadLibraries() is called after create/delete/update operations
|
|
}
|
|
|
|
// Export functions for global access
|
|
export {
|
|
addLibraryFolder,
|
|
confirmDeleteLibrary,
|
|
deleteLibrary,
|
|
editLibrary,
|
|
handleCreateLibrarySubmit,
|
|
hideDeleteModal,
|
|
hideFolderBrowser,
|
|
initializeLibraryAdmin,
|
|
loadUserVisibility,
|
|
navigateFolderBrowser,
|
|
removeLibraryFolder,
|
|
selectBrowseFolder,
|
|
setLibraryVisibility,
|
|
showDeleteModal,
|
|
showFolderBrowser,
|
|
showLibraryFolders,
|
|
};
|
|
|
|
Alpine.data("library", () => ({
|
|
addLibraryFolder,
|
|
confirmDeleteLibrary,
|
|
deleteLibrary,
|
|
editLibrary,
|
|
handleCreateLibrarySubmit,
|
|
hideDeleteModal,
|
|
hideFolderBrowser,
|
|
initializeLibraryAdmin,
|
|
loadUserVisibility,
|
|
navigateFolderBrowser,
|
|
removeLibraryFolder,
|
|
selectBrowseFolder,
|
|
setLibraryVisibility,
|
|
showDeleteModal,
|
|
showFolderBrowser,
|
|
showLibraryFolders,
|
|
}));
|