Add media filename renaming and preserve library filters
This commit is contained in:
1 parent
2fd059d35a
commit
eb724167bc
6 files changed
+310
-20
No files matched your search
@@ -4,6 +4,7 @@ const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const vm = require('node:vm');
|
||||
const sharp = require('sharp');
|
||||
require('../src/common');
|
||||
const mediaLibrary = require('../src/web/lib/media/library');
|
||||
@@ -212,3 +213,161 @@ test('media library upload and delete routes audit successful asset changes', as
|
||||
await fs.promises.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('media library delete redirects preserve filters for individual and bulk outcomes', async () => {
|
||||
const originalCountReferences = mediaLibrary.countMediaAssetReferences;
|
||||
const originalRemoveVariants = mediaLibrary.removeImageVariants;
|
||||
const handlers = {};
|
||||
let outcome = 'deleted';
|
||||
const query = { search: 'Photo & Video', type: 'image', usage: 'unused', sort: 'name-desc' };
|
||||
mediaLibrary.countMediaAssetReferences = async function () { return outcome === 'in-use' ? 1 : 0; };
|
||||
mediaLibrary.removeImageVariants = async function () {};
|
||||
try {
|
||||
registerMediaLibraryRoutes({
|
||||
get() {},
|
||||
post(route, ...middleware) { handlers[route] = middleware.at(-1); }
|
||||
}, {
|
||||
pool: {
|
||||
async query(sql) {
|
||||
if (sql.startsWith('SELECT')) {
|
||||
return [outcome === 'missing' ? [] : [{ id: 9, media_path: '/media/uploads/missing-test.png' }]];
|
||||
}
|
||||
return [[]];
|
||||
}
|
||||
},
|
||||
pages: {},
|
||||
upload: { single() { return function () {}; } },
|
||||
uploadDir: path.join(os.tmpdir(), 'pulse-media-filter-test', 'uploads'),
|
||||
requirePermission() { return function () {}; }
|
||||
});
|
||||
for (const route of ['/media-library/:id/delete', '/media-library/delete']) {
|
||||
for (outcome of ['deleted', 'in-use', 'missing', 'empty']) {
|
||||
let location;
|
||||
await handlers[route]({ query, params: { id: '9' }, body: { 'assetIds[]': outcome === 'empty' ? [] : ['9'] } }, {
|
||||
redirect(value) { location = value; }
|
||||
}, function (error) { throw error; });
|
||||
const redirect = new URL(location, 'http://localhost');
|
||||
assert.equal(redirect.pathname, '/media-library');
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
assert.equal(redirect.searchParams.get(key), value, route + ': ' + outcome + ': ' + key);
|
||||
}
|
||||
assert.ok(redirect.searchParams.get('message'));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mediaLibrary.countMediaAssetReferences = originalCountReferences;
|
||||
mediaLibrary.removeImageVariants = originalRemoveVariants;
|
||||
}
|
||||
});
|
||||
|
||||
test('media gallery restores controls and submits current filters with both delete actions', () => {
|
||||
const listeners = {};
|
||||
const controls = {};
|
||||
for (const [name, values] of Object.entries({
|
||||
search: [''], 'type-filter': ['all', 'image', 'video'],
|
||||
'usage-filter': ['all', 'used', 'unused'], sort: ['date-desc', 'name-desc', 'size-asc']
|
||||
})) {
|
||||
controls['[data-media-gallery-' + name + ']'] = {
|
||||
value: values[0], options: values.map(value => ({ value })), addEventListener() {}
|
||||
};
|
||||
}
|
||||
const gallery = {
|
||||
dataset: {}, querySelector(selector) { return controls[selector] || null; },
|
||||
querySelectorAll() { return []; },
|
||||
addEventListener(name, callback) { listeners[name] = callback; }
|
||||
};
|
||||
let submittedForm;
|
||||
const context = vm.createContext({
|
||||
URLSearchParams,
|
||||
window: { location: { search: '?search=Photo+%26+Video&type=image&usage=unused&sort=name-desc' }, innerWidth: 1200, confirm() { return true; } },
|
||||
document: {
|
||||
getElementById() { return null; },
|
||||
querySelector(selector) { return selector === '.media-gallery' ? gallery : null; },
|
||||
createElement() { return { appendChild() {}, submit() { submittedForm = this; } }; },
|
||||
body: { appendChild() {} }
|
||||
}
|
||||
});
|
||||
vm.runInContext(fs.readFileSync(path.join(__dirname, '../src/web/public/js/settings/media-library.js'), 'utf8'), context);
|
||||
assert.equal(controls['[data-media-gallery-search]'].value, 'Photo & Video');
|
||||
assert.equal(controls['[data-media-gallery-type-filter]'].value, 'image');
|
||||
assert.equal(controls['[data-media-gallery-usage-filter]'].value, 'unused');
|
||||
assert.equal(controls['[data-media-gallery-sort]'].value, 'name-desc');
|
||||
controls['[data-media-gallery-search]'].value = 'Updated & current';
|
||||
controls['[data-media-gallery-sort]'].value = 'size-asc';
|
||||
const form = { getAttribute() { return '/media-library/9/delete'; } };
|
||||
listeners.submit({ target: form });
|
||||
vm.runInContext("selectedAssetIds.add('9'); submitBulkDelete();", context);
|
||||
for (const target of [form, submittedForm]) {
|
||||
const url = new URL(target.action, 'http://localhost');
|
||||
assert.equal(url.searchParams.get('search'), 'Updated & current');
|
||||
assert.equal(url.searchParams.get('type'), 'image');
|
||||
assert.equal(url.searchParams.get('usage'), 'unused');
|
||||
assert.equal(url.searchParams.get('sort'), 'size-asc');
|
||||
assert.equal(url.searchParams.has('offset'), false);
|
||||
}
|
||||
});
|
||||
|
||||
test('empty media gallery retains filters and its refreshable results container', () => {
|
||||
const renderMediaLibraryPage = require('../src/web/routes/settings/media-library/list');
|
||||
const html = renderMediaLibraryPage({ assets: [], assetTotal: 0 }, '', { permissions: ['media-library.read', 'media-library.delete'] });
|
||||
assert.match(html, /data-media-gallery-search/);
|
||||
assert.match(html, /data-media-gallery-type-filter/);
|
||||
assert.match(html, /data-media-gallery-usage-filter/);
|
||||
assert.match(html, /data-media-gallery-sort/);
|
||||
assert.match(html, /class="media-gallery-scroll"/);
|
||||
assert.match(html, /data-media-gallery-no-results>/);
|
||||
});
|
||||
|
||||
test('media rename validates filenames and changes only metadata with update permission and auditing', async () => {
|
||||
const handlers = {};
|
||||
const queries = [];
|
||||
const events = [];
|
||||
const permissions = [];
|
||||
let exists = true;
|
||||
registerMediaLibraryRoutes({
|
||||
get() {}, post(route, ...middleware) { handlers[route] = middleware.at(-1); }
|
||||
}, {
|
||||
pool: {
|
||||
async query(sql, params) {
|
||||
queries.push({ sql, params });
|
||||
return [sql.startsWith('SELECT') && exists ? [{ id: 9, original_name: 'old.png', media_path: '/media/uploads/uuid.png' }] : []];
|
||||
}
|
||||
}, pages: {}, upload: { single() { return function () {}; } }, uploadDir: os.tmpdir(),
|
||||
getAuditUserId() { return 4; },
|
||||
requirePermission(permission) { permissions.push(permission); return function () {}; },
|
||||
recordRequestAuditEvent(_pool, _req, event) { events.push(event); }
|
||||
});
|
||||
assert.ok(permissions.includes('media-library.update'));
|
||||
const rename = handlers['/media-library/:id/rename'];
|
||||
const response = { statusCode: 200, status(code) { this.statusCode = code; return this; }, json(data) { this.data = data; } };
|
||||
const next = function (error) { throw error; };
|
||||
for (const name of ['', ' ', '../photo.png', 'a\\b.png', 'a\n.png', 'a?b.png', 'a'.repeat(256), ['photo.png'], '..']) {
|
||||
await rename({ params: { id: '9' }, body: { name } }, response, next);
|
||||
assert.equal(response.statusCode, 400);
|
||||
}
|
||||
assert.equal(queries.length, 0);
|
||||
response.statusCode = 200;
|
||||
await rename({ params: { id: '9' }, body: { name: ' New photo.png ' } }, response, next);
|
||||
assert.deepEqual(response.data, { id: 9, name: 'New photo.png' });
|
||||
assert.deepEqual(queries[1], {
|
||||
sql: 'UPDATE c_media_assets SET original_name = ?, modified_by = ? WHERE id = ?', params: ['New photo.png', 4, 9]
|
||||
});
|
||||
assert.equal(events[0].eventType, 'media.renamed');
|
||||
assert.deepEqual(events[0].details, { mediaPath: '/media/uploads/uuid.png', previousName: 'old.png', name: 'New photo.png' });
|
||||
await rename({ params: { id: '9' }, body: { name: 'old.png' } }, response, next);
|
||||
assert.equal(queries.filter(query => query.sql.startsWith('UPDATE')).length, 1);
|
||||
exists = false;
|
||||
await rename({ params: { id: '9' }, body: { name: 'missing.png' } }, response, next);
|
||||
assert.equal(response.statusCode, 404);
|
||||
});
|
||||
|
||||
test('media preview offers filename editing only with media update permission', () => {
|
||||
const renderMediaLibraryPage = require('../src/web/routes/settings/media-library/list');
|
||||
const data = { assets: [], assetTotal: 0 };
|
||||
const readOnly = renderMediaLibraryPage(data, '', { permissions: ['media-library.read'] });
|
||||
assert.doesNotMatch(readOnly, /data-media-gallery-rename-form/);
|
||||
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-rename-save/);
|
||||
});
|
||||
Reference in new issue
Block a user