@sphoro/bugcatch-web-sdk 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -3
- package/dist/bugcatch-call.es.js +201 -200
- package/dist/bugcatch-call.es.js.map +1 -1
- package/dist/bugcatch-call.umd.js +1 -1
- package/dist/bugcatch-call.umd.js.map +1 -1
- package/dist/bugcatch-core.es.js +1085 -1007
- package/dist/bugcatch-core.es.js.map +1 -1
- package/dist/bugcatch-panel.es.js +1337 -1067
- package/dist/bugcatch-panel.es.js.map +1 -1
- package/dist/bugcatch-panel.umd.js +427 -315
- package/dist/bugcatch-panel.umd.js.map +1 -1
- package/dist/bugcatch.standalone.umd.js +553 -429
- package/dist/bugcatch.standalone.umd.js.map +1 -1
- package/dist/bugcatch.umd.js +132 -120
- package/dist/bugcatch.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/accent.js +38 -0
- package/src/incoming-call.js +17 -15
- package/src/index.d.ts +52 -2
- package/src/index.js +26 -7
- package/src/panel/call.js +3 -2
- package/src/panel/compose.js +7 -3
- package/src/panel/icons.js +2 -0
- package/src/panel/index.js +26 -3
- package/src/panel/messages.js +2 -2
- package/src/panel/picks.js +6 -2
- package/src/panel/rate-due.js +155 -0
- package/src/panel/rating.js +87 -0
- package/src/panel/refs.js +1 -1
- package/src/panel/render.js +11 -3
- package/src/panel/signin.js +1 -1
- package/src/panel/styles.js +270 -186
- package/src/panel/thread.js +48 -7
- package/src/updates.js +77 -27
- package/src/widget.js +160 -141
package/src/panel/thread.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// What happens to a report once it is filed: the confirmation, the
|
|
2
2
|
// reporter's list of everything they have filed, and one report as a
|
|
3
3
|
// conversation — with the assistant's live card over it, the "does the fix
|
|
4
|
-
// work"
|
|
4
|
+
// work" and "how did we do" questions under it, and the reply box at the
|
|
5
|
+
// bottom (composer.js).
|
|
5
6
|
|
|
6
7
|
import * as ICONS from './icons.js';
|
|
7
8
|
import { threadHtml, typingHtml } from './messages.js';
|
|
@@ -10,11 +11,23 @@ import {
|
|
|
10
11
|
atLatest, scrollToLatest,
|
|
11
12
|
} from './render.js';
|
|
12
13
|
import { mountComposer } from './composer.js';
|
|
14
|
+
import { drawRating, ratingKey } from './rating.js';
|
|
13
15
|
|
|
14
16
|
// What each open thread was last drawn from, so a poll that brings nothing new
|
|
15
17
|
// leaves the DOM (and the reader's place in it) alone.
|
|
16
18
|
const threadDrawn = new WeakMap();
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The rating's word on a history row: the score they gave, or a nudge on a
|
|
22
|
+
* settled report that is waiting for one. Nothing on an open report.
|
|
23
|
+
*/
|
|
24
|
+
function historyRating(r) {
|
|
25
|
+
if (r.rating) {
|
|
26
|
+
return ` · <span class="rated" aria-label="You rated this ${Number(r.rating)} out of 5">${ICONS.star}${Number(r.rating)}</span>`;
|
|
27
|
+
}
|
|
28
|
+
return r.ratingAsked ? ' · <span class="rate-ask">Rate it</span>' : '';
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
export function mountThread(ctx) {
|
|
19
32
|
const {
|
|
20
33
|
client, scrim, close,
|
|
@@ -88,7 +101,7 @@ export function mountThread(ctx) {
|
|
|
88
101
|
openReportId = null;
|
|
89
102
|
panel.classList.remove('is-chat');
|
|
90
103
|
panel.innerHTML = `
|
|
91
|
-
${headHtml({ title: 'Your reports', sub:
|
|
104
|
+
${headHtml({ title: 'Your reports', sub: 'Where each one has got to.', theme: ctx.themeLabel() })}
|
|
92
105
|
<div class="history" aria-busy="true">${skeletonRows(3)}</div>`;
|
|
93
106
|
|
|
94
107
|
panel.querySelector('[data-act="close"]').addEventListener('click', close);
|
|
@@ -113,7 +126,7 @@ export function mountThread(ctx) {
|
|
|
113
126
|
<span class="row-ico" aria-hidden="true">${ICONS.chat}</span>
|
|
114
127
|
<span class="who">
|
|
115
128
|
<span class="ttl">${escapeHtml(r.title)}</span>
|
|
116
|
-
<span class="sub">${escapeHtml(r.reference)} · ${escapeHtml(relativeDay(r.createdAt))}</span>
|
|
129
|
+
<span class="sub">${escapeHtml(r.reference)} · ${escapeHtml(relativeDay(r.createdAt))}${historyRating(r)}</span>
|
|
117
130
|
</span>
|
|
118
131
|
<span class="pill ${escapeHtml(String(r.status).toLowerCase())}">${escapeHtml(statusWord(r.status))}</span>
|
|
119
132
|
<span class="chev" aria-hidden="true">${ICONS.chevronRight}</span>
|
|
@@ -146,7 +159,8 @@ export function mountThread(ctx) {
|
|
|
146
159
|
${headHtml({
|
|
147
160
|
back: true,
|
|
148
161
|
title: fresh ? 'Looking into it' : 'Your report',
|
|
149
|
-
sub: fresh ? '
|
|
162
|
+
sub: fresh ? 'It may ask you a question here.' : 'Updates, and a way to reply.',
|
|
163
|
+
theme: ctx.themeLabel(),
|
|
150
164
|
})}
|
|
151
165
|
<div class="history chatting" aria-busy="true"><div class="thread">${skeletonBubbles()}</div></div>`;
|
|
152
166
|
|
|
@@ -199,6 +213,7 @@ export function mountThread(ctx) {
|
|
|
199
213
|
threadDrawn.set(thread, drawn);
|
|
200
214
|
scrollToLatest(thread);
|
|
201
215
|
mountVerification(body, report);
|
|
216
|
+
mountRating(body, report);
|
|
202
217
|
if (report.canReply) mountComposer(ctx, body, thread, bugId);
|
|
203
218
|
mountAssistant(body, report, bugId);
|
|
204
219
|
// Again, now the cards above it have taken their share of the height: the
|
|
@@ -246,9 +261,8 @@ export function mountThread(ctx) {
|
|
|
246
261
|
const content = `
|
|
247
262
|
<div class="as-body${previous && previous !== a.phase ? ' entering' : ''}">
|
|
248
263
|
<div class="as-top">
|
|
249
|
-
<span class="as-orb
|
|
250
|
-
: a.phase === '
|
|
251
|
-
: a.phase === 'resolved' ? ICONS.check : ICONS.sparkle}</span>
|
|
264
|
+
<span class="as-orb${a.phase === 'calling' || a.phase === 'resolved' ? '' : ' orb'}" aria-hidden="true">${
|
|
265
|
+
a.phase === 'calling' ? ICONS.phone : a.phase === 'resolved' ? ICONS.check : ''}</span>
|
|
252
266
|
<div class="as-text">
|
|
253
267
|
<div class="as-title">${escapeHtml(a.title)}</div>
|
|
254
268
|
<div class="as-detail">${escapeHtml(a.detail)}</div>
|
|
@@ -314,6 +328,32 @@ export function mountThread(ctx) {
|
|
|
314
328
|
body.querySelector('.report-head')?.after(box);
|
|
315
329
|
}
|
|
316
330
|
|
|
331
|
+
/**
|
|
332
|
+
* "How did we do?" — five stars under a settled report.
|
|
333
|
+
*
|
|
334
|
+
* Only where the API says a rating is being asked for: the report is settled
|
|
335
|
+
* and the project has not switched ratings off. The box itself is rating.js.
|
|
336
|
+
*
|
|
337
|
+
* Redrawn only when the rating itself changed. The thread refreshes on every
|
|
338
|
+
* update and every few seconds while the assistant works, and a box rebuilt
|
|
339
|
+
* on each of those would eat the note somebody was halfway through.
|
|
340
|
+
*/
|
|
341
|
+
function mountRating(body, report) {
|
|
342
|
+
let box = body.querySelector('.fix-rating');
|
|
343
|
+
const settled = ['RESOLVED', 'CLOSED'].includes(report.status);
|
|
344
|
+
if (!settled || !report.ratingAsked) {
|
|
345
|
+
box?.remove();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (box && box.dataset.key === ratingKey(report.rating)) return;
|
|
349
|
+
if (!box) {
|
|
350
|
+
box = document.createElement('div');
|
|
351
|
+
box.className = 'fix-rating';
|
|
352
|
+
(body.querySelector('.fix-verification') || body.querySelector('.report-head'))?.after(box);
|
|
353
|
+
}
|
|
354
|
+
drawRating(box, { client, report, rating: report.rating || null });
|
|
355
|
+
}
|
|
356
|
+
|
|
317
357
|
/**
|
|
318
358
|
* Redraw the open thread in place, keeping whatever is half-typed.
|
|
319
359
|
*
|
|
@@ -334,6 +374,7 @@ export function mountThread(ctx) {
|
|
|
334
374
|
if (ctx.closed || !report || openReportId !== bugId) return;
|
|
335
375
|
|
|
336
376
|
mountVerification(body, report);
|
|
377
|
+
mountRating(body, report);
|
|
337
378
|
const pinned = atLatest(thread);
|
|
338
379
|
const html = threadHtml(report.timeline);
|
|
339
380
|
// Only rewritten when something changed, so a poll that brings nothing new
|
package/src/updates.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// filed them.
|
|
13
13
|
|
|
14
14
|
import { tooManyMessage } from './busy.js';
|
|
15
|
+
import { readableOn, INK, PAPER } from './accent.js';
|
|
15
16
|
|
|
16
17
|
const TOKEN_KEY = 'bugcatch.watch';
|
|
17
18
|
const BACKOFF_MS = [2000, 5000, 15000, 30000, 60000];
|
|
@@ -658,9 +659,21 @@ const STATUS_COPY = {
|
|
|
658
659
|
*/
|
|
659
660
|
const KIND_ACCENT = {
|
|
660
661
|
status: null, // falls back to the host's configured accent
|
|
661
|
-
severity: '#
|
|
662
|
-
assignment: '#
|
|
663
|
-
reply: '#
|
|
662
|
+
severity: '#f2a54a',
|
|
663
|
+
assignment: '#79a8ff',
|
|
664
|
+
reply: '#4fd19a',
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// The cards are always dark — they float over pages of every colour, and a
|
|
668
|
+
// near-black card with a hairline reads on all of them. The colours are the
|
|
669
|
+
// product's dark palette.
|
|
670
|
+
const CARD = {
|
|
671
|
+
bg: '#111113',
|
|
672
|
+
fg: '#f3f2ef',
|
|
673
|
+
muted: '#b3b0aa',
|
|
674
|
+
faint: '#8f8c86',
|
|
675
|
+
ring: 'rgba(255,255,255,.09)',
|
|
676
|
+
brand: '#a59cff',
|
|
664
677
|
};
|
|
665
678
|
|
|
666
679
|
/**
|
|
@@ -736,7 +749,7 @@ function dockOffset() {
|
|
|
736
749
|
* instead of a notification floating over the report it is about.
|
|
737
750
|
*/
|
|
738
751
|
export function createUpdateNotifier({
|
|
739
|
-
accent =
|
|
752
|
+
accent = null,
|
|
740
753
|
position = 'bottom-right',
|
|
741
754
|
onRead = null,
|
|
742
755
|
onReadAll = null,
|
|
@@ -784,17 +797,23 @@ export function createUpdateNotifier({
|
|
|
784
797
|
setTimeout(() => el.remove(), 250);
|
|
785
798
|
}
|
|
786
799
|
|
|
800
|
+
// On a dark card the ink default is a white pill with ink words; a host's
|
|
801
|
+
// own colour keeps whichever words read on it.
|
|
802
|
+
const primaryBg = accent || PAPER;
|
|
803
|
+
const primaryFg = accent ? readableOn(accent) : INK;
|
|
804
|
+
|
|
787
805
|
function button(label, primary) {
|
|
788
806
|
const el = document.createElement('button');
|
|
789
807
|
el.type = 'button';
|
|
790
808
|
el.textContent = label;
|
|
791
809
|
el.style.cssText = [
|
|
792
|
-
'padding:
|
|
793
|
-
|
|
794
|
-
'
|
|
795
|
-
|
|
796
|
-
`
|
|
797
|
-
|
|
810
|
+
'padding:6px 12px',
|
|
811
|
+
'border:0',
|
|
812
|
+
`box-shadow:${primary ? 'none' : `inset 0 0 0 1px ${CARD.ring}`}`,
|
|
813
|
+
'border-radius:999px',
|
|
814
|
+
`background:${primary ? primaryBg : 'rgba(255,255,255,.06)'}`,
|
|
815
|
+
`color:${primary ? primaryFg : CARD.fg}`,
|
|
816
|
+
'font:560 12px/1.4 inherit',
|
|
798
817
|
'cursor:pointer',
|
|
799
818
|
'white-space:nowrap',
|
|
800
819
|
].join(';');
|
|
@@ -807,13 +826,12 @@ export function createUpdateNotifier({
|
|
|
807
826
|
el.style.cssText = [
|
|
808
827
|
'box-sizing:border-box',
|
|
809
828
|
'width:100%',
|
|
810
|
-
'padding:
|
|
811
|
-
'border-radius:
|
|
812
|
-
|
|
813
|
-
|
|
829
|
+
'padding:13px 14px',
|
|
830
|
+
'border-radius:14px',
|
|
831
|
+
`background:${CARD.bg}`,
|
|
832
|
+
`color:${CARD.fg}`,
|
|
814
833
|
'font:13px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
|
|
815
|
-
|
|
816
|
-
`border-left:3px solid ${KIND_ACCENT[record?.kind] || accent}`,
|
|
834
|
+
`box-shadow:0 0 0 1px ${CARD.ring},0 20px 44px -12px rgba(0,0,0,.55),0 4px 12px rgba(0,0,0,.25)`,
|
|
817
835
|
'opacity:0',
|
|
818
836
|
`transform:translateY(${atTop ? -8 : 8}px)`,
|
|
819
837
|
'transition:opacity .2s ease,transform .2s ease',
|
|
@@ -821,25 +839,33 @@ export function createUpdateNotifier({
|
|
|
821
839
|
].join(';');
|
|
822
840
|
|
|
823
841
|
const title = document.createElement('div');
|
|
824
|
-
title.style.cssText = 'font-weight:600;margin-bottom:2px';
|
|
825
|
-
|
|
842
|
+
title.style.cssText = 'display:flex;align-items:baseline;gap:8px;font-weight:600;margin-bottom:2px';
|
|
843
|
+
// The kind's colour, as a dot before the headline rather than a stripe
|
|
844
|
+
// down the card: a fix and a severity change are still told apart.
|
|
845
|
+
const dot = document.createElement('span');
|
|
846
|
+
dot.setAttribute('aria-hidden', 'true');
|
|
847
|
+
dot.style.cssText = `flex:none;width:7px;height:7px;border-radius:50%;transform:translateY(-1px);background:${
|
|
848
|
+
KIND_ACCENT[record?.kind] || accent || CARD.brand}`;
|
|
849
|
+
const headline = document.createElement('span');
|
|
850
|
+
headline.textContent = updateHeadline(record);
|
|
851
|
+
title.append(dot, headline);
|
|
826
852
|
|
|
827
853
|
const body = document.createElement('div');
|
|
828
854
|
// A reply is the one kind whose detail is worth more room than a title, and
|
|
829
855
|
// it is the only text here the team wrote rather than the reporter.
|
|
830
856
|
body.style.cssText = record?.kind === 'reply'
|
|
831
|
-
?
|
|
832
|
-
:
|
|
857
|
+
? `color:${CARD.muted};padding-left:15px;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden`
|
|
858
|
+
: `color:${CARD.muted};padding-left:15px`;
|
|
833
859
|
// textContent, not innerHTML: this is text a person typed, being written
|
|
834
860
|
// back into somebody else's page.
|
|
835
861
|
body.textContent = updateDetail(record);
|
|
836
862
|
|
|
837
863
|
const actions = document.createElement('div');
|
|
838
864
|
actions.style.cssText =
|
|
839
|
-
'display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap';
|
|
865
|
+
'display:flex;align-items:center;gap:8px;margin-top:10px;padding-left:15px;flex-wrap:wrap';
|
|
840
866
|
|
|
841
867
|
const when = document.createElement('span');
|
|
842
|
-
when.style.cssText =
|
|
868
|
+
when.style.cssText = `color:${CARD.faint};font-size:11px;margin-right:auto`;
|
|
843
869
|
when.textContent = agoLabel(record?.at);
|
|
844
870
|
|
|
845
871
|
const read = button('Mark as read', false);
|
|
@@ -871,12 +897,12 @@ export function createUpdateNotifier({
|
|
|
871
897
|
'display:flex',
|
|
872
898
|
'align-items:center',
|
|
873
899
|
'gap:8px',
|
|
874
|
-
'padding:8px
|
|
875
|
-
'border-radius:
|
|
876
|
-
|
|
877
|
-
|
|
900
|
+
'padding:8px 10px 8px 14px',
|
|
901
|
+
'border-radius:999px',
|
|
902
|
+
`background:${CARD.bg}`,
|
|
903
|
+
`color:${CARD.muted}`,
|
|
878
904
|
'font:12px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
|
|
879
|
-
|
|
905
|
+
`box-shadow:0 0 0 1px ${CARD.ring},0 12px 30px -10px rgba(0,0,0,.5)`,
|
|
880
906
|
'pointer-events:auto',
|
|
881
907
|
].join(';');
|
|
882
908
|
|
|
@@ -1064,6 +1090,30 @@ async function readJoinError(res) {
|
|
|
1064
1090
|
return message || `Could not add you to this issue (HTTP ${res.status})`;
|
|
1065
1091
|
}
|
|
1066
1092
|
|
|
1093
|
+
/**
|
|
1094
|
+
* Rate how a settled report was handled, out of five, with a sentence if they
|
|
1095
|
+
* want one. Rating again replaces the earlier score.
|
|
1096
|
+
*
|
|
1097
|
+
* Same credential as everything else here: the watch token, which is proof of
|
|
1098
|
+
* having filed the report. `?projectId=` goes with it so a token that reaches
|
|
1099
|
+
* more than one project is narrowed to the one this widget is on.
|
|
1100
|
+
*/
|
|
1101
|
+
export async function rateReport({ apiBase, bugId, score, comment }) {
|
|
1102
|
+
const token = readToken();
|
|
1103
|
+
if (!token) throw new Error('This browser cannot access that report.');
|
|
1104
|
+
const res = await fetch(`${apiBase}/ingest/watch/reports/${encodeURIComponent(bugId)}/rating${projectQuery()}`, {
|
|
1105
|
+
method: 'POST',
|
|
1106
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
1107
|
+
body: JSON.stringify({ score, ...(comment ? { comment } : {}) }),
|
|
1108
|
+
});
|
|
1109
|
+
const data = await res.json().catch(() => ({}));
|
|
1110
|
+
if (!res.ok) {
|
|
1111
|
+
const message = Array.isArray(data.message) ? data.message.join(', ') : data.message;
|
|
1112
|
+
throw new Error(message || 'Could not send your rating. Please try again.');
|
|
1113
|
+
}
|
|
1114
|
+
return data;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1067
1117
|
export async function verifyFix({ apiBase, bugId, ...feedback }) {
|
|
1068
1118
|
const token = readToken();
|
|
1069
1119
|
if (!token) throw new Error('This browser cannot access that report.');
|