From 8d75b12354021bad82b112b2404ef9e4f6e2412a Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Sun, 18 Oct 2026 01:11:40 +0100 Subject: [PATCH] Keep media rename file extensions fixed --- CHANGELOG.md | 6 ++ build/package.player.json | 2 +- build/package.web.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/web/public/js/settings/media-library.js | 12 +++- src/web/views/settings/media-library/list.hbs | 1 + test/media-library.test.js | 60 +++++++++++++++++++ 8 files changed, 81 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8db5b2a..199603e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## 2.15.6 - 2026-10-18 + +### Changed + +- Media Library renaming now keeps the file extension fixed while allowing the filename to be edited. + ## 2.15.5 - 2026-10-17 ### Added diff --git a/build/package.player.json b/build/package.player.json index fffda15..52eab05 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.15.5", + "version": "2.15.6", "private": false, "description": "Pulse Signage player application bundle", "engines": { diff --git a/build/package.web.json b/build/package.web.json index 4960165..b265ec4 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.15.5", + "version": "2.15.6", "private": false, "description": "Pulse Signage web and bridge application bundle", "engines": { diff --git a/package-lock.json b/package-lock.json index 42cb27d..8fcab28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pulse-signage", - "version": "2.15.5", + "version": "2.15.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-signage", - "version": "2.15.5", + "version": "2.15.6", "dependencies": { "@sparticuz/chromium": "^149.0.0", "animate.css": "^4.1.1", diff --git a/package.json b/package.json index b2eadeb..31024dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.15.5", + "version": "2.15.6", "private": false, "description": "Pulse Signage application with MySQL and media storage", "engines": { diff --git a/src/web/public/js/settings/media-library.js b/src/web/public/js/settings/media-library.js index 54d6faf..c246a9c 100644 --- a/src/web/public/js/settings/media-library.js +++ b/src/web/public/js/settings/media-library.js @@ -556,9 +556,11 @@ function showPreviewModal() { if (modal && body) { const renameForm = modal.querySelector('[data-media-gallery-rename-form]'); const filenameInput = modal.querySelector('[data-media-gallery-filename]'); + const extensionLabel = modal.querySelector('[data-media-gallery-extension]'); const renameSave = modal.querySelector('[data-media-gallery-rename-save]'); const renameStatus = modal.querySelector('[data-media-gallery-rename-status]'); let activePreview = null; + let activeExtension = ''; if (renameForm && filenameInput && renameSave && renameStatus) { renameForm.addEventListener('submit', async function (event) { @@ -573,7 +575,7 @@ if (modal && body) { const response = await fetch(action, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, - body: JSON.stringify({ name: filenameInput.value }) + body: JSON.stringify({ name: filenameInput.value + activeExtension }) }); const result = await response.json(); if (!response.ok) throw new Error(result.error || 'Unable to rename media.'); @@ -586,7 +588,7 @@ if (modal && body) { label.title = result.name; } if (activePreview === preview) { - filenameInput.value = result.name; + filenameInput.value = activeExtension ? result.name.slice(0, -activeExtension.length) : result.name; if (title) title.textContent = result.name; const image = body.querySelector('img'); if (image) image.alt = result.name; @@ -625,7 +627,11 @@ if (modal && body) { const assetId = column && column.dataset.mediaId; renameForm.hidden = !assetId; renameForm.action = '/media-library/' + encodeURIComponent(assetId || '') + '/rename'; - filenameInput.value = name; + const extensionIndex = name.lastIndexOf('.'); + activeExtension = extensionIndex > 0 && extensionIndex < name.length - 1 ? name.slice(extensionIndex) : ''; + filenameInput.value = activeExtension ? name.slice(0, extensionIndex) : name; + filenameInput.maxLength = Math.max(1, 255 - activeExtension.length); + if (extensionLabel) extensionLabel.textContent = activeExtension; filenameInput.disabled = false; renameSave.disabled = false; renameStatus.hidden = true; diff --git a/src/web/views/settings/media-library/list.hbs b/src/web/views/settings/media-library/list.hbs index c79cf4a..18fb5a5 100644 --- a/src/web/views/settings/media-library/list.hbs +++ b/src/web/views/settings/media-library/list.hbs @@ -100,6 +100,7 @@
+
diff --git a/test/media-library.test.js b/test/media-library.test.js index 83f41b5..e27f5d1 100644 --- a/test/media-library.test.js +++ b/test/media-library.test.js @@ -408,5 +408,65 @@ test('media preview offers filename editing only with media update permission', const editable = renderMediaLibraryPage(data, '', { permissions: ['media-library.read', 'media-library.update'] }); assert.match(editable, /data-media-gallery-rename-form/); assert.match(editable, /maxlength="255" required data-media-gallery-filename/); + assert.match(editable, /data-media-gallery-extension/); assert.match(editable, /data-media-gallery-rename-save/); }); + +test('media preview rename keeps the original filename extension fixed', async () => { + const handlers = {}; + const submittedNames = []; + const input = { value: '', disabled: false, maxLength: 255 }; + const extension = { textContent: '' }; + const title = { textContent: '' }; + const body = { replaceChildren() {}, appendChild() {}, querySelector() { return null; } }; + const renameForm = { action: '', addEventListener(name, callback) { handlers['form:' + name] = callback; } }; + const renameSave = { disabled: false }; + const renameStatus = { hidden: true, classList: { add() {}, remove() {} } }; + const modal = { + querySelector(selector) { + return { + '[data-media-gallery-preview-body]': body, + '#media-gallery-preview-modal-label': title, + '[data-media-gallery-rename-form]': renameForm, + '[data-media-gallery-filename]': input, + '[data-media-gallery-extension]': extension, + '[data-media-gallery-rename-save]': renameSave, + '[data-media-gallery-rename-status]': renameStatus + }[selector] || null; + }, + addEventListener(name, callback) { handlers['modal:' + name] = callback; } + }; + const column = { dataset: { mediaId: '9' }, querySelector() { return null; } }; + const preview = { + dataset: { mediaName: 'original.png' }, + getAttribute(name) { return { 'data-media-path': '/media/uploads/id.png', 'data-media-type': 'image', 'data-media-name': 'original.png' }[name]; }, + closest() { return column; }, + setAttribute() {} + }; + const context = vm.createContext({ + URLSearchParams, + window: { location: { search: '' }, innerWidth: 1200, pulseModal: { show() {} } }, + document: { + getElementById() { return modal; }, + querySelector() { return null; }, + addEventListener(name, callback) { handlers['document:' + name] = callback; }, + createElement() { return {}; } + }, + fetch: async (_url, options) => { + if (options && options.body) { + submittedNames.push(JSON.parse(options.body).name); + return { ok: true, async json() { return { name: 'updated.png' }; } }; + } + return { ok: true, async json() { return { assets: [], nextOffset: 0, hasMore: false, total: 0 }; } }; + } + }); + vm.runInContext(fs.readFileSync(path.join(__dirname, '../src/web/public/js/settings/media-library.js'), 'utf8'), context); + + handlers['document:click']({ target: { closest() { return preview; } } }); + assert.equal(input.value, 'original'); + assert.equal(extension.textContent, '.png'); + assert.equal(input.maxLength, 251); + input.value = 'updated'; + await handlers['form:submit']({ preventDefault() {} }); + assert.deepEqual(submittedNames, ['updated.png']); +});