diff --git a/bye b/bye new file mode 100644 index 0000000..e69de29 diff --git a/lib/storage.mjs b/lib/storage.mjs index 709251e..578d5c4 100644 --- a/lib/storage.mjs +++ b/lib/storage.mjs @@ -169,6 +169,11 @@ export function delete_pdf(id) { return store.delete(`pdf:${id}`); } +// Returns all components that reference the given PDF id in their file_ids array. +export function find_pdf_references(pdf_id) { + return list_components().filter(c => c.file_ids?.includes(pdf_id)); +} + // --- Grid images --- export function list_grid_images() { diff --git a/public/app.mjs b/public/app.mjs index ff5f006..0ded26c 100644 --- a/public/app.mjs +++ b/public/app.mjs @@ -1479,32 +1479,49 @@ function render_file_picker_list() { row.appendChild(thumb); } + const name_wrap = document.createElement('div'); + name_wrap.className = 'fp-name-wrap'; const name_el = document.createElement('span'); name_el.className = 'fp-name'; name_el.textContent = pdf.display_name; + const filename_el = document.createElement('span'); + filename_el.className = 'fp-filename'; + filename_el.textContent = pdf.filename; + name_wrap.append(name_el, filename_el); const rename_btn = document.createElement('button'); rename_btn.type = 'button'; rename_btn.className = 'btn btn-secondary btn-sm'; rename_btn.textContent = 'Rename'; rename_btn.addEventListener('click', () => { - const input = document.createElement('input'); - input.type = 'text'; - input.className = 'fp-rename-input'; - input.value = pdf.display_name; - name_el.replaceWith(input); + const name_input = document.createElement('input'); + name_input.type = 'text'; + name_input.className = 'fp-rename-input'; + name_input.value = pdf.display_name; + name_input.placeholder = 'Display name'; + + const filename_input = document.createElement('input'); + filename_input.type = 'text'; + filename_input.className = 'fp-rename-input fp-rename-filename'; + filename_input.value = pdf.filename; + filename_input.placeholder = 'filename.pdf'; + filename_input.spellcheck = false; + + name_wrap.replaceChildren(name_input, filename_input); rename_btn.textContent = 'Save'; - input.focus(); - input.select(); + name_input.focus(); + name_input.select(); const do_rename = async () => { - const new_name = input.value.trim(); - if (!new_name || new_name === pdf.display_name) { + const new_display = name_input.value.trim(); + const new_filename = filename_input.value.trim(); + if (!new_display || !new_filename) { render_file_picker_list(); return; } + if (new_display === pdf.display_name && new_filename === pdf.filename) { render_file_picker_list(); return; } try { - const result = await api.rename_pdf(pdf.id, new_name); + const result = await api.rename_pdf(pdf.id, new_display, new_filename); const idx = all_pdfs.findIndex(p => p.id === pdf.id); if (idx !== -1) all_pdfs[idx] = result.pdf; all_pdfs.sort((a, b) => a.display_name.localeCompare(b.display_name)); @@ -1515,10 +1532,10 @@ function render_file_picker_list() { }; rename_btn.onclick = do_rename; - input.addEventListener('keydown', (e) => { + [name_input, filename_input].forEach(inp => inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { do_rename(); } if (e.key === 'Escape') { render_file_picker_list(); } - }); + })); }); const select_btn = document.createElement('button'); @@ -1542,7 +1559,7 @@ function render_file_picker_list() { render_file_picker_list(); }); - row.append(name_el, rename_btn, select_btn, del_btn); + row.append(name_wrap, rename_btn, select_btn, del_btn); return row; })); @@ -2062,22 +2079,32 @@ async function init() { const file = e.target.files[0]; if (!file) return; const name_input = document.getElementById('fp-upload-name'); + const filename_input = document.getElementById('fp-upload-filename'); if (!name_input.value.trim()) { name_input.value = file.name.replace(/\.pdf$/i, ''); } + if (!filename_input.value.trim()) { + filename_input.value = file.name; + } }); qs(document.getElementById('dialog-file-picker'), '#fp-upload-btn').addEventListener('click', async () => { const file_input = document.getElementById('fp-file-input'); const name_input = document.getElementById('fp-upload-name'); + const filename_input = document.getElementById('fp-upload-filename'); const file = file_input.files[0]; if (!file) return; + const display_name = name_input.value.trim(); + const filename = filename_input.value.trim(); + if (!display_name) { alert('Display name is required.'); return; } + if (!filename) { alert('Filename is required.'); return; } try { - const result = await api.upload_pdf(file, name_input.value.trim() || null); + const result = await api.upload_pdf(file, display_name, filename); all_pdfs.push(result.pdf); all_pdfs.sort((a, b) => a.display_name.localeCompare(b.display_name)); file_input.value = ''; name_input.value = ''; + filename_input.value = ''; render_file_picker_list(); } catch (err) { alert(err.message); diff --git a/public/lib/api.mjs b/public/lib/api.mjs index 936f699..e8998d2 100644 --- a/public/lib/api.mjs +++ b/public/lib/api.mjs @@ -46,13 +46,14 @@ export const delete_component_template = (id) => req('DELETE', `/api/component- // PDF files export const get_pdfs = () => req('GET', '/api/pdfs'); -export const rename_pdf = (id, display_name) => req('PUT', `/api/pdfs/${id}`, { display_name }); +export const rename_pdf = (id, display_name, filename) => req('PUT', `/api/pdfs/${id}`, { display_name, filename }); export const delete_pdf = (id) => req('DELETE', `/api/pdfs/${id}`); -export async function upload_pdf(file, display_name) { +export async function upload_pdf(file, display_name, filename) { const form = new FormData(); form.append('file', file); if (display_name) form.append('display_name', display_name); + if (filename) form.append('filename', filename); const res = await fetch('/api/pdfs', { method: 'POST', body: form }); const data = await res.json(); if (!data.ok) throw new Error(data.error ?? 'Upload failed'); diff --git a/public/style.css b/public/style.css index d326bf8..242f8bb 100644 --- a/public/style.css +++ b/public/style.css @@ -1701,22 +1701,44 @@ nav { background: var(--surface-raised); } -.fp-name { +.fp-name-wrap { flex: 1; + display: flex; + flex-direction: column; + gap: 0.1rem; + overflow: hidden; + min-width: 0; +} + +.fp-name { font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.fp-filename { + font-size: 0.75rem; + color: var(--text-dim); + font-family: var(--font-mono); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .fp-rename-input { - flex: 1; font-size: 0.9rem; background: var(--bg); color: var(--text); border: 1px solid var(--border-focus); border-radius: 4px; padding: 0.2rem 0.4rem; + width: 100%; +} + +.fp-rename-filename { + font-family: var(--font-mono); + font-size: 0.8rem; } .fp-upload-section { @@ -1729,7 +1751,14 @@ nav { display: flex; gap: 0.5rem; align-items: center; - flex-wrap: wrap; + margin-top: 0.4rem; +} + +.fp-field-label { + font-size: 0.85rem; + color: var(--text-dim); + white-space: nowrap; + min-width: 8rem; } .fp-upload-row input[type=text] { diff --git a/public/templates.html b/public/templates.html index a8ad97f..7779289 100644 --- a/public/templates.html +++ b/public/templates.html @@ -566,7 +566,16 @@
Upload new PDF
- +
+
+ + +
+
+ + +
+
diff --git a/server.mjs b/server.mjs index 1f8979f..2ab1ad4 100644 --- a/server.mjs +++ b/server.mjs @@ -526,27 +526,27 @@ app.get('/api/pdfs', (req, res) => { app.post('/api/pdfs', pdf_upload.single('file'), (req, res) => { if (!req.file) return fail(res, 'no file uploaded'); - const display_name = (req.body.display_name?.trim() || req.file.originalname).trim(); - if (list_pdfs().some(p => p.display_name === display_name)) { + const display_name = req.body.display_name?.trim() || req.file.originalname; + const filename = sanitize_pdf_filename(req.body.filename?.trim() || req.file.originalname); + const all = list_pdfs(); + if (all.some(p => p.display_name === display_name)) { try { unlinkSync(join('./data/pdfs', req.file.filename)); } catch {} - return fail(res, 'a file with that name already exists'); + return fail(res, 'a file with that display name already exists'); + } + if (all.some(p => p.filename === filename)) { + try { unlinkSync(join('./data/pdfs', req.file.filename)); } catch {} + return fail(res, 'a file with that filename already exists'); } const id = generate_id(); - const filename = sanitize_pdf_filename(display_name); const temp_path = join('./data/pdfs', req.file.filename); const final_path = join('./data/pdfs', filename); - if (!rename_no_replace(temp_path, final_path)) return fail(res, 'a file with that name already exists on disk'); + if (!rename_no_replace(temp_path, final_path)) { + try { unlinkSync(temp_path); } catch {} + return fail(res, 'a file with that filename already exists on disk'); + } const thumb_prefix = join('./data/pdfs', id + '-thumb'); const thumb_file = generate_pdf_thumb(final_path, thumb_prefix); - const pdf = { - id, - filename, - display_name, - original_name: req.file.originalname, - size: req.file.size, - thumb_filename: thumb_file, - uploaded_at: Date.now(), - }; + const pdf = { id, filename, display_name, original_name: req.file.originalname, size: req.file.size, thumb_filename: thumb_file, uploaded_at: Date.now() }; set_pdf(pdf); ok(res, { pdf }); }); @@ -555,16 +555,19 @@ app.put('/api/pdfs/:id', (req, res) => { const pdf = get_pdf(req.params.id); if (!pdf) return fail(res, 'not found', 404); const display_name = req.body.display_name?.trim(); + const filename = req.body.filename?.trim() ? sanitize_pdf_filename(req.body.filename.trim()) : pdf.filename; if (!display_name) return fail(res, 'display_name is required'); - if (list_pdfs().some(p => p.display_name === display_name && p.id !== pdf.id)) { - return fail(res, 'a file with that name already exists'); + const all = list_pdfs(); + if (all.some(p => p.display_name === display_name && p.id !== pdf.id)) + return fail(res, 'a file with that display name already exists'); + if (all.some(p => p.filename === filename && p.id !== pdf.id)) + return fail(res, 'a file with that filename already exists'); + if (filename !== pdf.filename) { + const new_path = join('./data/pdfs', filename); + if (!rename_no_replace(join('./data/pdfs', pdf.filename), new_path)) + return fail(res, 'a file with that filename already exists on disk'); } - const new_filename = sanitize_pdf_filename(display_name); - if (new_filename !== pdf.filename) { - const new_path = join('./data/pdfs', new_filename); - if (!rename_no_replace(join('./data/pdfs', pdf.filename), new_path)) return fail(res, 'a file with that name already exists on disk'); - } - const updated = { ...pdf, display_name, filename: new_filename }; + const updated = { ...pdf, display_name, filename }; set_pdf(updated); ok(res, { pdf: updated }); }); diff --git a/world b/world new file mode 100644 index 0000000..e69de29