Release 2.21.6: sort editor regions by name
This commit is contained in:
1 parent
675483624b
commit
7748129307
8 files changed
+113
-8
No files matched your search
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## 2.21.6 - 2026-10-27
|
## 2.21.6 - 2026-10-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Template and slide region editors list regions alphabetically, including the layout and animation selectors.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- List searches no longer issue duplicate requests when the search field loses focus, avoiding intermittent missed clicks on refreshed row buttons.
|
- List searches no longer issue duplicate requests when the search field loses focus, avoiding intermittent missed clicks on refreshed row buttons.
|
||||||
|
|||||||
@@ -223,7 +223,9 @@ export function createSlideAnimationEditor(options) {
|
|||||||
openButton.addEventListener('click', function () {
|
openButton.addEventListener('click', function () {
|
||||||
if (!allRegions().length || !window.pulseModal) return;
|
if (!allRegions().length || !window.pulseModal) return;
|
||||||
regionSelect.replaceChildren();
|
regionSelect.replaceChildren();
|
||||||
regions().forEach(function (region) {
|
window.templateDesignerUtils.sortByName(regions(), function (region) {
|
||||||
|
return region.label || region.region_key;
|
||||||
|
}).forEach(function (region) {
|
||||||
var option = document.createElement('option');
|
var option = document.createElement('option');
|
||||||
option.value = region.region_key;
|
option.value = region.region_key;
|
||||||
option.textContent = region.label || region.region_key;
|
option.textContent = region.label || region.region_key;
|
||||||
|
|||||||
@@ -951,7 +951,11 @@ import { createSlideAnimationEditor } from '/assets/js/slides/slide-animation-ed
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var editableRegions = template.regions.filter(function (region) { return !(region.read_only === true || Number(region.read_only) === 1); });
|
var editableRegions = window.templateDesignerUtils.sortByName(template.regions.filter(function (region) {
|
||||||
|
return !(region.read_only === true || Number(region.read_only) === 1);
|
||||||
|
}), function (region) {
|
||||||
|
return region.label || region.region_key;
|
||||||
|
});
|
||||||
templateFields.innerHTML = editableRegions.length ? editableRegions.map(function (region) {
|
templateFields.innerHTML = editableRegions.length ? editableRegions.map(function (region) {
|
||||||
return getRegionEditorCardHtml(region) || '<div class="muted">Region editor unavailable.</div>';
|
return getRegionEditorCardHtml(region) || '<div class="muted">Region editor unavailable.</div>';
|
||||||
}).join('') : '<div class="muted">This template has no editable regions.</div>';
|
}).join('') : '<div class="muted">This template has no editable regions.</div>';
|
||||||
|
|||||||
@@ -150,7 +150,10 @@ export function createSlideLayoutEditor(options) {
|
|||||||
openButton.disabled = active || !regions().length;
|
openButton.disabled = active || !regions().length;
|
||||||
animationButton.disabled = active || !allRegions().length;
|
animationButton.disabled = active || !allRegions().length;
|
||||||
if (!active || sharedSaving) return;
|
if (!active || sharedSaving) return;
|
||||||
if (!selected()) selectedKey = regions().length ? regions()[0].region_key : '';
|
var orderedRegions = window.templateDesignerUtils.sortByName(regions(), function (region) {
|
||||||
|
return region.label || region.region_key;
|
||||||
|
});
|
||||||
|
if (!selected()) selectedKey = orderedRegions.length ? orderedRegions[0].region_key : '';
|
||||||
var region = selected();
|
var region = selected();
|
||||||
var box = region && effective(region);
|
var box = region && effective(region);
|
||||||
Object.keys(fields).forEach(function (field) {
|
Object.keys(fields).forEach(function (field) {
|
||||||
@@ -216,7 +219,9 @@ export function createSlideLayoutEditor(options) {
|
|||||||
home = stage.parentNode;
|
home = stage.parentNode;
|
||||||
nextSibling = stage.nextSibling;
|
nextSibling = stage.nextSibling;
|
||||||
regionSelect.replaceChildren();
|
regionSelect.replaceChildren();
|
||||||
regions().forEach(function (region) {
|
window.templateDesignerUtils.sortByName(regions(), function (region) {
|
||||||
|
return region.label || region.region_key;
|
||||||
|
}).forEach(function (region) {
|
||||||
var option = document.createElement('option');
|
var option = document.createElement('option');
|
||||||
option.value = region.region_key;
|
option.value = region.region_key;
|
||||||
option.textContent = region.label || region.region_key;
|
option.textContent = region.label || region.region_key;
|
||||||
|
|||||||
@@ -84,6 +84,16 @@
|
|||||||
return String(card.querySelector('[name="region_name[]"]').value || '').trim();
|
return String(card.querySelector('[name="region_name[]"]').value || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortByName(items, getName) {
|
||||||
|
return Array.prototype.map.call(items || [], function (item, index) {
|
||||||
|
return { item: item, index: index, name: String(getName(item) || '').trim() };
|
||||||
|
}).sort(function (left, right) {
|
||||||
|
return left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }) || left.index - right.index;
|
||||||
|
}).map(function (entry) {
|
||||||
|
return entry.item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getAnimationConfig(card) {
|
function getAnimationConfig(card) {
|
||||||
var hiddenAnimationInput = card.querySelector('[name="region_animation_json[]"]');
|
var hiddenAnimationInput = card.querySelector('[name="region_animation_json[]"]');
|
||||||
var animationConfig = normalizeAnimationConfig(hiddenAnimationInput ? hiddenAnimationInput.value || '{}' : '{}');
|
var animationConfig = normalizeAnimationConfig(hiddenAnimationInput ? hiddenAnimationInput.value || '{}' : '{}');
|
||||||
@@ -354,6 +364,7 @@
|
|||||||
valueOrDefault: valueOrDefault,
|
valueOrDefault: valueOrDefault,
|
||||||
clamp: clamp,
|
clamp: clamp,
|
||||||
getRegionName: getRegionName,
|
getRegionName: getRegionName,
|
||||||
|
sortByName: sortByName,
|
||||||
syncRegionIdentity: syncRegionIdentity,
|
syncRegionIdentity: syncRegionIdentity,
|
||||||
readCard: readCard,
|
readCard: readCard,
|
||||||
writeCard: writeCard,
|
writeCard: writeCard,
|
||||||
|
|||||||
@@ -1237,8 +1237,11 @@
|
|||||||
if (title) {
|
if (title) {
|
||||||
title.textContent = label;
|
title.textContent = label;
|
||||||
}
|
}
|
||||||
if (index >= 0 && regionSelect.options[index]) {
|
var option = Array.prototype.find.call(regionSelect.options, function (item) {
|
||||||
regionSelect.options[index].textContent = label;
|
return item.value === String(index);
|
||||||
|
});
|
||||||
|
if (option) {
|
||||||
|
option.textContent = label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1351,11 +1354,13 @@
|
|||||||
updateCanvasSizeLock();
|
updateCanvasSizeLock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var orderedCards = utils.sortByName(cards, getRegionName);
|
||||||
regionSelect.disabled = false;
|
regionSelect.disabled = false;
|
||||||
if (selectedIndex < 0 || selectedIndex >= cards.length) {
|
if (selectedIndex < 0 || selectedIndex >= cards.length) {
|
||||||
selectedIndex = 0;
|
selectedIndex = cards.indexOf(orderedCards[0]);
|
||||||
}
|
}
|
||||||
cards.forEach(function (card, index) {
|
orderedCards.forEach(function (card) {
|
||||||
|
var index = cards.indexOf(card);
|
||||||
var option = document.createElement('option');
|
var option = document.createElement('option');
|
||||||
option.value = String(index);
|
option.value = String(index);
|
||||||
option.textContent = getRegionName(card) || ('Region ' + (index + 1));
|
option.textContent = getRegionName(card) || ('Region ' + (index + 1));
|
||||||
|
|||||||
@@ -75,6 +75,15 @@ test('slide form queues editor image cleanup on save and close', () => {
|
|||||||
assert.ok(slideFormSource.includes("regionMediaController.queueUploadCleanup(slideFormEditorController.getImageUploadCleanupPaths())"));
|
assert.ok(slideFormSource.includes("regionMediaController.queueUploadCleanup(slideFormEditorController.getImageUploadCleanupPaths())"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('slide region cards and layout and animation selectors use name ordering', () => {
|
||||||
|
const layoutEditorSource = fs.readFileSync(require.resolve('../src/web/public/js/slides/slide-layout-editor.js'), 'utf8');
|
||||||
|
const animationEditorSource = fs.readFileSync(require.resolve('../src/web/public/js/slides/slide-animation-editor.js'), 'utf8');
|
||||||
|
|
||||||
|
assert.match(slideFormSource, /window\.templateDesignerUtils\.sortByName\(template\.regions\.filter/);
|
||||||
|
assert.match(layoutEditorSource, /window\.templateDesignerUtils\.sortByName\(regions\(\)/);
|
||||||
|
assert.match(animationEditorSource, /window\.templateDesignerUtils\.sortByName\(regions\(\)/);
|
||||||
|
});
|
||||||
|
|
||||||
test('media library selections keep image and video remove controls', () => {
|
test('media library selections keep image and video remove controls', () => {
|
||||||
assert.ok(mediaPickerSource.includes("var removeAction = isVideo ? 'region-video' : isQrImage ? 'region-qr-image' : 'region-image';"));
|
assert.ok(mediaPickerSource.includes("var removeAction = isVideo ? 'region-video' : isQrImage ? 'region-qr-image' : 'region-image';"));
|
||||||
assert.ok(mediaPickerSource.includes('data-remove-\' + removeAction'));
|
assert.ok(mediaPickerSource.includes('data-remove-\' + removeAction'));
|
||||||
|
|||||||
@@ -381,6 +381,71 @@ test('template designer generates default region names per type', () => {
|
|||||||
assert.ok(script.includes('Math.max(count, suffix)'));
|
assert.ok(script.includes('Math.max(count, suffix)'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('region name sorting is case-insensitive, stable, and used by the template editor', () => {
|
||||||
|
const context = vm.createContext({ window: {} });
|
||||||
|
const utilsScript = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer-utils.js'), 'utf8');
|
||||||
|
vm.runInContext(utilsScript, context);
|
||||||
|
const items = [
|
||||||
|
{ name: 'Zulu' },
|
||||||
|
{ name: 'alpha' },
|
||||||
|
{ name: 'Alpha' },
|
||||||
|
{ name: 'bravo' }
|
||||||
|
];
|
||||||
|
const sorted = Array.from(context.window.templateDesignerUtils.sortByName(items, item => item.name));
|
||||||
|
|
||||||
|
assert.deepEqual(sorted, [items[1], items[2], items[3], items[0]]);
|
||||||
|
const designerSource = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer.js'), 'utf8');
|
||||||
|
assert.match(designerSource, /utils\.sortByName\(cards, getRegionName\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('template region selector sorts options and preserves the selected card index', () => {
|
||||||
|
const designerSource = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer.js'), 'utf8');
|
||||||
|
const match = designerSource.match(/ function renderRegionSidebar\(\) \{[^]*?\n \}/);
|
||||||
|
assert.ok(match);
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{ name: 'Zulu', hidden: false },
|
||||||
|
{ name: 'alpha', hidden: false },
|
||||||
|
{ name: 'Bravo', hidden: false }
|
||||||
|
];
|
||||||
|
const regionSelect = {
|
||||||
|
options: [],
|
||||||
|
disabled: false,
|
||||||
|
value: '',
|
||||||
|
set innerHTML(_value) { this.options = []; },
|
||||||
|
appendChild(option) { this.options.push(option); }
|
||||||
|
};
|
||||||
|
const regionList = { innerHTML: '' };
|
||||||
|
const context = vm.createContext({
|
||||||
|
cards,
|
||||||
|
regionSelect,
|
||||||
|
regionList,
|
||||||
|
selectedIndex: -1,
|
||||||
|
utils: { sortByName(items, getName) { return Array.from(items).sort((left, right) => getName(left).localeCompare(getName(right), undefined, { sensitivity: 'base' })); } },
|
||||||
|
getCards: () => cards,
|
||||||
|
getRegionName: card => card.name,
|
||||||
|
document: { createElement: () => ({}) },
|
||||||
|
updateCanvasSizeLock() {},
|
||||||
|
validateRegionNames() {},
|
||||||
|
updatePreviewCardVisibility() {}
|
||||||
|
});
|
||||||
|
|
||||||
|
vm.runInContext(match[0], context);
|
||||||
|
context.renderRegionSidebar();
|
||||||
|
assert.deepEqual(regionSelect.options.map(option => [option.textContent, option.value]), [
|
||||||
|
['alpha', '1'],
|
||||||
|
['Bravo', '2'],
|
||||||
|
['Zulu', '0']
|
||||||
|
]);
|
||||||
|
assert.equal(regionSelect.value, '1');
|
||||||
|
assert.deepEqual(cards.map(card => card.hidden), [true, false, true]);
|
||||||
|
|
||||||
|
context.selectedIndex = 0;
|
||||||
|
context.renderRegionSidebar();
|
||||||
|
assert.equal(regionSelect.value, '0');
|
||||||
|
assert.deepEqual(cards.map(card => card.hidden), [false, true, true]);
|
||||||
|
});
|
||||||
|
|
||||||
test('template designer warns when invalid region names block save', () => {
|
test('template designer warns when invalid region names block save', () => {
|
||||||
const script = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer.js'), 'utf8');
|
const script = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer.js'), 'utf8');
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user