700 lines
20 KiB
TypeScript
700 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);
|
|
}
|
|
|
|
// Load libraries from API on page load
|
|
void reloadLibraries();
|
|
}
|
|
|
|
// Initialize on DOM ready
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initializeLibraryAdmin);
|
|
} else {
|
|
initializeLibraryAdmin();
|
|
}
|
|
|
|
// Export functions for global access
|
|
export {
|
|
deleteLibrary,
|
|
showLibraryFolders,
|
|
addLibraryFolder,
|
|
removeLibraryFolder,
|
|
setLibraryVisibility,
|
|
loadUserVisibility,
|
|
editLibrary,
|
|
handleCreateLibrarySubmit,
|
|
showFolderBrowser,
|
|
navigateFolderBrowser,
|
|
selectBrowseFolder,
|
|
hideFolderBrowser,
|
|
showDeleteModal,
|
|
hideDeleteModal,
|
|
confirmDeleteLibrary,
|
|
};
|
|
|
|
Alpine.store("library", {
|
|
deleteLibrary,
|
|
showLibraryFolders,
|
|
addLibraryFolder,
|
|
removeLibraryFolder,
|
|
setLibraryVisibility,
|
|
loadUserVisibility,
|
|
editLibrary,
|
|
handleCreateLibrarySubmit,
|
|
showFolderBrowser,
|
|
navigateFolderBrowser,
|
|
selectBrowseFolder,
|
|
hideFolderBrowser,
|
|
showDeleteModal,
|
|
hideDeleteModal,
|
|
confirmDeleteLibrary,
|
|
});
|