Many UX and correctness improvements
- Components URL reflects selected component (/components/:id), survives refresh - Word-split search: "0603 res" matches "Resistor 0603" - Left pane resizable with localStorage persistence - Field values rendered via central render_field_value() (units, URLs, extensible) - Fields sorted alphabetically in both detail view and edit dialog - Edit component dialog widened; field rows use shared grid columns (table-like) - No space between value and unit (supports prefix suffixes like k, M, µ) - Grid viewer highlights and scrolls to cell when navigating from component detail - Cell inventory overlay items are <a> tags — middle-click opens in new tab - PDF files stored with sanitized human-readable filename, not random ID - PDF rename also renames file on disk - Atomic rename via renameat2(RENAME_NOREPLACE) through tools/mv-sync - Fix .cell-thumb-link → .cell-thumb-preview (div, not anchor), cursor: zoom-in - Fix field name overflow in detail view (auto column width, overflow-wrap) - Fix link color: use --accent instead of browser default dark blue Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
40
server.mjs
40
server.mjs
@@ -63,6 +63,31 @@ function remove_image_file(img_id) {
|
||||
try { unlinkSync(join('./data/images', img_id)); } catch {}
|
||||
}
|
||||
|
||||
const MV_SYNC = new URL('../tools/mv-sync', import.meta.url).pathname;
|
||||
|
||||
// Atomically rename src -> dst, failing if dst already exists.
|
||||
// Uses renameat2(RENAME_NOREPLACE) via tools/mv-sync.
|
||||
// Throws on unexpected errors; returns false if dst exists.
|
||||
function rename_no_replace(src, dst) {
|
||||
try {
|
||||
execFileSync(MV_SYNC, [src, dst]);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e.status === 1 && e.stderr?.toString().includes('File exists')) return false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitize_pdf_filename(display_name) {
|
||||
const base = display_name
|
||||
.replace(/\.pdf$/i, '')
|
||||
.replace(/[^a-zA-Z0-9._\- ]/g, '')
|
||||
.trim()
|
||||
.replace(/\s+/g, '_')
|
||||
|| 'document';
|
||||
return base + '.pdf';
|
||||
}
|
||||
|
||||
function generate_pdf_thumb(pdf_path, thumb_prefix) {
|
||||
// Returns thumb filename (id-thumb.png) on success, null if pdftoppm unavailable.
|
||||
try {
|
||||
@@ -507,11 +532,15 @@ app.post('/api/pdfs', pdf_upload.single('file'), (req, res) => {
|
||||
return fail(res, 'a file with that name 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');
|
||||
const thumb_prefix = join('./data/pdfs', id + '-thumb');
|
||||
const thumb_file = generate_pdf_thumb(join('./data/pdfs', req.file.filename), thumb_prefix);
|
||||
const thumb_file = generate_pdf_thumb(final_path, thumb_prefix);
|
||||
const pdf = {
|
||||
id,
|
||||
filename: req.file.filename,
|
||||
filename,
|
||||
display_name,
|
||||
original_name: req.file.originalname,
|
||||
size: req.file.size,
|
||||
@@ -530,7 +559,12 @@ app.put('/api/pdfs/:id', (req, res) => {
|
||||
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 updated = { ...pdf, display_name };
|
||||
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 };
|
||||
set_pdf(updated);
|
||||
ok(res, { pdf: updated });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user