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>
|
<button type="button" data-action="hide-create-modal" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="create-library-form">
|
<form id="create-library-form">
|
||||||
|
<input type="hidden" id="library-id" name="id">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Library Name</label>
|
<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>
|
<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>
|
||||||
</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/htmx.min.js"></script>
|
||||||
<script src="/static/toast.js"></script>
|
<script src="/static/toast.js"></script>
|
||||||
<script src="/static/api.js"></script>
|
<script src="/static/api.js"></script>
|
||||||
|
|||||||
+233
-33
@@ -18,6 +18,10 @@ interface LibraryFolder {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DeleteFolderRequest {
|
||||||
|
folder_path: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -147,6 +151,9 @@ async function handleCreateLibrarySubmit(event: Event): Promise<void> {
|
|||||||
const form = event.target as HTMLFormElement;
|
const form = event.target as HTMLFormElement;
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
const libraryId = (document.getElementById('library-id') as HTMLInputElement)?.value;
|
||||||
|
const isEdit = !!libraryId;
|
||||||
|
|
||||||
const libraryData = {
|
const libraryData = {
|
||||||
name: formData.get('name') as string,
|
name: formData.get('name') as string,
|
||||||
description: formData.get('description') as string,
|
description: formData.get('description') as string,
|
||||||
@@ -154,18 +161,32 @@ async function handleCreateLibrarySubmit(event: Event): Promise<void> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await (window as any).api.post('/libraries', libraryData);
|
const url = isEdit ? `/libraries/${libraryId}` : '/libraries';
|
||||||
await (window as any).api.handleResponse(response) as { data: Library };
|
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) {
|
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();
|
hideCreateLibraryModal();
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
|
const libraryIdInput = document.getElementById('library-id') as HTMLInputElement;
|
||||||
|
if (libraryIdInput) {
|
||||||
|
libraryIdInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
void reloadLibraries();
|
void reloadLibraries();
|
||||||
} catch (error) {
|
} 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);
|
const library = libraries.find(l => l.id === libraryId);
|
||||||
if (!library) return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,33 +229,30 @@ async function showLibraryFolders(libraryId: string): Promise<void> {
|
|||||||
const container = document.getElementById(`library-folders-${libraryId}`);
|
const container = document.getElementById(`library-folders-${libraryId}`);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
if (container.classList.contains('hidden')) {
|
try {
|
||||||
try {
|
const response = await (window as any).api.get(`/libraries/${libraryId}/folders`);
|
||||||
const response = await (window as any).api.get(`/libraries/${libraryId}/folders`);
|
const folders = await (window as any).api.handleResponse(response) as LibraryFolder[];
|
||||||
const folders = await (window as any).api.handleResponse(response) as LibraryFolder[];
|
|
||||||
|
|
||||||
container.innerHTML = folders.map((folder: 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)">' +
|
'<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>` +
|
`<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" ` +
|
`<button data-library-id="${libraryId}" data-folder-path="${escapeHtmlLocal(folder.folder_path)}" data-action="remove-folder" ` +
|
||||||
'class="text-xs text-red-500">Remove</button>' +
|
'class="text-xs text-red-500">Remove</button>' +
|
||||||
'</div>'
|
'</div>'
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
// Add folder input
|
container.innerHTML += '<div class="mt-2 flex space-x-2">' +
|
||||||
container.innerHTML += '<div class="mt-2 flex space-x-2">' +
|
`<input type="text" id="folder-path-${libraryId}" placeholder="Add folder path" ` +
|
||||||
`<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)">' +
|
||||||
'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}" ` +
|
||||||
`<button data-library-id="${libraryId}" data-action="add-folder" ` +
|
'class="btn-secondary px-2 py-1 text-xs rounded">Browse</button>' +
|
||||||
'class="btn-primary px-2 py-1 text-xs rounded">Add</button>' +
|
`<button data-library-id="${libraryId}" data-action="add-folder" ` +
|
||||||
'</div>';
|
'class="btn-primary px-2 py-1 text-xs rounded">Add</button>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
container.classList.remove('hidden');
|
container.classList.remove('hidden');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
(window as any).api.handleError(error, 'Failed to load folders');
|
(window as any).api.handleError(error, 'Failed to load folders');
|
||||||
}
|
|
||||||
} else {
|
|
||||||
container.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +291,10 @@ async function removeLibraryFolder(libraryId: string, folderPath: string): Promi
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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);
|
await (window as any).api.handleVoidResponse(response);
|
||||||
|
|
||||||
if ((window as any).showToast?.success) {
|
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)
|
// Edit library (placeholder - opens modal or navigates to edit page)
|
||||||
function editLibrary(libraryId: string): void {
|
function editLibrary(libraryId: string): void {
|
||||||
console.log('Edit library:', libraryId);
|
const library = libraries.find(l => l.id === libraryId);
|
||||||
if ((window as any).showToast?.info) {
|
if (!library) {
|
||||||
(window as any).showToast.info('Edit library functionality coming soon');
|
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
|
// Modal controls
|
||||||
@@ -288,6 +346,11 @@ function showCreateLibraryModal(): void {
|
|||||||
const modal = document.getElementById('create-library-modal') as HTMLElement;
|
const modal = document.getElementById('create-library-modal') as HTMLElement;
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.classList.remove('hidden');
|
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);
|
removeLibraryFolder(libraryId, button.dataset.folderPath);
|
||||||
}
|
}
|
||||||
break;
|
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
|
// Initialize page
|
||||||
function initializeLibraryAdmin(): void {
|
function initializeLibraryAdmin(): void {
|
||||||
// Setup event listeners
|
// Setup event listeners
|
||||||
@@ -360,6 +551,11 @@ function initializeLibraryAdmin(): void {
|
|||||||
librariesList.addEventListener('click', handleLibraryListClick);
|
librariesList.addEventListener('click', handleLibraryListClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const folderBrowserModal = document.getElementById('folder-browser-modal');
|
||||||
|
if (folderBrowserModal) {
|
||||||
|
folderBrowserModal.addEventListener('click', handleFolderBrowserClick);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('click', handleGlobalClick);
|
document.addEventListener('click', handleGlobalClick);
|
||||||
|
|
||||||
// Setup form submission
|
// Setup form submission
|
||||||
@@ -385,6 +581,10 @@ function initializeLibraryAdmin(): void {
|
|||||||
(window as any).loadUserVisibility = loadUserVisibility;
|
(window as any).loadUserVisibility = loadUserVisibility;
|
||||||
(window as any).editLibrary = editLibrary;
|
(window as any).editLibrary = editLibrary;
|
||||||
(window as any).handleCreateLibrarySubmit = handleCreateLibrarySubmit;
|
(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
|
// Initialize on DOM ready
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
|
|||||||
Reference in New Issue
Block a user