fix(webui): sp4 second review round, a project filter stays while its organization does not answer, a gone filter is cleaned only from the address its read started on, a read in flight never paints over the not-on-your-list page (#1873, lane sp4)
This commit is contained in:
1 parent
d7b98b4698
commit
79df01f1de
2 files changed
+77
-7
No files matched your search
@@ -132,18 +132,28 @@
|
||||
// Only the latest read lands: a filter change and the poll can both be reading.
|
||||
// A read that a newer one replaced keeps nothing; `newest` lets a view wait for the read that does.
|
||||
let readSeq = 0, newest = null;
|
||||
const reload = () => { const n = ++readSeq; return (newest = read().then(next => { if (n !== readSeq) return false; snap = next; allowed = next.takes; canWrite = next.writes; return prune() ? reload() : true; })); };
|
||||
// The address the read starts from goes to prune, so a cleanup never touches a page opened since.
|
||||
const reload = () => { const n = ++readSeq, at = location.hash; return (newest = read().then(next => { if (n !== readSeq) return false; snap = next; allowed = next.takes; canWrite = next.writes; return prune(at) ? reload() : true; })); };
|
||||
async function refilter() { paint(); try { if (await reload()) paint(); } catch { /* the next poll shows the failed read */ } }
|
||||
// A filter naming an org or project that is gone falls back to all.
|
||||
// The address drops it too, so it never names a filter the board is not using. A dropped project
|
||||
// was filtered on the server, so it returns true and the read runs again without it.
|
||||
function prune() {
|
||||
// A project is gone only when its organization is off the list, or answered with its projects and
|
||||
// the project is not among them and no task came back for it. An organization that is down,
|
||||
// starting or timed out, or whose projects read failed, lists no projects, so its filter stays.
|
||||
// `at` is the address the read started from.
|
||||
function prune(at) {
|
||||
const gone = {};
|
||||
if (!scope && filter.org && !snap.orgs.some(o => o.id === filter.org)) { filter.org = ''; gone.org = null; }
|
||||
if (filter.project && filter.project !== '-' && !snap.projects.some(p => `${p.org}:${p.id}` === filter.project)) { filter.project = ''; gone.project = null; }
|
||||
// After this render: bus.js drops a render whose address changed while it ran. Only if the address
|
||||
// has not moved on, and with a repaint in case the render in flight was the one dropped.
|
||||
if (Object.keys(gone).length) { const at = location.hash; setTimeout(() => { if (location.hash === at) ws()?.setQuery?.(gone, true); }, 0); }
|
||||
if (filter.project && filter.project !== '-') {
|
||||
const po = snap.orgs.find(o => o.id === filter.project.split(':')[0]);
|
||||
const answered = !po || (po.state === 'ok' && !po.errors?.projects);
|
||||
if (answered && !snap.tasks.length && !snap.projects.some(p => `${p.org}:${p.id}` === filter.project)) { filter.project = ''; gone.project = null; }
|
||||
}
|
||||
// After this render: bus.js drops a render whose address changed while it ran. Only while the
|
||||
// address is still the one the read started from, and with a repaint in case the render in
|
||||
// flight was the one dropped.
|
||||
if (Object.keys(gone).length) setTimeout(() => { if (location.hash === at) ws()?.setQuery?.(gone, true); }, 0);
|
||||
return 'project' in gone;
|
||||
}
|
||||
|
||||
@@ -327,7 +337,8 @@
|
||||
// organization's own config whether it takes task changes.
|
||||
async function view(q = new URLSearchParams(), opts = {}) {
|
||||
// An address that names an organization no id can match is not the system board.
|
||||
if (typeof opts.scope === 'string' && opts.scope && !ORG_ID.test(opts.scope)) return `<section class="ws tb" data-tasks-board><div class="ws-vh"><h1 tabindex="-1">Tasks</h1></div><p class="ws-panel tight note">This organization is not on your list.</p></section>`;
|
||||
// A read still in flight from the board left behind lands nowhere and paints nothing.
|
||||
if (typeof opts.scope === 'string' && opts.scope && !ORG_ID.test(opts.scope)) { ++readSeq; snap = null; return `<section class="ws tb" data-tasks-board><div class="ws-vh"><h1 tabindex="-1">Tasks</h1></div><p class="ws-panel tight note">This organization is not on your list.</p></section>`; }
|
||||
const s = typeof opts.scope === 'string' && ORG_ID.test(opts.scope) ? opts.scope : null;
|
||||
if (s !== scope) snap = null;
|
||||
scope = s; writesIn = typeof opts.writesIn === 'function' ? opts.writesIn : null;
|
||||
|
||||
@@ -204,3 +204,62 @@ test('a card opened from the system board opens the task in its organization', a
|
||||
await b.evaluate('document.querySelector("#tb-dialog [data-tb-cancel]").click()');
|
||||
await wait('location.hash === "#/orgs/beta/tasks" && !document.querySelector("#tb-dialog").open');
|
||||
});
|
||||
|
||||
// Holds each task board read for ?hold= ms (or window.__hold, set later) and counts the reads that
|
||||
// started and the ones that came back, so a test can act while a read is in flight.
|
||||
const HOLD = `window.__hold = Number(new URLSearchParams(location.search).get('hold')) || 0; window.__started = 0; window.__landed = 0;
|
||||
(() => {
|
||||
const real = window.fetch.bind(window);
|
||||
window.fetch = async (url, init = {}) => {
|
||||
if (!/^\\/api\\/workspace\\?sections=tasks(?:%2C|,)projects/.test(String(url))) return real(url, init);
|
||||
window.__started++;
|
||||
if (window.__hold) await new Promise(r => setTimeout(r, window.__hold));
|
||||
try { return await real(url, init); } finally { window.__landed++; }
|
||||
};
|
||||
})();`;
|
||||
const settle = b => b.evaluate('new Promise(r => setTimeout(r, 200))');
|
||||
|
||||
test('a project filter stays in the address while its organization does not answer', async t => {
|
||||
const { b, orgs, wait } = await setup(t, { hash: '#/tasks?project=beta%3Ashop' });
|
||||
const SHOP = JSON.stringify(JSON.stringify(['beta|task:2']));
|
||||
await wait(`${cards} === ${SHOP}`);
|
||||
const list = await orgs.list();
|
||||
list[1].busHealthy = false;
|
||||
await b.evaluate('document.dispatchEvent(new CustomEvent("mosaic-workspace-render"))');
|
||||
await wait('/Beta Works/.test(document.querySelector("[data-tasks-board] [data-ws-unread]")?.textContent || "")');
|
||||
await settle(b);
|
||||
assert.equal(await b.evaluate('location.hash'), '#/tasks?project=beta%3Ashop');
|
||||
list[1].busHealthy = true;
|
||||
await b.evaluate('document.dispatchEvent(new CustomEvent("mosaic-workspace-render"))');
|
||||
await wait(`${cards} === ${SHOP} && !/Beta Works/.test(document.querySelector("[data-tasks-board] [data-ws-unread]")?.textContent || "")`);
|
||||
assert.equal(await b.evaluate('location.hash'), '#/tasks?project=beta%3Ashop');
|
||||
assert.equal(await b.evaluate('document.querySelector("[data-tb-filter=project]").value'), 'beta:shop');
|
||||
});
|
||||
|
||||
test('a gone filter is cleaned from the tasks address only, never from a page opened while it was read', async t => {
|
||||
const { b, base, wait } = await setup(t);
|
||||
await wait(`${cards} === ${ALL}`);
|
||||
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: HOLD });
|
||||
await b.navigate(`${base}/?hold=800#/tasks?org=gone`);
|
||||
await wait('window.__started >= 1');
|
||||
await b.evaluate('location.hash = "#/inbox?org=acme"');
|
||||
await wait('window.__landed >= 1');
|
||||
await settle(b);
|
||||
assert.equal(await b.evaluate('location.hash'), '#/inbox?org=acme');
|
||||
});
|
||||
|
||||
test('a read in flight does not paint the board over the page for an address no organization matches', async t => {
|
||||
const { b, base, wait } = await setup(t);
|
||||
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: HOLD });
|
||||
await b.navigate(`${base}/?reload=1#/tasks`);
|
||||
await wait(`${cards} === ${ALL}`);
|
||||
await b.evaluate('window.__hold = 800; window.__started = 0; window.__landed = 0');
|
||||
await b.evaluate('(() => { const s = document.querySelector("[data-tb-filter=state]"); s.value = "todo"; s.dispatchEvent(new Event("change", { bubbles: true })); })()');
|
||||
await wait('window.__started >= 1');
|
||||
await b.evaluate('location.hash = "#/orgs/Not%20An%20Id/tasks"');
|
||||
await wait('/not on your list/.test(document.querySelector("[data-tasks-board]")?.textContent || "")');
|
||||
await wait('window.__landed >= window.__started');
|
||||
await settle(b);
|
||||
assert.match(await b.evaluate('document.querySelector("[data-tasks-board]")?.textContent || ""'), /not on your list/);
|
||||
assert.equal(await b.evaluate(cards), '[]');
|
||||
});
|
||||
Reference in new issue
Block a user