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:
@@ -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
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user