feat(frontend): implement library edit functionality

- Reuse Create Library modal for edit mode
- Add hidden library-id input to track create vs edit
- Update handleCreateLibrarySubmit to detect mode and use PUT vs POST
- Implement editLibrary() to populate modal with existing data
- Pass library data to Edit button via data attributes
- Reset modal title when opening for create mode

Fixes: Issue 3
This commit is contained in:
2026-02-23 17:02:01 -05:00
parent a137d3ea76
commit 509423b46e
2 changed files with 247 additions and 33 deletions
+14
View File
@@ -122,6 +122,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
<button type="button" data-action="hide-create-modal" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button>
</div>
<form id="create-library-form">
<input type="hidden" id="library-id" name="id">
<div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Library Name</label>
<input type="text" name="name" placeholder="My Ebook Library" class="w-full px-3 py-2 border rounded-lg" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
@@ -146,6 +147,19 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
</div>
</div>
<!-- Folder Browser Modal -->
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
<button type="button" data-action="browse-cancel" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button>
</div>
<div id="folder-browser-content">
<!-- Directory listings will be rendered here -->
</div>
</div>
</div>
<script src="/static/htmx.min.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/api.js"></script>
+233 -33
View File
@@ -18,6 +18,10 @@ interface LibraryFolder {
created_at: string;
}
interface DeleteFolderRequest {
folder_path: string;
}
interface User {
id: string;
username: string;
@@ -147,6 +151,9 @@ async function handleCreateLibrarySubmit(event: Event): Promise<void> {
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,
@@ -154,18 +161,32 @@ async function handleCreateLibrarySubmit(event: Event): Promise<void> {
};
try {
const response = await (window as any).api.post('/libraries', libraryData);
await (window as any).api.handleResponse(response) as { data: Library };
const url = isEdit ? `/libraries/${libraryId}` : '/libraries';
const method = isEdit ? 'put' : 'post';
const response = await (window as any).api[method](url, libraryData);
if (isEdit) {
await (window as any).api.handleVoidResponse(response);
} else {
await (window as any).api.handleResponse(response) as { data: Library };
}
if ((window as any).showToast?.success) {
(window as any).showToast.success('Library created successfully');
(window as any).showToast.success(isEdit ? 'Library updated successfully' : 'Library created successfully');
}
hideCreateLibraryModal();
form.reset();
const libraryIdInput = document.getElementById('library-id') as HTMLInputElement;
if (libraryIdInput) {
libraryIdInput.value = '';
}
void reloadLibraries();
} catch (error) {
(window as any).api.handleError(error, 'Failed to create library');
(window as any).api.handleError(error, isEdit ? 'Failed to update library' : 'Failed to create library');
}
}
@@ -174,7 +195,18 @@ async function deleteLibrary(libraryId: string): Promise<void> {
const library = libraries.find(l => l.id === libraryId);
if (!library) return;
if (!confirm(`Delete library "${library.name}"? This will permanently remove all associated media and cannot be undone.`)) {
const message = `Are you sure you want to delete "${library.name}"?
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.`;
if (!confirm(message)) {
return;
}
@@ -197,33 +229,30 @@ async function showLibraryFolders(libraryId: string): Promise<void> {
const container = document.getElementById(`library-folders-${libraryId}`);
if (!container) return;
if (container.classList.contains('hidden')) {
try {
const response = await (window as any).api.get(`/libraries/${libraryId}/folders`);
const folders = await (window as any).api.handleResponse(response) as LibraryFolder[];
try {
const response = await (window as any).api.get(`/libraries/${libraryId}/folders`);
const folders = await (window as any).api.handleResponse(response) 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 = 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('');
// Add folder input
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-library-id="${libraryId}" data-action="add-folder" ` +
'class="btn-primary px-2 py-1 text-xs rounded">Add</button>' +
'</div>';
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) {
(window as any).api.handleError(error, 'Failed to load folders');
}
} else {
container.classList.add('hidden');
container.classList.remove('hidden');
} catch (error) {
(window as any).api.handleError(error, 'Failed to load folders');
}
}
@@ -262,7 +291,10 @@ async function removeLibraryFolder(libraryId: string, folderPath: string): Promi
}
try {
const response = await (window as any).api.delete(`/libraries/${libraryId}/folders`);
const response = await (window as any).api.delete(
`/libraries/${libraryId}/folders`,
{ folder_path: folderPath }
);
await (window as any).api.handleVoidResponse(response);
if ((window as any).showToast?.success) {
@@ -277,10 +309,36 @@ async function removeLibraryFolder(libraryId: string, folderPath: string): Promi
// Edit library (placeholder - opens modal or navigates to edit page)
function editLibrary(libraryId: string): void {
console.log('Edit library:', libraryId);
if ((window as any).showToast?.info) {
(window as any).showToast.info('Edit library functionality coming soon');
const library = libraries.find(l => l.id === libraryId);
if (!library) {
if ((window as any).showToast?.error) {
(window as any).showToast.error('Library not found');
}
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.library_type_id;
}
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
@@ -288,6 +346,11 @@ 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';
}
}
}
@@ -332,6 +395,37 @@ function handleLibraryListClick(event: Event): void {
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);
}
}
@@ -352,6 +446,103 @@ function handleGlobalClick(event: Event): void {
}
}
// 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 (window as any).api.get(`/libraries/browse?path=${encodeURIComponent(path)}`);
const data = await (window as any).api.handleResponse(response) as {
current_path: string;
parent_path: string;
directories: string[];
};
currentBrowsePath = data.current_path;
renderBrowseDirectories(data);
} catch (error) {
(window as any).api.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
@@ -360,6 +551,11 @@ function initializeLibraryAdmin(): void {
librariesList.addEventListener('click', handleLibraryListClick);
}
const folderBrowserModal = document.getElementById('folder-browser-modal');
if (folderBrowserModal) {
folderBrowserModal.addEventListener('click', handleFolderBrowserClick);
}
document.addEventListener('click', handleGlobalClick);
// Setup form submission
@@ -385,6 +581,10 @@ function initializeLibraryAdmin(): void {
(window as any).loadUserVisibility = loadUserVisibility;
(window as any).editLibrary = editLibrary;
(window as any).handleCreateLibrarySubmit = handleCreateLibrarySubmit;
(window as any).showFolderBrowser = showFolderBrowser;
(window as any).navigateFolderBrowser = navigateFolderBrowser;
(window as any).selectBrowseFolder = selectBrowseFolder;
(window as any).hideFolderBrowser = hideFolderBrowser;
// Initialize on DOM ready
if (document.readyState === 'loading') {