fix: keep Tags prompt results on the current filter

Fixes #178
This commit is contained in:
Vito Sansevero
2026-10-16 05:48:25 -07:00
parent 80ba034fbf
commit cccf872642
2 changed files with 94 additions and 7 deletions
+71
View File
@@ -0,0 +1,71 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
function fixture() {
const requests = [];
const grid = { replaceChildren() {}, appendChild() {} };
const context = vm.createContext({ window: {}, URLSearchParams, console: { error() {} },
requestAnimationFrame() {}, document: { getElementById: id => id === 'tagPromptsGrid' ? grid : null,
querySelector: () => null, createElement: () => ({ appendChild() {} }) },
});
const Manager = vm.runInContext(fs.readFileSync(path.join(__dirname, '../../web/js/tags-page.js'), 'utf8') + '\nTagsPageManager;', context);
const manager = Object.create(Manager.prototype);
Object.assign(manager, { selectedTags: ['cats'], filterMode: 'and', promptLimit: 20,
promptOffset: 0, isLoadingPrompts: false, prompts: [], hasMorePrompts: false,
fetchJson(url) { return new Promise((resolve, reject) => requests.push({ url, resolve, reject })); },
renderTagPrompts() {}, appendTagPrompts() {}, updatePromptCountBadge() {}, updateLoadMoreButton() {},
});
const reply = (index, text, more = false) => requests[index].resolve({ success: true,
prompts: [{ text }], pagination: { has_more: more, total: more ? 40 : 1 } });
return { manager, requests, reply };
}
test('changing a filter starts a new request and late results cannot overwrite it', async () => {
const { manager, requests, reply } = fixture();
const old = manager.loadTagPrompts();
manager.selectedTags = ['dogs'];
const current = manager.loadTagPrompts();
assert.equal(requests.length, 2);
assert.match(requests[1].url, /dogs/);
reply(1, 'dog prompt'); await current;
reply(0, 'cat prompt', true); await old;
assert.equal(manager.prompts[0].text, 'dog prompt');
assert.equal(manager.promptTotal, 1);
assert.equal(manager.hasMorePrompts, false);
});
test('clearing selection invalidates pending results', async () => {
const { manager, reply } = fixture();
const old = manager.loadTagPrompts();
manager.selectedTags = [];
manager.clearPrompts();
reply(0, 'cat prompt'); await old;
assert.equal(manager.prompts.length, 0);
assert.equal(manager.promptTotal, 0);
assert.equal(manager.isLoadingPrompts, false);
});
test('a stale pagination response cannot append rows from the previous filter', async () => {
const { manager, requests, reply } = fixture();
Object.assign(manager, { prompts: [{ text: 'cat first page' }], hasMorePrompts: true, promptOffset: 20 });
const old = manager.loadMoreTagPrompts();
manager.selectedTags = ['dogs'];
const current = manager.loadTagPrompts();
assert.equal(requests.length, 2);
reply(1, 'dog prompt'); await current;
reply(0, 'cat second page', true); await old;
assert.equal(manager.prompts.length, 1);
assert.equal(manager.prompts[0].text, 'dog prompt');
assert.equal(manager.promptOffset, 20);
});
test('a stale failure cannot finish the new request or clear its loading state', async () => {
const { manager, requests, reply } = fixture();
const old = manager.loadTagPrompts();
manager.selectedTags = ['dogs'];
const current = manager.loadTagPrompts();
assert.equal(requests.length, 2);
requests[0].reject(new Error('old failure')); await old;
assert.equal(manager.isLoadingPrompts, true);
reply(1, 'dog prompt'); await current;
assert.equal(manager.isLoadingPrompts, false);
assert.equal(manager.prompts[0].text, 'dog prompt');
});
+23 -7
View File
@@ -583,8 +583,12 @@ class TagsPageManager {
// ── Prompts Display ───────────────────────────────────────
async loadTagPrompts() {
if (this.isLoadingPrompts) return;
if (this.selectedTags.length === 0) return;
if (this.selectedTags.length === 0) {
this.clearPrompts();
return;
}
const request = {};
this.promptRequest = request;
this.isLoadingPrompts = true;
this.promptOffset = 0;
@@ -622,6 +626,7 @@ class TagsPageManager {
}
const data = await this.fetchJson(url);
if (this.promptRequest !== request) return;
if (data.success) {
this.prompts = data.prompts;
@@ -634,6 +639,7 @@ class TagsPageManager {
requestAnimationFrame(() => this.refreshPromptObserver());
}
} catch (err) {
if (this.promptRequest !== request) return;
console.error('Failed to load tag prompts:', err);
if (grid) {
grid.replaceChildren();
@@ -643,12 +649,14 @@ class TagsPageManager {
grid.appendChild(errDiv);
}
} finally {
this.isLoadingPrompts = false;
if (this.promptRequest === request) this.isLoadingPrompts = false;
}
}
async loadMoreTagPrompts() {
if (this.isLoadingPrompts || !this.hasMorePrompts) return;
const request = {};
this.promptRequest = request;
this.isLoadingPrompts = true;
const spinner = document.querySelector('#tagPromptsLoader .prompts-loader-spinner');
@@ -675,6 +683,7 @@ class TagsPageManager {
}
const data = await this.fetchJson(url);
if (this.promptRequest !== request) return;
if (data.success) {
const startIndex = this.prompts.length;
@@ -685,13 +694,16 @@ class TagsPageManager {
this.updatePromptCountBadge();
}
} catch (err) {
if (this.promptRequest !== request) return;
console.error('Failed to load more prompts:', err);
this.hasMorePrompts = false;
} finally {
this.isLoadingPrompts = false;
if (spinner) spinner.classList.add('hidden');
this.updateLoadMoreButton();
requestAnimationFrame(() => this.refreshPromptObserver());
if (this.promptRequest === request) {
this.isLoadingPrompts = false;
if (spinner) spinner.classList.add('hidden');
this.updateLoadMoreButton();
requestAnimationFrame(() => this.refreshPromptObserver());
}
}
}
@@ -987,6 +999,9 @@ class TagsPageManager {
}
clearPrompts() {
this.promptRequest = null;
this.isLoadingPrompts = false;
this.promptOffset = 0;
this.prompts = [];
this.promptTotal = 0;
this.hasMorePrompts = false;
@@ -1091,6 +1106,7 @@ class TagsPageManager {
}
destroy() {
this.promptRequest = null;
this.destroyInfiniteScroll();
this.hideContextMenu();
clearTimeout(this.searchTimer);