fix(webui): sp11 item 6, an organization's decision page, decision list, replies and messages name a decision class in the Inbox's words from the same map; message classes the map does not know show as they are (#1873, lane sp11-fixes)
This commit is contained in:
1 parent
9cff3255e6
commit
8aefc0f7a9
3 files changed
+28
-7
No files matched your search
@@ -208,7 +208,7 @@
|
||||
out.groupAs = m?.from_role === 'human' ? 'request' : m?.decision ? 'decision' : 'other';
|
||||
} else if (r.table === 'decisions') {
|
||||
out.kind = 'decision.raised'; out.decision = r.id; out.actor = { role: r.raised_by_role, run: r.raised_by_run };
|
||||
out.text = `Raised a ${r.class} decision for ${r.route_to}: ${String(r.question).split('\n')[0]}`;
|
||||
out.text = `Raised a ${String(classWord(r.class)).toLowerCase()} decision for ${r.route_to}: ${String(r.question).split('\n')[0]}`;
|
||||
} else if (r.table === 'decision_events') {
|
||||
// The human answers from the CLI (via cli) or, since row 252 (#1753), a grant request from the
|
||||
// signed-in Console (via console); every other row is the broker's.
|
||||
@@ -243,7 +243,9 @@
|
||||
const badge = (text, kind = 'muted') => `<span class="badge badge-${kind}">${txt(text)}</span>`;
|
||||
// Class chip: the decision class, the action in mono. BLOCKING is its own chip (CSS capitalises it; the text stays lowercase).
|
||||
const CLASSES = ['routine', 'within-role', 'cross-role', 'gated'];
|
||||
const chip = (cls, action) => `<span class="tag${CLASSES.includes(cls) ? ` tag-${cls}` : ''}">${txt(cls)} · <code>${txt(action)}</code></span>`;
|
||||
// SP11: the class in the Inbox's words, from workspace.js's map.
|
||||
const classWord = cls => window.MosaicWorkspace?.classWord?.(cls) ?? cls;
|
||||
const chip = (cls, action) => `<span class="tag${CLASSES.includes(cls) ? ` tag-${cls}` : ''}">${txt(classWord(cls))} · <code>${txt(action)}</code></span>`;
|
||||
const blockChip = text => `<span class="tag tag-block">${txt(text)}</span>`;
|
||||
// An empty state says what would appear and where it comes from.
|
||||
const empty = (what, from) => `<div class="empty"><b>${what}</b>${from ? `<p>${from}</p>` : ''}</div>`;
|
||||
@@ -354,7 +356,7 @@
|
||||
function repliesHtml() {
|
||||
if (replies === null) return '<p class="small warn">The replies to it could not be read. Read again to retry; <code>mosaic inbox</code> in a terminal lists them.</p>';
|
||||
if (!replies.length) return '<p class="small muted">No reply yet. One shows here when the role answers, as the page refreshes.</p>';
|
||||
return `<h3 class="small">Replies <span class="count">${replies.length}</span></h3><ol class="s1-plain s1-replies">${replies.map(m => `<li><p class="small muted">${txt(m.from_role)} ${when(m.at)} · ${txt(m.class)} · <a href="#/trail/message/${enc(m.id)}">trail</a></p><p class="s1-q">${txt(m.body)}</p></li>`).join('')}</ol>${replies.length >= REPLIES ? `<p class="small muted">The first ${REPLIES} replies show here.</p>` : ''}`;
|
||||
return `<h3 class="small">Replies <span class="count">${replies.length}</span></h3><ol class="s1-plain s1-replies">${replies.map(m => `<li><p class="small muted">${txt(m.from_role)} ${when(m.at)} · ${txt(classWord(m.class))} · <a href="#/trail/message/${enc(m.id)}">trail</a></p><p class="s1-q">${txt(m.body)}</p></li>`).join('')}</ol>${replies.length >= REPLIES ? `<p class="small muted">The first ${REPLIES} replies show here.</p>` : ''}`;
|
||||
}
|
||||
const draft = { to: '', body: '' };
|
||||
function composerHtml() {
|
||||
@@ -707,7 +709,7 @@
|
||||
if (after !== null && !SAFE.test(after)) return notFound('The message cursor is not a message id.');
|
||||
const page = await get('messages', after === null ? undefined : after);
|
||||
const body = page.rows.length ? `<ul id="message-list" class="waiting s1-replies">${page.rows.map(m =>
|
||||
`<li class="wait-item"><p class="small muted">${txt(m.from_role)} · ${txt(m.class)} · ${when(m.at)} · ${m.read ? 'Read' : 'Unread'} · <a href="#/trail/message/${enc(m.id)}">trail</a></p><p class="s1-message">${messageText(m)}</p></li>`).join('')}</ul>`
|
||||
`<li class="wait-item"><p class="small muted">${txt(m.from_role)} · ${txt(classWord(m.class))} · ${when(m.at)} · ${m.read ? 'Read' : 'Unread'} · <a href="#/trail/message/${enc(m.id)}">trail</a></p><p class="s1-message">${messageText(m)}</p></li>`).join('')}</ul>`
|
||||
: empty('No messages to you.', 'Messages appear here as the bus is read.');
|
||||
return `${head('Messages', `${readNote(get)} Every message to you, newest first. Reading here marks nothing read.`)}
|
||||
<p class="small muted">${page.total} messages · ${page.unread} unread</p>${body}
|
||||
|
||||
@@ -634,7 +634,9 @@
|
||||
const CLASS_CHIP = { gated: 'gated', 'within-role': 'within', routine: 'routine', 'cross-role': 'cross' };
|
||||
// The one map of decision classes to words: the class chips and the Inbox Class filter both read it.
|
||||
const TYPE_WORD = { gated: 'Gated', 'within-role': 'Within role', 'cross-role': 'Cross-role', routine: 'Routine' };
|
||||
const classChip = c => (c ? `<span class="ws-chip ${CLASS_CHIP[c] || ''}">${esc(TYPE_WORD[c] || c)}</span>` : '');
|
||||
// SP11: the Inbox's word for a decision class; a class the map does not know shows as it is.
|
||||
const classWord = c => (Object.hasOwn(TYPE_WORD, c) ? TYPE_WORD[c] : c);
|
||||
const classChip = c => (c ? `<span class="ws-chip ${CLASS_CHIP[c] || ''}">${esc(classWord(c))}</span>` : '');
|
||||
const itemKey = d => `${d.org}:${d.id}`;
|
||||
const roleOf = d => d.session?.role || d.raisedBy || '';
|
||||
const fromWords = (o, d, withOrg = true) => [withOrg ? o.name(d.org) : '', d.project?.name || 'No project', roleOf(d)].filter(Boolean).map(esc).join(' · ');
|
||||
@@ -1134,6 +1136,6 @@
|
||||
|
||||
// U4b: the org mark (colour and initials) for the task board (tasks-board.js).
|
||||
const orgMark = Object.freeze({ ready: ensureThemes, av, colours, initials });
|
||||
window.MosaicWorkspace = Object.freeze({ dashboard, holdLive, graftLive, agentGrid, icon, orgMark, refresh: () => refresh(), av, colours, label, state, dot, themes: ensureThemes, home, projectsPage, inboxPage, agentsPage, inboxCount, setGrouping, orgsOf, orgChip, projChip, unreadNote, setQuery, get pollMs() { return pollMs; } });
|
||||
window.MosaicWorkspace = Object.freeze({ dashboard, holdLive, graftLive, agentGrid, icon, orgMark, refresh: () => refresh(), av, colours, label, state, dot, themes: ensureThemes, home, projectsPage, inboxPage, agentsPage, inboxCount, setGrouping, orgsOf, orgChip, projChip, unreadNote, setQuery, classWord, get pollMs() { return pollMs; } });
|
||||
refresh();
|
||||
})();
|
||||
@@ -61,7 +61,9 @@ function fixture({ pollMs, fail = {}, down = [] } = {}) {
|
||||
return {
|
||||
busHealthy: true,
|
||||
reads: { business: async () => ({ business: { id, human: 'jason', projects: DATA[id].projects.map(p => p.repo) } }), settings: async () => ({}), queue: async () => ({ rows: [] }), busStatus: async () => ({}) },
|
||||
bus: { messages: async () => ({ messages: [], unread: 0 }), agents: async () => [], 'thread.claims': async () => [], inbox: async () => DATA[id].inbox.map(d => ({ ...d, business: id, raised_by_run: 'run', routed_to: 'human' })), tasks: async () => [], instances: async () => ({ business: id, instances: {} }) },
|
||||
bus: { messages: async () => ({ messages: [], unread: 0 }), agents: async () => [], 'thread.claims': async () => [], inbox: async () => DATA[id].inbox.map(d => ({ ...d, business: id, raised_by_run: 'run', routed_to: 'human' })), tasks: async () => [], instances: async () => ({ business: id, instances: {} }),
|
||||
// SP11: a decision's own page reads its trail.
|
||||
trail: async subject => DATA[id].inbox.filter(d => d.id === subject).map((d, i) => ({ table: 'decisions', seq: i + 1, ...d, business: id, raised_by_run: 'run', route_to: 'human', action: 'price.set', recommendation: null })) },
|
||||
chat: { projects: async () => DATA[id].projects, threads: async () => [] },
|
||||
};
|
||||
},
|
||||
@@ -224,6 +226,21 @@ test('Inbox lists both organizations, copies a command with --org and the full i
|
||||
await wait(`document.querySelectorAll("[data-ws-item]").length === 1 && document.querySelector("[data-ws-item]").dataset.wsItem === ${JSON.stringify(`beta:${BETA_DECISION}`)}`);
|
||||
});
|
||||
|
||||
// SP11 (#1873): an organization's own decision page names the class in the words the Inbox uses,
|
||||
// from the same map, not the raw class.
|
||||
test('SP11: the per-organization decision page names the class as the Inbox does', async t => {
|
||||
const { b, base, wait } = await setup(t, { hash: '#/inbox' });
|
||||
const item = `[data-ws-item="beta:${BETA_DECISION}"]`;
|
||||
await wait(`!!document.querySelector(${JSON.stringify(`${item} .ws-chip.within`)})`);
|
||||
const word = await b.evaluate(`document.querySelector(${JSON.stringify(`${item} .ws-chip.within`)}).textContent`);
|
||||
assert.equal(word, 'Within role');
|
||||
await b.navigate(`${base}/#/orgs/beta/inbox/${BETA_DECISION}`);
|
||||
await wait('!!document.querySelector("#bus-view .tag.tag-within-role")');
|
||||
const chip = await b.evaluate('document.querySelector("#bus-view .tag.tag-within-role").textContent');
|
||||
assert.match(chip, new RegExp(`^${word} · `), chip);
|
||||
assert.doesNotMatch(await b.evaluate('document.getElementById("bus-view").innerText'), /within-role/);
|
||||
});
|
||||
|
||||
test('A poll repaints in place: focus, caret and scroll stay where they were', async t => {
|
||||
const { b, wait } = await setup(t, { hash: '/projects', viewport: [1000, 420] });
|
||||
await wait('document.querySelector("[data-ws-table]") && document.querySelectorAll("[data-ws-project]").length === 3');
|
||||
|
||||
Reference in new issue
Block a user