fix(webui): sp6 re-check notes, a held message is never overwritten or held out of sight, and a pane whose organization left the list does not promise it comes back (#1873, lane sp6-threads)
The held draft goes back only into an empty composer and is cleared only once it is in it. A thread that reads without a composer shows the held message in the pane's line. Only a stopped or unavailable bus says the message comes back when the pane reads again.
This commit is contained in:
1 parent
b982404963
commit
02df2a3a25
2 files changed
+33
-8
No files matched your search
@@ -214,14 +214,14 @@
|
|||||||
if (org) p.el.setAttribute('style', ws().colours(org)); else p.el.removeAttribute('style');
|
if (org) p.el.setAttribute('style', ws().colours(org)); else p.el.removeAttribute('style');
|
||||||
const project = thread && i.projects?.find(x => x.id === thread.project)?.name;
|
const project = thread && i.projects?.find(x => x.id === thread.project)?.name;
|
||||||
setHtml(e.chips, `${ws().orgChip(o, p.org)}${ws().projChip(project || '')}`);
|
setHtml(e.chips, `${ws().orgChip(o, p.org)}${ws().projChip(project || '')}`);
|
||||||
let line = '', readable = true, why = 'Nothing is read for this thread.';
|
let line = '', readable = true, why = 'Nothing is read for this thread.', back = false;
|
||||||
if (!list) line = 'The organization list could not be read, so sending is off here until it reads again.';
|
if (!list) line = 'The organization list could not be read, so sending is off here until it reads again.';
|
||||||
else if (!org) { line = `${p.org} is not on your organization list.`; readable = false; }
|
else if (!org) { line = `${p.org} is not on your organization list.`; readable = false; }
|
||||||
else if (org.invalid) { line = `${name}: organization invalid. Fix its file under Settings, Organizations.`; readable = false; }
|
else if (org.invalid) { line = `${name}: organization invalid. Fix its file under Settings, Organizations.`; readable = false; }
|
||||||
else if (org.busHealthy === false) {
|
else if (org.busHealthy === false) {
|
||||||
const how = org.busStopped ? 'stopped' : 'unavailable';
|
const how = org.busStopped ? 'stopped' : 'unavailable';
|
||||||
line = `${name}: bus is ${how}. This pane cannot read or send until it starts again; the other panes are not affected.`;
|
line = `${name}: bus is ${how}. This pane cannot read or send until it starts again; the other panes are not affected.`;
|
||||||
why = `Nothing is read for this thread while ${name}'s bus is ${how}.`; readable = false;
|
why = `Nothing is read for this thread while ${name}'s bus is ${how}.`; readable = false; back = true;
|
||||||
}
|
}
|
||||||
else if (i?.threads && !thread) line = `This thread is not in ${name}'s thread list.`;
|
else if (i?.threads && !thread) line = `This thread is not in ${name}'s thread list.`;
|
||||||
setText(e.line, line); e.line.hidden = !line;
|
setText(e.line, line); e.line.hidden = !line;
|
||||||
@@ -231,16 +231,24 @@
|
|||||||
const typed = p.chat && e.body.querySelector('form textarea')?.value;
|
const typed = p.chat && e.body.querySelector('form textarea')?.value;
|
||||||
if (typed?.trim()) p.draft = typed;
|
if (typed?.trim()) p.draft = typed;
|
||||||
p.chat?.dispose(); p.chat = null;
|
p.chat?.dispose(); p.chat = null;
|
||||||
setHtml(e.body, `<p class="ws-line">${esc(why)}${p.draft ? ' Your unsent message is kept and comes back here when it reads again.' : ''}</p>`);
|
const kept = !p.draft ? '' : back ? ' Your unsent message is kept and comes back here when it reads again.' : ' Your unsent message is kept while this pane stays open.';
|
||||||
|
setHtml(e.body, `<p class="ws-line">${esc(why)}${kept}</p>`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!p.chat) { p.chat = window.MosaicChat.pane(p.org); p.first = true; }
|
if (!p.chat) { p.chat = window.MosaicChat.pane(p.org); p.first = true; }
|
||||||
const first = p.first, chat = p.chat; p.first = false;
|
const first = p.first, chat = p.chat; p.first = false;
|
||||||
void chat.render({ path: `/chat/threads/${p.id}`, container: e.body, canSend: !!list && i?.chatSend === true, navigated: first }).then(() => {
|
void chat.render({ path: `/chat/threads/${p.id}`, container: e.body, canSend: !!list && i?.chatSend === true, navigated: first }).then(() => {
|
||||||
const field = p.draft && p.chat === chat ? e.body.querySelector('form textarea') : null;
|
if (!p.draft || p.chat !== chat || !p.el.isConnected) return;
|
||||||
if (!field) return;
|
const field = e.body.querySelector('form textarea');
|
||||||
|
if (!field) {
|
||||||
|
// The thread reads but offers no composer: the held message stays visible here, never silently held.
|
||||||
|
const held = p.draft.length > 200 ? `${p.draft.slice(0, 200)}…` : p.draft;
|
||||||
|
setText(e.line, `${line ? `${line} ` : ''}Your unsent message is kept until this thread can send: “${held}”`); e.line.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Put the held message back only into an empty composer; text typed meanwhile is never overwritten.
|
||||||
if (!field.value) { field.value = p.draft; field.dispatchEvent(new Event('input', { bubbles: true })); }
|
if (!field.value) { field.value = p.draft; field.dispatchEvent(new Event('input', { bubbles: true })); }
|
||||||
p.draft = '';
|
if (field.value === p.draft) p.draft = '';
|
||||||
}, () => {});
|
}, () => {});
|
||||||
}
|
}
|
||||||
function paintTabs() {
|
function paintTabs() {
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ function orgs(state) {
|
|||||||
const MESSAGES = { [T.a1]: 'Alpha draft is ready.', [T.a2]: 'Kickoff agenda.', [T.b1]: 'Beta tokens rotate nightly.', [T.h1]: 'Seeds arrive Monday.' };
|
const MESSAGES = { [T.a1]: 'Alpha draft is ready.', [T.a2]: 'Kickoff agenda.', [T.b1]: 'Beta tokens rotate nightly.', [T.h1]: 'Seeds arrive Monday.' };
|
||||||
const down = () => { throw Object.assign(new Error('bus down'), { code: 'bus-unavailable' }); };
|
const down = () => { throw Object.assign(new Error('bus down'), { code: 'bus-unavailable' }); };
|
||||||
return {
|
return {
|
||||||
list: async () => ORGS.map(o => ({ ...o, busHealthy: !state.down.has(o.id), ...(state.down.has(o.id) ? { busStopped: true } : {}) })),
|
list: async () => ORGS.filter(o => !state.gone?.has(o.id)).map(o => ({ ...o, busHealthy: !state.down.has(o.id), ...(state.down.has(o.id) ? { busStopped: true } : {}) })),
|
||||||
resolve: async id => {
|
resolve: async id => {
|
||||||
const org = ORGS.find(o => o.id === id); if (!org) return null;
|
const org = ORGS.find(o => o.id === id); if (!org || state.gone?.has(id)) return null;
|
||||||
if (state.down.has(id)) return { busHealthy: false, reads: null, bus: {}, chat: { projects: down, threads: down, sessions: down, shares: down, messages: down } };
|
if (state.down.has(id)) return { busHealthy: false, reads: null, bus: {}, chat: { projects: down, threads: down, sessions: down, shares: down, messages: down } };
|
||||||
const threads = () => [...THREADS[id], ...state.created.filter(c => c.org === id).map(c => ({ id: c.id, project: c.project, name: c.name, instance: c.instance }))];
|
const threads = () => [...THREADS[id], ...state.created.filter(c => c.org === id).map(c => ({ id: c.id, project: c.project, name: c.name, instance: c.instance }))];
|
||||||
return {
|
return {
|
||||||
@@ -214,6 +214,23 @@ test('a stopped organization is named at the top, its pane says so, and the othe
|
|||||||
assert.deepEqual(state.sent.map(({ org, body }) => `${org}:${body}`), ['alpha:still here'], 'the held message is not sent by itself');
|
assert.deepEqual(state.sent.map(({ org, body }) => `${org}:${body}`), ['alpha:still here'], 'the held message is not sent by itself');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a pane whose organization leaves the list keeps its unsent message and does not promise it comes back', async t => {
|
||||||
|
const state = { down: new Set(['home']), gone: new Set(), sent: [], created: [] };
|
||||||
|
const { b, wait } = await open(t, { hash: `#/assistant?open=alpha:${T.a1},beta:${T.b1}`, state });
|
||||||
|
await wait(`${ready(`alpha:${T.a1}`)} && ${ready(`beta:${T.b1}`)}`);
|
||||||
|
const betaField = `${PANE(`beta:${T.b1}`)}.querySelector('form textarea')`;
|
||||||
|
await wait(`!!${betaField} && !${betaField}.disabled`);
|
||||||
|
await b.evaluate(`(() => { const el = ${betaField}; el.value = 'beta draft'; el.dispatchEvent(new Event('input', { bubbles: true })); })()`);
|
||||||
|
state.gone.add('beta');
|
||||||
|
await b.evaluate('window.MosaicAssistant.refresh()');
|
||||||
|
await wait(`/not on your organization list/.test(${PANE(`beta:${T.b1}`)}.querySelector('[data-assist-line]')?.textContent || '') && !${betaField}`);
|
||||||
|
const body = await b.evaluate(`${PANE(`beta:${T.b1}`)}.querySelector('.assist-body').innerText`);
|
||||||
|
assert.match(body, /Your unsent message is kept while this pane stays open\./);
|
||||||
|
assert.doesNotMatch(body, /comes back/, 'no promise that an organization off the list reads again');
|
||||||
|
assert.equal(await b.evaluate('window.MosaicAssistant.hasDraft()'), true);
|
||||||
|
assert.deepEqual(state.sent, []);
|
||||||
|
});
|
||||||
|
|
||||||
test('an open pane shows a new reply on its own, with no refresh', async t => {
|
test('an open pane shows a new reply on its own, with no refresh', async t => {
|
||||||
const state = { down: new Set(['home']), sent: [], created: [], replies: [] };
|
const state = { down: new Set(['home']), sent: [], created: [], replies: [] };
|
||||||
const { b, wait } = await open(t, { hash: `#/assistant?open=alpha:${T.a1},beta:${T.b1}`, state });
|
const { b, wait } = await open(t, { hash: `#/assistant?open=alpha:${T.a1},beta:${T.b1}`, state });
|
||||||
|
|||||||
Reference in new issue
Block a user