PDF: separate display name and filename; show filename in picker; fix rename

- Upload dialog now has distinct display name + filename fields, both pre-filled
  from the uploaded file but independently editable
- Rename in file picker shows and edits both display name and filename separately
- Filename conflict checked against both KV store and disk (via rename_no_replace)
- Display name and filename are fully independent — no longer derived from each other
- Add find_pdf_references() helper in storage.mjs for future use
- CSS: fp-name-wrap shows display name + dim monospace filename below it;
  rename mode stacks two inputs; fp-field-label for upload form labels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 11:45:50 +00:00
parent d3df99a8f0
commit 4813a65a53
8 changed files with 116 additions and 42 deletions

View File

@@ -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);

View File

@@ -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');

View File

@@ -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] {

View File

@@ -566,7 +566,16 @@
<div class="form-section-label">Upload new PDF</div>
<div class="fp-upload-row">
<input type="file" id="fp-file-input" accept=".pdf,application/pdf">
<input type="text" id="fp-upload-name" placeholder="Display name" autocomplete="off">
</div>
<div class="fp-upload-row">
<label class="fp-field-label">Display name</label>
<input type="text" id="fp-upload-name" placeholder="Human readable label" autocomplete="off">
</div>
<div class="fp-upload-row">
<label class="fp-field-label">Filename on disk</label>
<input type="text" id="fp-upload-filename" placeholder="e.g. lm741.pdf" autocomplete="off" spellcheck="false">
</div>
<div class="fp-upload-row">
<button type="button" class="btn btn-primary btn-sm" id="fp-upload-btn">Upload</button>
</div>
</div>