Release 2.21.6: prevent duplicate list searches
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m33s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 41s

This commit is contained in:
lzstealth committed 2026-10-27 18:56:40 +00:00
1 parent 9e988c307e
commit 675483624b
7 files changed
+66 -10

No files matched your search

+6
View File
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## 2.21.6 - 2026-10-27
### Fixed
- List searches no longer issue duplicate requests when the search field loses focus, avoiding intermittent missed clicks on refreshed row buttons.
## 2.21.5 - 2026-10-27 ## 2.21.5 - 2026-10-27
### Fixed ### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage-player", "name": "pulse-signage-player",
"version": "2.21.5", "version": "2.21.6",
"private": false, "private": false,
"description": "Pulse Signage player application bundle", "description": "Pulse Signage player application bundle",
"engines": { "engines": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage-web", "name": "pulse-signage-web",
"version": "2.21.5", "version": "2.21.6",
"private": false, "private": false,
"description": "Pulse Signage web and bridge application bundle", "description": "Pulse Signage web and bridge application bundle",
"engines": { "engines": {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.21.5", "version": "2.21.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.21.5", "version": "2.21.6",
"dependencies": { "dependencies": {
"@better-auth/passkey": "^1.7.13", "@better-auth/passkey": "^1.7.13",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.21.5", "version": "2.21.6",
"private": false, "private": false,
"description": "Pulse Signage application with MySQL and media storage", "description": "Pulse Signage application with MySQL and media storage",
"engines": { "engines": {
+19 -1
View File
@@ -191,6 +191,7 @@
var container = input.closest('[data-table-search-container]') || input.closest('.card') || null; var container = input.closest('[data-table-search-container]') || input.closest('.card') || null;
input.value = String(currentUrl.searchParams.get(searchParam) || '').trim(); input.value = String(currentUrl.searchParams.get(searchParam) || '').trim();
var lastSearchValue = input.value;
if (container) { if (container) {
container._cancelTableSearch = function () { container._cancelTableSearch = function () {
window.clearTimeout(pendingSearchTimer); window.clearTimeout(pendingSearchTimer);
@@ -220,6 +221,11 @@
function updateSearch() { function updateSearch() {
var query = String(input.value || '').trim(); var query = String(input.value || '').trim();
lastSearchValue = query;
if (pendingSearchTimer) {
window.clearTimeout(pendingSearchTimer);
pendingSearchTimer = null;
}
var nextUrl = new URL(window.location.href); var nextUrl = new URL(window.location.href);
if (query) { if (query) {
@@ -233,6 +239,7 @@
} }
function scheduleSearchUpdate() { function scheduleSearchUpdate() {
lastSearchValue = String(input.value || '').trim();
if (container) { if (container) {
container._tableRequestSequence = (container._tableRequestSequence || 0) + 1; container._tableRequestSequence = (container._tableRequestSequence || 0) + 1;
} }
@@ -247,7 +254,18 @@
input.addEventListener('input', scheduleSearchUpdate); input.addEventListener('input', scheduleSearchUpdate);
input.addEventListener('search', scheduleSearchUpdate); input.addEventListener('search', scheduleSearchUpdate);
input.addEventListener('change', updateSearch); input.addEventListener('change', function () {
var query = String(input.value || '').trim();
if (query === lastSearchValue) {
if (pendingSearchTimer) {
window.clearTimeout(pendingSearchTimer);
pendingSearchTimer = null;
updateSearch();
}
return;
}
updateSearch();
});
input.addEventListener('keydown', function (event) { input.addEventListener('keydown', function (event) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
+36 -4
View File
@@ -159,6 +159,8 @@ function createNavigationHarness() {
const footer = { outerHTML: 'original footer' }; const footer = { outerHTML: 'original footer' };
const attributes = {}; const attributes = {};
const inputEvents = {}; const inputEvents = {};
const timers = new Map();
let nextTimerId = 0;
const scroller = { scrollTop: 90 }; const scroller = { scrollTop: 90 };
const input = { const input = {
value: '', value: '',
@@ -212,8 +214,12 @@ function createNavigationHarness() {
location: { href: 'http://localhost/slides?search=demo&sort=name', assign(url) { navigations.push(url); } }, location: { href: 'http://localhost/slides?search=demo&sort=name', assign(url) { navigations.push(url); } },
history: { state: { preserved: true } }, history: { state: { preserved: true } },
addEventListener(name, handler) { events[name] = handler; }, addEventListener(name, handler) { events[name] = handler; },
clearTimeout() {}, clearTimeout(id) { timers.delete(id); },
setTimeout(handler) { events.timer = handler; return 1; } setTimeout(handler) {
const id = ++nextTimerId;
timers.set(id, handler);
return id;
}
} }
}; };
for (const method of ['pushState', 'replaceState']) { for (const method of ['pushState', 'replaceState']) {
@@ -248,7 +254,14 @@ function createNavigationHarness() {
requests[index].resolve({ ok, text: async () => text }); requests[index].resolve({ ok, text: async () => text });
await new Promise(resolve => setImmediate(resolve)); await new Promise(resolve => setImmediate(resolve));
} }
return { sandbox, container, events, requests, navigations, history, body, footer, attributes, input, inputEvents, scroller, click, respond }; return {
sandbox, container, events, requests, navigations, history, body, footer, attributes, input, inputEvents, scroller, click, respond,
runTimers() {
const pending = Array.from(timers.values());
timers.clear();
pending.forEach(handler => handler());
}
};
} }
test('pagination replaces only table results and pushes a URL preserving search and sort', async () => { test('pagination replaces only table results and pushes a URL preserving search and sort', async () => {
@@ -319,9 +332,28 @@ test('typing invalidates pending pagination and search still resets to page one'
await harness.respond(0, 'outdated'); await harness.respond(0, 'outdated');
assert.equal(harness.body.outerHTML, 'original rows'); assert.equal(harness.body.outerHTML, 'original rows');
assert.equal(harness.input.value, 'new query'); assert.equal(harness.input.value, 'new query');
harness.events.timer(); harness.runTimers();
assert.equal(new URL(harness.requests[1].url).searchParams.get('page'), null); assert.equal(new URL(harness.requests[1].url).searchParams.get('page'), null);
await harness.respond(1, 'searched'); await harness.respond(1, 'searched');
assert.equal(harness.body.outerHTML, 'searched rows'); assert.equal(harness.body.outerHTML, 'searched rows');
assert.equal(harness.history[0].method, 'replaceState'); assert.equal(harness.history[0].method, 'replaceState');
}); });
test('blur flushes a pending search once and does not repeat an active search', async () => {
const pendingHarness = createNavigationHarness();
pendingHarness.input.value = 'button race';
pendingHarness.inputEvents.input();
pendingHarness.inputEvents.change();
assert.equal(pendingHarness.requests.length, 1);
pendingHarness.runTimers();
assert.equal(pendingHarness.requests.length, 1);
const activeHarness = createNavigationHarness();
activeHarness.input.value = 'button race';
activeHarness.inputEvents.input();
activeHarness.runTimers();
assert.equal(activeHarness.requests.length, 1);
activeHarness.inputEvents.change();
assert.equal(activeHarness.requests.length, 1);
await activeHarness.respond(0, 'searched');
});