diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd7b7b..77dd8cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 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 ### Fixed diff --git a/build/package.player.json b/build/package.player.json index 1e8395f..2c735c2 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.21.5", + "version": "2.21.6", "private": false, "description": "Pulse Signage player application bundle", "engines": { diff --git a/build/package.web.json b/build/package.web.json index 6e458fc..bddc403 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.21.5", + "version": "2.21.6", "private": false, "description": "Pulse Signage web and bridge application bundle", "engines": { diff --git a/package-lock.json b/package-lock.json index 882e924..129bf46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pulse-signage", - "version": "2.21.5", + "version": "2.21.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-signage", - "version": "2.21.5", + "version": "2.21.6", "dependencies": { "@better-auth/passkey": "^1.7.13", "animate.css": "^4.1.1", diff --git a/package.json b/package.json index cfa21e2..052e1fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.21.5", + "version": "2.21.6", "private": false, "description": "Pulse Signage application with MySQL and media storage", "engines": { diff --git a/src/web/public/js/table/table-search.js b/src/web/public/js/table/table-search.js index 50c95b2..3da5827 100644 --- a/src/web/public/js/table/table-search.js +++ b/src/web/public/js/table/table-search.js @@ -191,6 +191,7 @@ var container = input.closest('[data-table-search-container]') || input.closest('.card') || null; input.value = String(currentUrl.searchParams.get(searchParam) || '').trim(); + var lastSearchValue = input.value; if (container) { container._cancelTableSearch = function () { window.clearTimeout(pendingSearchTimer); @@ -220,6 +221,11 @@ function updateSearch() { var query = String(input.value || '').trim(); + lastSearchValue = query; + if (pendingSearchTimer) { + window.clearTimeout(pendingSearchTimer); + pendingSearchTimer = null; + } var nextUrl = new URL(window.location.href); if (query) { @@ -233,6 +239,7 @@ } function scheduleSearchUpdate() { + lastSearchValue = String(input.value || '').trim(); if (container) { container._tableRequestSequence = (container._tableRequestSequence || 0) + 1; } @@ -247,7 +254,18 @@ input.addEventListener('input', 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) { if (event.key === 'Enter') { event.preventDefault(); diff --git a/test/table-pagination-height.test.js b/test/table-pagination-height.test.js index 384c221..d1d3f3a 100644 --- a/test/table-pagination-height.test.js +++ b/test/table-pagination-height.test.js @@ -159,6 +159,8 @@ function createNavigationHarness() { const footer = { outerHTML: 'original footer' }; const attributes = {}; const inputEvents = {}; + const timers = new Map(); + let nextTimerId = 0; const scroller = { scrollTop: 90 }; const input = { value: '', @@ -212,8 +214,12 @@ function createNavigationHarness() { location: { href: 'http://localhost/slides?search=demo&sort=name', assign(url) { navigations.push(url); } }, history: { state: { preserved: true } }, addEventListener(name, handler) { events[name] = handler; }, - clearTimeout() {}, - setTimeout(handler) { events.timer = handler; return 1; } + clearTimeout(id) { timers.delete(id); }, + setTimeout(handler) { + const id = ++nextTimerId; + timers.set(id, handler); + return id; + } } }; for (const method of ['pushState', 'replaceState']) { @@ -248,7 +254,14 @@ function createNavigationHarness() { requests[index].resolve({ ok, text: async () => text }); 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 () => { @@ -319,9 +332,28 @@ test('typing invalidates pending pagination and search still resets to page one' await harness.respond(0, 'outdated'); assert.equal(harness.body.outerHTML, 'original rows'); assert.equal(harness.input.value, 'new query'); - harness.events.timer(); + harness.runTimers(); assert.equal(new URL(harness.requests[1].url).searchParams.get('page'), null); await harness.respond(1, 'searched'); assert.equal(harness.body.outerHTML, 'searched rows'); 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'); +});