From 8b4a26dd2bccd84de3ceb76d6e2d8644c090f127 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 15 Nov 2026 09:32:01 -0600 Subject: [PATCH] fix(webui): sp11 item 8, with no organizations #/dashboard is the Dashboard: it says there is nothing to count and offers New organization, which opens the Organizations form at its ID field; #/ stays the Console's own board (#1873, lane sp11-fixes) --- packages/webui/src/public/bus.js | 6 ++++-- packages/webui/src/public/workspace.js | 2 +- .../webui/tests/kpi-tiles-browser.test.mjs | 20 +++++++++++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/webui/src/public/bus.js b/packages/webui/src/public/bus.js index 49ee77e42..dd281fb42 100644 --- a/packages/webui/src/public/bus.js +++ b/packages/webui/src/public/bus.js @@ -1691,7 +1691,9 @@ if (parts.includes(null)) return notFound(); const [a, b, c] = parts, n = parts.length; // #1873 SP1: `#/dashboard` is the system dashboard, every organization on one page. - if ((n === 0 || (a === 'dashboard' && n === 1)) && window.MosaicOrg) return window.MosaicWorkspace.home(); + // SP11: with no organization it is still the Dashboard (it says so and offers New organization); + // #/ is then the Console's own board. + if ((n === 0 && window.MosaicOrg) || (a === 'dashboard' && n === 1 && window.MosaicWorkspace)) return window.MosaicWorkspace.home(); if (a === 'projects' && n === 1 && window.MosaicOrg) return window.MosaicWorkspace.projectsPage(q); if (a === 'organization' && n === 1 && window.MosaicOrg) return window.MosaicWorkspace.dashboard(); // SP3: #/inbox is every organization's items; #/orgs//inbox is that list for one, with its hire form. @@ -1739,7 +1741,7 @@ const paused = () => $('pause')?.getAttribute('aria-pressed') === 'true'; // A file open in the editor is never refreshed from under its draft; Reload is the person's to press. // The cross-organization pages (U3) poll at the workspace read's pollMs. - const wsPage = path => !!window.MosaicOrg && (path === '/' || path === '' || path === '/dashboard' || path === '/projects' || path === '/inbox' || path === '/agents' || path === '/tasks' || /^\/workflows(\/[^/]+)?$/.test(path)); + const wsPage = path => (path === '/dashboard' && !!window.MosaicWorkspace) || !!window.MosaicOrg && (path === '/' || path === '' || path === '/dashboard' || path === '/projects' || path === '/inbox' || path === '/agents' || path === '/tasks' || /^\/workflows(\/[^/]+)?$/.test(path)); function schedule() { clearTimeout(timer); if (!paused() && !out() && !routeOf().startsWith('/docs/')) timer = setTimeout(() => render(false), wsPage(routeOf().split('?')[0]) ? window.MosaicWorkspace?.pollMs || 10000 : 10000); } // Keep focus, open
and the copy status across a refresh of the same page. // In the send form, the focused field and its caret too (its values come from `draft`). diff --git a/packages/webui/src/public/workspace.js b/packages/webui/src/public/workspace.js index 309da2af2..05e7777da 100644 --- a/packages/webui/src/public/workspace.js +++ b/packages/webui/src/public/workspace.js @@ -710,7 +710,7 @@ const why = r.state === 'ok' || !(reason || r.boot?.at) ? '' : `${esc(reason)}${r.boot?.at ? `${reason ? ' · ' : ''}last start ${esc(when(r.boot.at))}` : ''}`; return `${av(o.meta(id))}${esc(o.name(id))}${dot(cls, glyph, word, true)}${why}${agentsCell}${projectsCell}${needsCell}`; }).join(''); - const orgList = o.rows.length ? `
${orgTable}
OrganizationAgentsProjectsNeeds you
` : '

No organizations yet

'; + const orgList = o.rows.length ? `
${orgTable}
OrganizationAgentsProjectsNeeds you
` : '

No organizations yet. New organization

'; const recent = projectRows.filter(p => p.lastActivity).slice(0, 6); const STATE_WORD = { working: 'has a session working', waiting: 'is waiting on you', idle: 'has an idle session', none: 'no live session' }; diff --git a/packages/webui/tests/kpi-tiles-browser.test.mjs b/packages/webui/tests/kpi-tiles-browser.test.mjs index c65988ac3..fde76f62c 100644 --- a/packages/webui/tests/kpi-tiles-browser.test.mjs +++ b/packages/webui/tests/kpi-tiles-browser.test.mjs @@ -170,10 +170,22 @@ test('When no answering organization could read its agents, the Agents tile says assert.equal(await b.evaluate('document.querySelector("[data-ws-tile=agents]").getAttribute("href")'), '#/agents?state=working'); }); -test('With no organizations there is nothing to count, so no tile shows a zero', async t => { - // With no organization the workspace pages are not drawn; the Console's own board keeps its dashes. - const { b, wait } = await setup(t, { none: true }); - await wait('document.querySelector("#dashboard-kpis .kpi strong")'); +// SP11 (#1873): with no organizations, #/dashboard is the Dashboard, not Not found. It says there is +// nothing to count and offers New organization, which opens the Organizations form at its ID field. +// #/ stays the Console's own board, with its dashes. +test('With no organizations the Dashboard says so, shows no tile, and offers New organization', async t => { + const { b, wait, text } = await setup(t, { none: true }); + await wait('document.querySelector("[data-ws-home] h1")'); + assert.equal(await text('#bus-view h1'), 'Dashboard'); + assert.doesNotMatch(await text('#bus-view'), /Not found/); + assert.equal(await b.evaluate('document.querySelectorAll(".ws-tile, .ws-big").length'), 0); + assert.match(await text('[data-ws-home] [data-ws-tiles]'), /^No organizations yet, so there is nothing to count\./); + assert.equal(await b.evaluate('document.querySelector("[data-ws-home] [data-ws-orgs] a")?.getAttribute("href") ?? null'), '#/settings/organizations?new=1'); + assert.equal(await text('[data-ws-home] [data-ws-orgs] a'), 'New organization'); + await b.evaluate('document.querySelector("[data-ws-home] [data-ws-orgs] a").click()'); + await wait('document.activeElement?.id === "org-new-id"'); + await b.evaluate('location.hash = "#/"'); + await wait('document.querySelector("#dashboard-kpis .kpi strong") && !document.querySelector("[data-ws-home]")'); assert.equal(await b.evaluate('document.querySelectorAll(".ws-tile, .ws-big").length'), 0); assert.deepEqual(await b.evaluate('[...new Set([...document.querySelectorAll("#dashboard-kpis .kpi strong")].map(e => e.textContent.trim()))]'), ['–']); });