@mhosaic/feedback 0.6.3 → 0.7.1
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/dist/{chunk-3RIR3JHF.mjs → chunk-F3FVKCBE.mjs} +837 -33
- package/dist/chunk-F3FVKCBE.mjs.map +1 -0
- package/dist/embed.min.js +264 -5
- package/dist/embed.min.js.map +1 -1
- package/dist/error-tracking.d.ts +1 -1
- package/dist/{index-DRmZlKSW.d.ts → index-CasX-2Bm.d.ts} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.mjs +1 -1
- package/dist/replay.d.ts +1 -1
- package/dist/{types-BCay5lo8.d.ts → types-CNwNoYUE.d.ts} +10 -0
- package/dist/webvitals.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-3RIR3JHF.mjs.map +0 -1
|
@@ -27,6 +27,9 @@ function createApiClient(options) {
|
|
|
27
27
|
form.append("technical_context", JSON.stringify(payload.technical_context));
|
|
28
28
|
if (payload.screenshot) form.append("screenshot", payload.screenshot, "screenshot.png");
|
|
29
29
|
if (payload.synthetic) form.append("synthetic", "true");
|
|
30
|
+
if (payload.user?.id) {
|
|
31
|
+
form.append("user", JSON.stringify(payload.user));
|
|
32
|
+
}
|
|
30
33
|
const response = await fetcher(`${endpoint}/api/feedback/v1/reports/`, {
|
|
31
34
|
method: "POST",
|
|
32
35
|
headers: { Authorization: `Bearer ${options.apiKey}` },
|
|
@@ -38,7 +41,75 @@ function createApiClient(options) {
|
|
|
38
41
|
}
|
|
39
42
|
return response.json();
|
|
40
43
|
}
|
|
41
|
-
|
|
44
|
+
function widgetHeaders(externalId) {
|
|
45
|
+
return {
|
|
46
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
47
|
+
"X-Mhosaic-User": externalId
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function listMine(externalId) {
|
|
51
|
+
const response = await fetcher(`${endpoint}/api/feedback/v1/reports/widget/mine/`, {
|
|
52
|
+
method: "GET",
|
|
53
|
+
headers: widgetHeaders(externalId)
|
|
54
|
+
});
|
|
55
|
+
if (response.status === 404) return [];
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const text = await response.text().catch(() => "");
|
|
58
|
+
throw new Error(`listMine failed: ${response.status} ${text}`);
|
|
59
|
+
}
|
|
60
|
+
return response.json();
|
|
61
|
+
}
|
|
62
|
+
async function getReport(reportId, externalId) {
|
|
63
|
+
const response = await fetcher(
|
|
64
|
+
`${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
|
|
65
|
+
{ method: "GET", headers: widgetHeaders(externalId) }
|
|
66
|
+
);
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const text = await response.text().catch(() => "");
|
|
69
|
+
throw new Error(`getReport failed: ${response.status} ${text}`);
|
|
70
|
+
}
|
|
71
|
+
return response.json();
|
|
72
|
+
}
|
|
73
|
+
async function addComment(reportId, externalId, body, clientNonce) {
|
|
74
|
+
const response = await fetcher(
|
|
75
|
+
`${endpoint}/api/feedback/v1/reports/widget/${reportId}/comments/`,
|
|
76
|
+
{
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: {
|
|
79
|
+
...widgetHeaders(externalId),
|
|
80
|
+
"Content-Type": "application/json"
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
body,
|
|
84
|
+
...clientNonce !== void 0 && { client_nonce: clientNonce }
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const text = await response.text().catch(() => "");
|
|
90
|
+
throw new Error(`addComment failed: ${response.status} ${text}`);
|
|
91
|
+
}
|
|
92
|
+
return response.json();
|
|
93
|
+
}
|
|
94
|
+
async function closeAsResolved(reportId, externalId) {
|
|
95
|
+
const response = await fetcher(
|
|
96
|
+
`${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
|
|
97
|
+
{
|
|
98
|
+
method: "PATCH",
|
|
99
|
+
headers: {
|
|
100
|
+
...widgetHeaders(externalId),
|
|
101
|
+
"Content-Type": "application/json"
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({ status: "closed" })
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const text = await response.text().catch(() => "");
|
|
108
|
+
throw new Error(`closeAsResolved failed: ${response.status} ${text}`);
|
|
109
|
+
}
|
|
110
|
+
return response.json();
|
|
111
|
+
}
|
|
112
|
+
return { submitReport, listMine, getReport, addComment, closeAsResolved };
|
|
42
113
|
}
|
|
43
114
|
|
|
44
115
|
// src/capture/urlSanitizer.ts
|
|
@@ -396,7 +467,41 @@ var DEFAULT_STRINGS = {
|
|
|
396
467
|
"annotator.count_suffix": "annotations",
|
|
397
468
|
"annotator.loading": "Loading\u2026",
|
|
398
469
|
"annotator.apply": "Apply",
|
|
399
|
-
"annotator.applying": "Applying\u2026"
|
|
470
|
+
"annotator.applying": "Applying\u2026",
|
|
471
|
+
"tab.send": "Send",
|
|
472
|
+
"tab.mine": "My reports",
|
|
473
|
+
"mine.empty.title": "No reports yet",
|
|
474
|
+
"mine.empty.body": "Once you send feedback you can follow the thread here.",
|
|
475
|
+
"mine.refresh": "Refresh",
|
|
476
|
+
"mine.loading": "Loading\u2026",
|
|
477
|
+
"mine.error": "Could not load your reports.",
|
|
478
|
+
"mine.replies_one": "1 reply",
|
|
479
|
+
"mine.replies_many": "{count} replies",
|
|
480
|
+
"detail.back": "Back",
|
|
481
|
+
"detail.thread": "Conversation",
|
|
482
|
+
"detail.no_replies": "No replies yet \u2014 we\u2019ll let you know when an operator responds.",
|
|
483
|
+
"detail.compose_placeholder": "Add a follow-up reply\u2026",
|
|
484
|
+
"detail.compose_send": "Reply",
|
|
485
|
+
"detail.compose_sending": "Sending\u2026",
|
|
486
|
+
"detail.close_cta": "Mark as resolved",
|
|
487
|
+
"detail.close_busy": "Marking\u2026",
|
|
488
|
+
"detail.history": "Status history",
|
|
489
|
+
"detail.context.submitted_at": "Submitted",
|
|
490
|
+
"detail.context.page": "Page",
|
|
491
|
+
"detail.context.capture.manual": "Manual capture",
|
|
492
|
+
"detail.context.capture.html2canvas": "Auto capture",
|
|
493
|
+
"detail.context.capture.display_media": "Screen share",
|
|
494
|
+
"detail.context.capture.none": "No screenshot",
|
|
495
|
+
"detail.author.staff": "Operator",
|
|
496
|
+
"detail.author.mcp": "Mhosaic Team",
|
|
497
|
+
"detail.author.system": "System",
|
|
498
|
+
"status.new": "New",
|
|
499
|
+
"status.in_progress": "In progress",
|
|
500
|
+
"status.awaiting_validation": "Awaiting your validation",
|
|
501
|
+
"status.closed": "Closed",
|
|
502
|
+
"status.rejected": "Rejected",
|
|
503
|
+
"status.duplicate": "Duplicate",
|
|
504
|
+
"status.wontfix": "Won\u2019t fix"
|
|
400
505
|
};
|
|
401
506
|
var FRENCH_STRINGS = {
|
|
402
507
|
"fab.label": "Envoyer un commentaire",
|
|
@@ -441,7 +546,41 @@ var FRENCH_STRINGS = {
|
|
|
441
546
|
"annotator.count_suffix": "annotations",
|
|
442
547
|
"annotator.loading": "Chargement\u2026",
|
|
443
548
|
"annotator.apply": "Appliquer",
|
|
444
|
-
"annotator.applying": "Application\u2026"
|
|
549
|
+
"annotator.applying": "Application\u2026",
|
|
550
|
+
"tab.send": "Envoyer",
|
|
551
|
+
"tab.mine": "Mes rapports",
|
|
552
|
+
"mine.empty.title": "Aucun rapport",
|
|
553
|
+
"mine.empty.body": "Apr\xE8s votre premier envoi vous pourrez suivre la conversation ici.",
|
|
554
|
+
"mine.refresh": "Actualiser",
|
|
555
|
+
"mine.loading": "Chargement\u2026",
|
|
556
|
+
"mine.error": "Impossible de charger vos rapports.",
|
|
557
|
+
"mine.replies_one": "1 r\xE9ponse",
|
|
558
|
+
"mine.replies_many": "{count} r\xE9ponses",
|
|
559
|
+
"detail.back": "Retour",
|
|
560
|
+
"detail.thread": "Conversation",
|
|
561
|
+
"detail.no_replies": "Pas encore de r\xE9ponse \u2014 vous serez notifi\xE9 d\xE8s qu\u2019un op\xE9rateur r\xE9pondra.",
|
|
562
|
+
"detail.compose_placeholder": "Ajouter une r\xE9ponse\u2026",
|
|
563
|
+
"detail.compose_send": "R\xE9pondre",
|
|
564
|
+
"detail.compose_sending": "Envoi\u2026",
|
|
565
|
+
"detail.close_cta": "Marquer comme r\xE9solu",
|
|
566
|
+
"detail.close_busy": "Validation\u2026",
|
|
567
|
+
"detail.history": "Historique du statut",
|
|
568
|
+
"detail.context.submitted_at": "Envoy\xE9",
|
|
569
|
+
"detail.context.page": "Page",
|
|
570
|
+
"detail.context.capture.manual": "Capture manuelle",
|
|
571
|
+
"detail.context.capture.html2canvas": "Capture automatique",
|
|
572
|
+
"detail.context.capture.display_media": "Partage d\u2019\xE9cran",
|
|
573
|
+
"detail.context.capture.none": "Sans capture",
|
|
574
|
+
"detail.author.staff": "Op\xE9rateur",
|
|
575
|
+
"detail.author.mcp": "\xC9quipe Mhosaic",
|
|
576
|
+
"detail.author.system": "Syst\xE8me",
|
|
577
|
+
"status.new": "Nouveau",
|
|
578
|
+
"status.in_progress": "En cours",
|
|
579
|
+
"status.awaiting_validation": "En attente de validation",
|
|
580
|
+
"status.closed": "Ferm\xE9",
|
|
581
|
+
"status.rejected": "Rejet\xE9",
|
|
582
|
+
"status.duplicate": "Doublon",
|
|
583
|
+
"status.wontfix": "Non corrig\xE9"
|
|
445
584
|
};
|
|
446
585
|
var LOCALE_PACKS = {
|
|
447
586
|
fr: FRENCH_STRINGS
|
|
@@ -1058,13 +1197,123 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
|
|
|
1058
1197
|
] });
|
|
1059
1198
|
}
|
|
1060
1199
|
|
|
1061
|
-
// src/widget/
|
|
1062
|
-
import { useEffect as useEffect3, useRef as useRef3 } from "preact/hooks";
|
|
1200
|
+
// src/widget/MineList.tsx
|
|
1201
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "preact/hooks";
|
|
1202
|
+
|
|
1203
|
+
// src/widget/ReportRow.tsx
|
|
1063
1204
|
import { jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
|
|
1064
|
-
function
|
|
1065
|
-
|
|
1066
|
-
|
|
1205
|
+
function statusClassName(status) {
|
|
1206
|
+
return `pill pill-status pill-status--${status}`;
|
|
1207
|
+
}
|
|
1208
|
+
function severityClassName(severity) {
|
|
1209
|
+
return `pill pill-severity pill-severity--${severity}`;
|
|
1210
|
+
}
|
|
1211
|
+
function typeClassName() {
|
|
1212
|
+
return "pill pill-type";
|
|
1213
|
+
}
|
|
1214
|
+
function formatRelative(iso) {
|
|
1215
|
+
const then = Date.parse(iso);
|
|
1216
|
+
if (!Number.isFinite(then)) return "";
|
|
1217
|
+
const seconds = Math.max(1, Math.round((Date.now() - then) / 1e3));
|
|
1218
|
+
if (seconds < 60) return `${seconds}s`;
|
|
1219
|
+
const minutes = Math.round(seconds / 60);
|
|
1220
|
+
if (minutes < 60) return `${minutes}m`;
|
|
1221
|
+
const hours = Math.round(minutes / 60);
|
|
1222
|
+
if (hours < 48) return `${hours}h`;
|
|
1223
|
+
const days = Math.round(hours / 24);
|
|
1224
|
+
return `${days}d`;
|
|
1225
|
+
}
|
|
1226
|
+
function repliesLabel(count, strings) {
|
|
1227
|
+
if (count === 1) return strings["mine.replies_one"];
|
|
1228
|
+
return strings["mine.replies_many"].replace("{count}", String(count));
|
|
1229
|
+
}
|
|
1230
|
+
function ReportRow({ row, strings, onClick }) {
|
|
1231
|
+
const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
|
|
1232
|
+
return /* @__PURE__ */ jsxs3("button", { type: "button", class: "mine-row", onClick, children: [
|
|
1233
|
+
/* @__PURE__ */ jsxs3("div", { class: "mine-row-pills", children: [
|
|
1234
|
+
/* @__PURE__ */ jsx4("span", { class: statusClassName(row.status), children: strings[`status.${row.status}`] ?? row.status }),
|
|
1235
|
+
/* @__PURE__ */ jsx4("span", { class: typeClassName(), children: strings[`type.${row.feedback_type}`] }),
|
|
1236
|
+
/* @__PURE__ */ jsx4("span", { class: severityClassName(row.severity), children: strings[`severity.${row.severity}`] })
|
|
1237
|
+
] }),
|
|
1238
|
+
/* @__PURE__ */ jsx4("div", { class: "mine-row-preview", children: preview }),
|
|
1239
|
+
/* @__PURE__ */ jsxs3("div", { class: "mine-row-meta", children: [
|
|
1240
|
+
/* @__PURE__ */ jsx4("span", { children: formatRelative(row.updated_at || row.created_at) }),
|
|
1241
|
+
row.comment_count > 0 && /* @__PURE__ */ jsxs3("span", { children: [
|
|
1242
|
+
"\xB7 ",
|
|
1243
|
+
repliesLabel(row.comment_count, strings)
|
|
1244
|
+
] })
|
|
1245
|
+
] })
|
|
1246
|
+
] });
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/widget/MineList.tsx
|
|
1250
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
|
|
1251
|
+
var POLL_MS = 3e4;
|
|
1252
|
+
function MineList({ api, externalId, strings, onSelect }) {
|
|
1253
|
+
const [rows, setRows] = useState3(null);
|
|
1254
|
+
const [error, setError] = useState3(null);
|
|
1255
|
+
const [refreshing, setRefreshing] = useState3(false);
|
|
1256
|
+
const mountedRef = useRef3(true);
|
|
1257
|
+
const fetchRows = async () => {
|
|
1258
|
+
setRefreshing(true);
|
|
1259
|
+
setError(null);
|
|
1260
|
+
try {
|
|
1261
|
+
const next = await api.listMine(externalId);
|
|
1262
|
+
if (!mountedRef.current) return;
|
|
1263
|
+
setRows(next);
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
if (!mountedRef.current) return;
|
|
1266
|
+
setError(err instanceof Error ? err.message : strings["mine.error"]);
|
|
1267
|
+
} finally {
|
|
1268
|
+
if (mountedRef.current) setRefreshing(false);
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1067
1271
|
useEffect3(() => {
|
|
1272
|
+
mountedRef.current = true;
|
|
1273
|
+
void fetchRows();
|
|
1274
|
+
const timer = setInterval(() => {
|
|
1275
|
+
void fetchRows();
|
|
1276
|
+
}, POLL_MS);
|
|
1277
|
+
return () => {
|
|
1278
|
+
mountedRef.current = false;
|
|
1279
|
+
clearInterval(timer);
|
|
1280
|
+
};
|
|
1281
|
+
}, [externalId]);
|
|
1282
|
+
const isEmpty = rows !== null && rows.length === 0;
|
|
1283
|
+
const isLoading = rows === null && !error;
|
|
1284
|
+
return /* @__PURE__ */ jsxs4("div", { class: "mine-list", children: [
|
|
1285
|
+
/* @__PURE__ */ jsxs4("div", { class: "mine-list-header", children: [
|
|
1286
|
+
/* @__PURE__ */ jsx5("h2", { children: strings["tab.mine"] }),
|
|
1287
|
+
/* @__PURE__ */ jsx5(
|
|
1288
|
+
"button",
|
|
1289
|
+
{
|
|
1290
|
+
type: "button",
|
|
1291
|
+
class: "btn",
|
|
1292
|
+
onClick: () => {
|
|
1293
|
+
void fetchRows();
|
|
1294
|
+
},
|
|
1295
|
+
disabled: refreshing,
|
|
1296
|
+
children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
|
|
1297
|
+
}
|
|
1298
|
+
)
|
|
1299
|
+
] }),
|
|
1300
|
+
isLoading && /* @__PURE__ */ jsx5("div", { class: "mine-loading", children: strings["mine.loading"] }),
|
|
1301
|
+
error && /* @__PURE__ */ jsx5("div", { class: "error", children: error }),
|
|
1302
|
+
isEmpty && /* @__PURE__ */ jsxs4("div", { class: "mine-empty", children: [
|
|
1303
|
+
/* @__PURE__ */ jsx5("strong", { children: strings["mine.empty.title"] }),
|
|
1304
|
+
/* @__PURE__ */ jsx5("p", { children: strings["mine.empty.body"] })
|
|
1305
|
+
] }),
|
|
1306
|
+
rows && rows.length > 0 && /* @__PURE__ */ jsx5("ul", { class: "mine-rows", children: rows.map((row) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
|
|
1307
|
+
] });
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// src/widget/Modal.tsx
|
|
1311
|
+
import { useEffect as useEffect4, useRef as useRef4 } from "preact/hooks";
|
|
1312
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
|
|
1313
|
+
function Modal({ onDismiss, children, closeLabel = "Close" }) {
|
|
1314
|
+
const modalRef = useRef4(null);
|
|
1315
|
+
const previouslyFocused = useRef4(null);
|
|
1316
|
+
useEffect4(() => {
|
|
1068
1317
|
previouslyFocused.current = document.activeElement;
|
|
1069
1318
|
const onKey = (e) => {
|
|
1070
1319
|
if (e.key !== "Escape") return;
|
|
@@ -1086,7 +1335,7 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
|
|
|
1086
1335
|
if (prev && typeof prev.focus === "function") prev.focus();
|
|
1087
1336
|
};
|
|
1088
1337
|
}, [onDismiss]);
|
|
1089
|
-
return /* @__PURE__ */
|
|
1338
|
+
return /* @__PURE__ */ jsx6(
|
|
1090
1339
|
"div",
|
|
1091
1340
|
{
|
|
1092
1341
|
class: "backdrop",
|
|
@@ -1094,8 +1343,8 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
|
|
|
1094
1343
|
onClick: (e) => {
|
|
1095
1344
|
if (e.target === e.currentTarget) onDismiss();
|
|
1096
1345
|
},
|
|
1097
|
-
children: /* @__PURE__ */
|
|
1098
|
-
/* @__PURE__ */
|
|
1346
|
+
children: /* @__PURE__ */ jsxs5("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
|
|
1347
|
+
/* @__PURE__ */ jsx6(
|
|
1099
1348
|
"button",
|
|
1100
1349
|
{
|
|
1101
1350
|
type: "button",
|
|
@@ -1111,6 +1360,216 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
|
|
|
1111
1360
|
);
|
|
1112
1361
|
}
|
|
1113
1362
|
|
|
1363
|
+
// src/widget/ReportDetailView.tsx
|
|
1364
|
+
import { useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "preact/hooks";
|
|
1365
|
+
|
|
1366
|
+
// src/widget/CommentBubble.tsx
|
|
1367
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "preact/jsx-runtime";
|
|
1368
|
+
function CommentBubble({ comment, strings }) {
|
|
1369
|
+
const isMine = comment.is_mine;
|
|
1370
|
+
const isAgent = !isMine && comment.author_source === "mcp";
|
|
1371
|
+
const isSystem = !isMine && comment.author_source === "system";
|
|
1372
|
+
const variant = isAgent ? "mcp" : isSystem ? "system" : "staff";
|
|
1373
|
+
const labelKey = variant === "mcp" ? "detail.author.mcp" : variant === "system" ? "detail.author.system" : "detail.author.staff";
|
|
1374
|
+
const label = comment.author_label || strings[labelKey];
|
|
1375
|
+
return /* @__PURE__ */ jsxs6("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
|
|
1376
|
+
!isMine && label && /* @__PURE__ */ jsx7("div", { class: `comment-author comment-author--${variant}`, children: label }),
|
|
1377
|
+
/* @__PURE__ */ jsx7("div", { class: "comment-body", children: comment.body }),
|
|
1378
|
+
/* @__PURE__ */ jsx7("div", { class: "comment-time", children: formatTime(comment.created_at) })
|
|
1379
|
+
] });
|
|
1380
|
+
}
|
|
1381
|
+
function formatTime(iso) {
|
|
1382
|
+
try {
|
|
1383
|
+
return new Date(iso).toLocaleString(void 0, {
|
|
1384
|
+
dateStyle: "short",
|
|
1385
|
+
timeStyle: "short"
|
|
1386
|
+
});
|
|
1387
|
+
} catch {
|
|
1388
|
+
return iso;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// src/widget/ReportDetailView.tsx
|
|
1393
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "preact/jsx-runtime";
|
|
1394
|
+
var POLL_MS2 = 3e4;
|
|
1395
|
+
function ReportDetailView({
|
|
1396
|
+
api,
|
|
1397
|
+
externalId,
|
|
1398
|
+
reportId,
|
|
1399
|
+
strings,
|
|
1400
|
+
onBack
|
|
1401
|
+
}) {
|
|
1402
|
+
const [detail, setDetail] = useState4(null);
|
|
1403
|
+
const [error, setError] = useState4(null);
|
|
1404
|
+
const [composeBody, setComposeBody] = useState4("");
|
|
1405
|
+
const [sending, setSending] = useState4(false);
|
|
1406
|
+
const [closing, setClosing] = useState4(false);
|
|
1407
|
+
const mountedRef = useRef5(true);
|
|
1408
|
+
const fetchDetail = async () => {
|
|
1409
|
+
try {
|
|
1410
|
+
const next = await api.getReport(reportId, externalId);
|
|
1411
|
+
if (!mountedRef.current) return;
|
|
1412
|
+
setDetail(next);
|
|
1413
|
+
setError(null);
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
if (!mountedRef.current) return;
|
|
1416
|
+
setError(err instanceof Error ? err.message : "load_failed");
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
useEffect5(() => {
|
|
1420
|
+
mountedRef.current = true;
|
|
1421
|
+
void fetchDetail();
|
|
1422
|
+
const timer = setInterval(() => {
|
|
1423
|
+
void fetchDetail();
|
|
1424
|
+
}, POLL_MS2);
|
|
1425
|
+
return () => {
|
|
1426
|
+
mountedRef.current = false;
|
|
1427
|
+
clearInterval(timer);
|
|
1428
|
+
};
|
|
1429
|
+
}, [reportId, externalId]);
|
|
1430
|
+
const handleSend = async () => {
|
|
1431
|
+
if (!composeBody.trim() || sending) return;
|
|
1432
|
+
setSending(true);
|
|
1433
|
+
try {
|
|
1434
|
+
const nonce = `${reportId}:${Date.now()}`;
|
|
1435
|
+
const created = await api.addComment(reportId, externalId, composeBody.trim(), nonce);
|
|
1436
|
+
if (!mountedRef.current) return;
|
|
1437
|
+
setComposeBody("");
|
|
1438
|
+
setDetail(
|
|
1439
|
+
(prev) => prev ? { ...prev, comments: appendComment(prev.comments, created) } : prev
|
|
1440
|
+
);
|
|
1441
|
+
void fetchDetail();
|
|
1442
|
+
} catch (err) {
|
|
1443
|
+
if (!mountedRef.current) return;
|
|
1444
|
+
setError(err instanceof Error ? err.message : "comment_failed");
|
|
1445
|
+
} finally {
|
|
1446
|
+
if (mountedRef.current) setSending(false);
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
const handleClose = async () => {
|
|
1450
|
+
if (closing) return;
|
|
1451
|
+
setClosing(true);
|
|
1452
|
+
try {
|
|
1453
|
+
const next = await api.closeAsResolved(reportId, externalId);
|
|
1454
|
+
if (!mountedRef.current) return;
|
|
1455
|
+
setDetail(next);
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
if (!mountedRef.current) return;
|
|
1458
|
+
setError(err instanceof Error ? err.message : "close_failed");
|
|
1459
|
+
} finally {
|
|
1460
|
+
if (mountedRef.current) setClosing(false);
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
if (!detail && !error) {
|
|
1464
|
+
return /* @__PURE__ */ jsx8("div", { class: "mine-loading", children: strings["mine.loading"] });
|
|
1465
|
+
}
|
|
1466
|
+
if (!detail) {
|
|
1467
|
+
return /* @__PURE__ */ jsx8("div", { class: "error", role: "alert", children: error });
|
|
1468
|
+
}
|
|
1469
|
+
const showCloseCta = detail.status === "awaiting_validation";
|
|
1470
|
+
return /* @__PURE__ */ jsxs7("div", { class: "report-detail", children: [
|
|
1471
|
+
/* @__PURE__ */ jsxs7("div", { class: "report-detail-header", children: [
|
|
1472
|
+
/* @__PURE__ */ jsxs7("button", { type: "button", class: "btn", onClick: onBack, children: [
|
|
1473
|
+
"\u2190 ",
|
|
1474
|
+
strings["detail.back"]
|
|
1475
|
+
] }),
|
|
1476
|
+
/* @__PURE__ */ jsx8("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
|
|
1477
|
+
] }),
|
|
1478
|
+
/* @__PURE__ */ jsxs7("div", { class: "report-detail-body", children: [
|
|
1479
|
+
/* @__PURE__ */ jsx8(ContextBlock, { detail, strings }),
|
|
1480
|
+
/* @__PURE__ */ jsx8("p", { class: "report-detail-description", children: detail.description }),
|
|
1481
|
+
detail.screenshot_url && /* @__PURE__ */ jsx8(
|
|
1482
|
+
"a",
|
|
1483
|
+
{
|
|
1484
|
+
class: "report-detail-screenshot",
|
|
1485
|
+
href: detail.screenshot_url,
|
|
1486
|
+
target: "_blank",
|
|
1487
|
+
rel: "noopener noreferrer",
|
|
1488
|
+
children: /* @__PURE__ */ jsx8("img", { src: detail.screenshot_url, alt: "", loading: "lazy" })
|
|
1489
|
+
}
|
|
1490
|
+
),
|
|
1491
|
+
/* @__PURE__ */ jsx8("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
|
|
1492
|
+
detail.comments.length === 0 ? /* @__PURE__ */ jsx8("p", { class: "report-detail-empty", children: strings["detail.no_replies"] }) : /* @__PURE__ */ jsx8("ul", { class: "report-comments", children: detail.comments.map((c) => /* @__PURE__ */ jsx8("li", { children: /* @__PURE__ */ jsx8(CommentBubble, { comment: c, strings }) })) }),
|
|
1493
|
+
/* @__PURE__ */ jsxs7("div", { class: "report-compose", children: [
|
|
1494
|
+
/* @__PURE__ */ jsx8(
|
|
1495
|
+
"textarea",
|
|
1496
|
+
{
|
|
1497
|
+
value: composeBody,
|
|
1498
|
+
placeholder: strings["detail.compose_placeholder"],
|
|
1499
|
+
onInput: (e) => setComposeBody(e.target.value),
|
|
1500
|
+
disabled: sending
|
|
1501
|
+
}
|
|
1502
|
+
),
|
|
1503
|
+
/* @__PURE__ */ jsxs7("div", { class: "report-compose-actions", children: [
|
|
1504
|
+
showCloseCta && /* @__PURE__ */ jsx8(
|
|
1505
|
+
"button",
|
|
1506
|
+
{
|
|
1507
|
+
type: "button",
|
|
1508
|
+
class: "btn",
|
|
1509
|
+
onClick: handleClose,
|
|
1510
|
+
disabled: closing,
|
|
1511
|
+
children: closing ? strings["detail.close_busy"] : strings["detail.close_cta"]
|
|
1512
|
+
}
|
|
1513
|
+
),
|
|
1514
|
+
/* @__PURE__ */ jsx8(
|
|
1515
|
+
"button",
|
|
1516
|
+
{
|
|
1517
|
+
type: "button",
|
|
1518
|
+
class: "btn btn--primary",
|
|
1519
|
+
onClick: handleSend,
|
|
1520
|
+
disabled: !composeBody.trim() || sending,
|
|
1521
|
+
children: sending ? strings["detail.compose_sending"] : strings["detail.compose_send"]
|
|
1522
|
+
}
|
|
1523
|
+
)
|
|
1524
|
+
] })
|
|
1525
|
+
] }),
|
|
1526
|
+
error && /* @__PURE__ */ jsx8("div", { class: "error", children: error })
|
|
1527
|
+
] })
|
|
1528
|
+
] });
|
|
1529
|
+
}
|
|
1530
|
+
function appendComment(current, next) {
|
|
1531
|
+
if (current.some((c) => c.id === next.id)) return current;
|
|
1532
|
+
return [...current, next];
|
|
1533
|
+
}
|
|
1534
|
+
function ContextBlock({ detail, strings }) {
|
|
1535
|
+
const captureKey = `detail.context.capture.${detail.capture_method}`;
|
|
1536
|
+
const captureLabel = strings[captureKey] ?? detail.capture_method;
|
|
1537
|
+
return /* @__PURE__ */ jsxs7("div", { class: "report-detail-context", children: [
|
|
1538
|
+
/* @__PURE__ */ jsxs7("div", { class: "report-detail-context-pills", children: [
|
|
1539
|
+
/* @__PURE__ */ jsx8("span", { class: "pill pill-type", children: strings[`type.${detail.feedback_type}`] }),
|
|
1540
|
+
/* @__PURE__ */ jsx8("span", { class: `pill pill-severity pill-severity--${detail.severity}`, children: strings[`severity.${detail.severity}`] }),
|
|
1541
|
+
/* @__PURE__ */ jsx8("span", { class: "pill pill-capture", children: captureLabel })
|
|
1542
|
+
] }),
|
|
1543
|
+
/* @__PURE__ */ jsxs7("div", { class: "report-detail-context-line", children: [
|
|
1544
|
+
/* @__PURE__ */ jsx8("span", { class: "report-detail-context-label", children: strings["detail.context.submitted_at"] }),
|
|
1545
|
+
/* @__PURE__ */ jsx8("span", { children: formatSubmittedAt(detail.created_at) })
|
|
1546
|
+
] }),
|
|
1547
|
+
detail.page_url && /* @__PURE__ */ jsxs7("div", { class: "report-detail-context-line", title: detail.page_url, children: [
|
|
1548
|
+
/* @__PURE__ */ jsx8("span", { class: "report-detail-context-label", children: strings["detail.context.page"] }),
|
|
1549
|
+
/* @__PURE__ */ jsx8(
|
|
1550
|
+
"a",
|
|
1551
|
+
{
|
|
1552
|
+
class: "report-detail-context-url",
|
|
1553
|
+
href: detail.page_url,
|
|
1554
|
+
target: "_blank",
|
|
1555
|
+
rel: "noopener noreferrer",
|
|
1556
|
+
children: truncateUrl(detail.page_url, 64)
|
|
1557
|
+
}
|
|
1558
|
+
)
|
|
1559
|
+
] })
|
|
1560
|
+
] });
|
|
1561
|
+
}
|
|
1562
|
+
function formatSubmittedAt(iso) {
|
|
1563
|
+
try {
|
|
1564
|
+
return new Date(iso).toLocaleString(void 0, {
|
|
1565
|
+
dateStyle: "medium",
|
|
1566
|
+
timeStyle: "short"
|
|
1567
|
+
});
|
|
1568
|
+
} catch {
|
|
1569
|
+
return iso;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1114
1573
|
// src/widget/styles.ts
|
|
1115
1574
|
var WIDGET_STYLES = `
|
|
1116
1575
|
:host {
|
|
@@ -1496,10 +1955,269 @@ var WIDGET_STYLES = `
|
|
|
1496
1955
|
padding: 10px 14px;
|
|
1497
1956
|
border-top: 1px solid var(--mfb-border);
|
|
1498
1957
|
}
|
|
1958
|
+
|
|
1959
|
+
/* ---- v0.7: tabs + reader UI -------------------------------------- */
|
|
1960
|
+
|
|
1961
|
+
.tab-strip {
|
|
1962
|
+
display: flex;
|
|
1963
|
+
gap: 4px;
|
|
1964
|
+
border-bottom: 1px solid var(--mfb-border);
|
|
1965
|
+
margin: 0 -4px 4px;
|
|
1966
|
+
padding: 0 4px;
|
|
1967
|
+
}
|
|
1968
|
+
.tab-button {
|
|
1969
|
+
appearance: none;
|
|
1970
|
+
background: transparent;
|
|
1971
|
+
border: 0;
|
|
1972
|
+
border-bottom: 2px solid transparent;
|
|
1973
|
+
padding: 8px 12px;
|
|
1974
|
+
font: inherit;
|
|
1975
|
+
font-size: 13px;
|
|
1976
|
+
font-weight: 500;
|
|
1977
|
+
color: var(--mfb-text-muted);
|
|
1978
|
+
cursor: pointer;
|
|
1979
|
+
}
|
|
1980
|
+
.tab-button:hover { color: var(--mfb-text); }
|
|
1981
|
+
.tab-button.is-active {
|
|
1982
|
+
color: var(--mfb-text);
|
|
1983
|
+
border-bottom-color: var(--mfb-accent);
|
|
1984
|
+
}
|
|
1985
|
+
.tab-button[aria-selected="true"] { font-weight: 600; }
|
|
1986
|
+
|
|
1987
|
+
.mine-list { display: flex; flex-direction: column; gap: 10px; }
|
|
1988
|
+
.mine-list-header {
|
|
1989
|
+
display: flex;
|
|
1990
|
+
align-items: center;
|
|
1991
|
+
justify-content: space-between;
|
|
1992
|
+
}
|
|
1993
|
+
.mine-list-header h2 { margin: 0; font-size: 15px; font-weight: 600; }
|
|
1994
|
+
.mine-loading { color: var(--mfb-text-muted); font-size: 13px; }
|
|
1995
|
+
.mine-empty {
|
|
1996
|
+
text-align: center;
|
|
1997
|
+
padding: 24px 12px;
|
|
1998
|
+
color: var(--mfb-text-muted);
|
|
1999
|
+
font-size: 13px;
|
|
2000
|
+
}
|
|
2001
|
+
.mine-empty strong { display: block; color: var(--mfb-text); margin-bottom: 4px; }
|
|
2002
|
+
|
|
2003
|
+
.mine-rows {
|
|
2004
|
+
list-style: none;
|
|
2005
|
+
margin: 0;
|
|
2006
|
+
padding: 0;
|
|
2007
|
+
display: flex;
|
|
2008
|
+
flex-direction: column;
|
|
2009
|
+
gap: 6px;
|
|
2010
|
+
max-height: 380px;
|
|
2011
|
+
overflow-y: auto;
|
|
2012
|
+
}
|
|
2013
|
+
.mine-row {
|
|
2014
|
+
appearance: none;
|
|
2015
|
+
text-align: left;
|
|
2016
|
+
background: var(--mfb-surface);
|
|
2017
|
+
border: 1px solid var(--mfb-border);
|
|
2018
|
+
border-radius: var(--mfb-radius);
|
|
2019
|
+
padding: 10px 12px;
|
|
2020
|
+
font: inherit;
|
|
2021
|
+
color: inherit;
|
|
2022
|
+
cursor: pointer;
|
|
2023
|
+
display: flex;
|
|
2024
|
+
flex-direction: column;
|
|
2025
|
+
gap: 4px;
|
|
2026
|
+
width: 100%;
|
|
2027
|
+
transition: border-color 120ms ease, background 120ms ease;
|
|
2028
|
+
}
|
|
2029
|
+
.mine-row:hover { border-color: var(--mfb-text-muted); }
|
|
2030
|
+
.mine-row:focus-visible {
|
|
2031
|
+
outline: 2px solid var(--mfb-accent);
|
|
2032
|
+
outline-offset: 2px;
|
|
2033
|
+
}
|
|
2034
|
+
.mine-row-pills { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
2035
|
+
.mine-row-preview {
|
|
2036
|
+
font-size: 13px;
|
|
2037
|
+
color: var(--mfb-text);
|
|
2038
|
+
display: -webkit-box;
|
|
2039
|
+
-webkit-line-clamp: 2;
|
|
2040
|
+
-webkit-box-orient: vertical;
|
|
2041
|
+
overflow: hidden;
|
|
2042
|
+
}
|
|
2043
|
+
.mine-row-meta {
|
|
2044
|
+
display: flex;
|
|
2045
|
+
gap: 6px;
|
|
2046
|
+
font-size: 11px;
|
|
2047
|
+
color: var(--mfb-text-muted);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
.pill {
|
|
2051
|
+
display: inline-block;
|
|
2052
|
+
font-size: 10px;
|
|
2053
|
+
font-weight: 600;
|
|
2054
|
+
text-transform: uppercase;
|
|
2055
|
+
letter-spacing: 0.04em;
|
|
2056
|
+
padding: 2px 8px;
|
|
2057
|
+
border-radius: 999px;
|
|
2058
|
+
border: 1px solid transparent;
|
|
2059
|
+
}
|
|
2060
|
+
.pill-status { background: #eff6ff; color: #1e40af; border-color: #dbeafe; }
|
|
2061
|
+
.pill-status--in_progress { background: #fffbeb; color: #92400e; border-color: #fde68a; }
|
|
2062
|
+
.pill-status--awaiting_validation { background: #faf5ff; color: #6b21a8; border-color: #e9d5ff; }
|
|
2063
|
+
.pill-status--closed { background: #ecfdf5; color: #065f46; border-color: #a7f3d0; }
|
|
2064
|
+
.pill-status--rejected, .pill-status--wontfix, .pill-status--duplicate { background: #f3f4f6; color: #374151; border-color: #e5e7eb; }
|
|
2065
|
+
.pill-type { background: var(--mfb-surface); color: var(--mfb-text-muted); border-color: var(--mfb-border); }
|
|
2066
|
+
.pill-severity { background: var(--mfb-surface); color: var(--mfb-text-muted); border-color: var(--mfb-border); }
|
|
2067
|
+
.pill-severity--blocker { background: #fef2f2; color: #991b1b; border-color: #fecaca; }
|
|
2068
|
+
.pill-severity--high { background: #fff7ed; color: #9a3412; border-color: #fed7aa; }
|
|
2069
|
+
.pill-severity--medium { background: #fefce8; color: #854d0e; border-color: #fef08a; }
|
|
2070
|
+
.pill-severity--low { background: var(--mfb-surface); color: var(--mfb-text-muted); }
|
|
2071
|
+
|
|
2072
|
+
.report-detail { display: flex; flex-direction: column; gap: 12px; }
|
|
2073
|
+
.report-detail-header {
|
|
2074
|
+
display: flex;
|
|
2075
|
+
align-items: center;
|
|
2076
|
+
justify-content: space-between;
|
|
2077
|
+
gap: 8px;
|
|
2078
|
+
}
|
|
2079
|
+
.report-detail-body { display: flex; flex-direction: column; gap: 10px; }
|
|
2080
|
+
.report-detail-description {
|
|
2081
|
+
font-size: 14px;
|
|
2082
|
+
white-space: pre-wrap;
|
|
2083
|
+
margin: 0;
|
|
2084
|
+
}
|
|
2085
|
+
.report-detail-screenshot {
|
|
2086
|
+
display: block;
|
|
2087
|
+
border: 1px solid var(--mfb-border);
|
|
2088
|
+
border-radius: var(--mfb-radius);
|
|
2089
|
+
overflow: hidden;
|
|
2090
|
+
background: #1a1a1a;
|
|
2091
|
+
}
|
|
2092
|
+
.report-detail-screenshot img {
|
|
2093
|
+
display: block;
|
|
2094
|
+
width: 100%;
|
|
2095
|
+
max-height: 200px;
|
|
2096
|
+
object-fit: contain;
|
|
2097
|
+
}
|
|
2098
|
+
.report-detail-section {
|
|
2099
|
+
font-size: 12px;
|
|
2100
|
+
text-transform: uppercase;
|
|
2101
|
+
letter-spacing: 0.04em;
|
|
2102
|
+
color: var(--mfb-text-muted);
|
|
2103
|
+
margin: 8px 0 4px;
|
|
2104
|
+
font-weight: 600;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
.report-detail-context {
|
|
2108
|
+
display: flex;
|
|
2109
|
+
flex-direction: column;
|
|
2110
|
+
gap: 6px;
|
|
2111
|
+
padding: 10px 12px;
|
|
2112
|
+
background: var(--mfb-surface);
|
|
2113
|
+
border-radius: var(--mfb-radius);
|
|
2114
|
+
border: 1px solid var(--mfb-border);
|
|
2115
|
+
}
|
|
2116
|
+
.report-detail-context-pills { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
2117
|
+
.report-detail-context-line {
|
|
2118
|
+
display: flex;
|
|
2119
|
+
align-items: baseline;
|
|
2120
|
+
gap: 8px;
|
|
2121
|
+
font-size: 12px;
|
|
2122
|
+
color: var(--mfb-text);
|
|
2123
|
+
}
|
|
2124
|
+
.report-detail-context-label {
|
|
2125
|
+
text-transform: uppercase;
|
|
2126
|
+
letter-spacing: 0.04em;
|
|
2127
|
+
font-size: 10px;
|
|
2128
|
+
font-weight: 600;
|
|
2129
|
+
color: var(--mfb-text-muted);
|
|
2130
|
+
min-width: 56px;
|
|
2131
|
+
}
|
|
2132
|
+
.report-detail-context-url {
|
|
2133
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2134
|
+
color: var(--mfb-accent);
|
|
2135
|
+
text-decoration: none;
|
|
2136
|
+
font-size: 11px;
|
|
2137
|
+
overflow: hidden;
|
|
2138
|
+
text-overflow: ellipsis;
|
|
2139
|
+
white-space: nowrap;
|
|
2140
|
+
}
|
|
2141
|
+
.report-detail-context-url:hover { text-decoration: underline; }
|
|
2142
|
+
.pill-capture { background: var(--mfb-bg); color: var(--mfb-text-muted); border-color: var(--mfb-border); }
|
|
2143
|
+
.report-detail-empty {
|
|
2144
|
+
font-size: 12px;
|
|
2145
|
+
color: var(--mfb-text-muted);
|
|
2146
|
+
margin: 0;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
.report-comments {
|
|
2150
|
+
list-style: none;
|
|
2151
|
+
margin: 0;
|
|
2152
|
+
padding: 0;
|
|
2153
|
+
display: flex;
|
|
2154
|
+
flex-direction: column;
|
|
2155
|
+
gap: 6px;
|
|
2156
|
+
max-height: 300px;
|
|
2157
|
+
overflow-y: auto;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
.comment-bubble {
|
|
2161
|
+
padding: 8px 10px;
|
|
2162
|
+
border-radius: 12px;
|
|
2163
|
+
max-width: 88%;
|
|
2164
|
+
font-size: 13px;
|
|
2165
|
+
line-height: 1.4;
|
|
2166
|
+
}
|
|
2167
|
+
.comment-bubble.is-mine {
|
|
2168
|
+
align-self: flex-end;
|
|
2169
|
+
background: var(--mfb-accent);
|
|
2170
|
+
color: var(--mfb-accent-contrast);
|
|
2171
|
+
}
|
|
2172
|
+
.comment-bubble.is-other {
|
|
2173
|
+
align-self: flex-start;
|
|
2174
|
+
background: var(--mfb-surface);
|
|
2175
|
+
border: 1px solid var(--mfb-border);
|
|
2176
|
+
color: var(--mfb-text);
|
|
2177
|
+
}
|
|
2178
|
+
.comment-author {
|
|
2179
|
+
font-size: 10px;
|
|
2180
|
+
text-transform: uppercase;
|
|
2181
|
+
letter-spacing: 0.04em;
|
|
2182
|
+
font-weight: 600;
|
|
2183
|
+
margin-bottom: 2px;
|
|
2184
|
+
}
|
|
2185
|
+
.comment-author--mcp { color: #6b21a8; }
|
|
2186
|
+
.comment-author--staff { color: #1e40af; }
|
|
2187
|
+
.comment-author--system { color: var(--mfb-text-muted); }
|
|
2188
|
+
.comment-body { white-space: pre-wrap; }
|
|
2189
|
+
.comment-time {
|
|
2190
|
+
font-size: 10px;
|
|
2191
|
+
margin-top: 4px;
|
|
2192
|
+
opacity: 0.7;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
.report-compose {
|
|
2196
|
+
display: flex;
|
|
2197
|
+
flex-direction: column;
|
|
2198
|
+
gap: 6px;
|
|
2199
|
+
margin-top: 4px;
|
|
2200
|
+
}
|
|
2201
|
+
.report-compose textarea {
|
|
2202
|
+
font: inherit;
|
|
2203
|
+
font-size: 13px;
|
|
2204
|
+
padding: 8px 10px;
|
|
2205
|
+
border: 1px solid var(--mfb-border);
|
|
2206
|
+
border-radius: var(--mfb-radius);
|
|
2207
|
+
background: var(--mfb-surface);
|
|
2208
|
+
color: inherit;
|
|
2209
|
+
min-height: 64px;
|
|
2210
|
+
resize: vertical;
|
|
2211
|
+
}
|
|
2212
|
+
.report-compose-actions {
|
|
2213
|
+
display: flex;
|
|
2214
|
+
justify-content: flex-end;
|
|
2215
|
+
gap: 6px;
|
|
2216
|
+
}
|
|
1499
2217
|
`;
|
|
1500
2218
|
|
|
1501
2219
|
// src/widget/mount.tsx
|
|
1502
|
-
import { Fragment, jsx as
|
|
2220
|
+
import { Fragment, jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
|
|
1503
2221
|
function mountWidget(options) {
|
|
1504
2222
|
const shadow = options.host.attachShadow({ mode: "open" });
|
|
1505
2223
|
const style = document.createElement("style");
|
|
@@ -1507,45 +2225,113 @@ function mountWidget(options) {
|
|
|
1507
2225
|
shadow.appendChild(style);
|
|
1508
2226
|
const mountPoint = document.createElement("div");
|
|
1509
2227
|
shadow.appendChild(mountPoint);
|
|
1510
|
-
let currentState = { open: false, status: "idle" };
|
|
2228
|
+
let currentState = { open: false, status: "idle", tab: "send" };
|
|
1511
2229
|
function rerender(state) {
|
|
1512
2230
|
currentState = state;
|
|
1513
2231
|
render(h(Root, { state }), mountPoint);
|
|
1514
2232
|
}
|
|
2233
|
+
function clearSelected(s) {
|
|
2234
|
+
const { selectedReportId: _drop, ...rest } = s;
|
|
2235
|
+
void _drop;
|
|
2236
|
+
return rest;
|
|
2237
|
+
}
|
|
1515
2238
|
function Root({ state }) {
|
|
1516
2239
|
const handleSubmit = useCallback(async (values) => {
|
|
1517
|
-
rerender({
|
|
2240
|
+
rerender({ ...currentState, status: "submitting" });
|
|
1518
2241
|
try {
|
|
1519
2242
|
await options.onSubmit(values);
|
|
1520
|
-
rerender({
|
|
1521
|
-
setTimeout(
|
|
2243
|
+
rerender({ ...currentState, status: "success" });
|
|
2244
|
+
setTimeout(
|
|
2245
|
+
() => rerender({
|
|
2246
|
+
...currentState,
|
|
2247
|
+
open: false,
|
|
2248
|
+
status: "idle",
|
|
2249
|
+
// After a successful submit, jump the user to "My reports" so
|
|
2250
|
+
// they immediately see their just-filed report in the list
|
|
2251
|
+
// (and the conversation that's about to start).
|
|
2252
|
+
tab: options.api ? "mine" : "send"
|
|
2253
|
+
}),
|
|
2254
|
+
1200
|
|
2255
|
+
);
|
|
1522
2256
|
} catch (err) {
|
|
1523
|
-
rerender({
|
|
2257
|
+
rerender({
|
|
2258
|
+
...currentState,
|
|
2259
|
+
status: "error",
|
|
2260
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2261
|
+
});
|
|
1524
2262
|
}
|
|
1525
2263
|
}, []);
|
|
1526
|
-
|
|
1527
|
-
|
|
2264
|
+
const externalId = options.getExternalId?.();
|
|
2265
|
+
const fabVisible = options.showFAB && (options.getExternalId === void 0 ? true : Boolean(externalId));
|
|
2266
|
+
const showMineTab = Boolean(options.api && externalId);
|
|
2267
|
+
return /* @__PURE__ */ jsxs8(Fragment, { children: [
|
|
2268
|
+
fabVisible && /* @__PURE__ */ jsx9(
|
|
1528
2269
|
Fab,
|
|
1529
2270
|
{
|
|
1530
2271
|
label: options.strings["fab.label"],
|
|
1531
2272
|
onClick: () => rerender({ ...currentState, open: true })
|
|
1532
2273
|
}
|
|
1533
2274
|
),
|
|
1534
|
-
state.open && /* @__PURE__ */
|
|
2275
|
+
state.open && /* @__PURE__ */ jsxs8(
|
|
1535
2276
|
Modal,
|
|
1536
2277
|
{
|
|
1537
|
-
onDismiss: () => rerender({ open: false, status: "idle" }),
|
|
2278
|
+
onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
|
|
1538
2279
|
closeLabel: options.strings["form.close"],
|
|
1539
|
-
children:
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
2280
|
+
children: [
|
|
2281
|
+
showMineTab && /* @__PURE__ */ jsxs8("div", { class: "tab-strip", role: "tablist", children: [
|
|
2282
|
+
/* @__PURE__ */ jsx9(
|
|
2283
|
+
"button",
|
|
2284
|
+
{
|
|
2285
|
+
type: "button",
|
|
2286
|
+
role: "tab",
|
|
2287
|
+
"aria-selected": state.tab === "send",
|
|
2288
|
+
class: `tab-button ${state.tab === "send" ? "is-active" : ""}`,
|
|
2289
|
+
onClick: () => rerender(clearSelected({ ...currentState, tab: "send" })),
|
|
2290
|
+
children: options.strings["tab.send"]
|
|
2291
|
+
}
|
|
2292
|
+
),
|
|
2293
|
+
/* @__PURE__ */ jsx9(
|
|
2294
|
+
"button",
|
|
2295
|
+
{
|
|
2296
|
+
type: "button",
|
|
2297
|
+
role: "tab",
|
|
2298
|
+
"aria-selected": state.tab === "mine",
|
|
2299
|
+
class: `tab-button ${state.tab === "mine" ? "is-active" : ""}`,
|
|
2300
|
+
onClick: () => rerender(clearSelected({ ...currentState, tab: "mine" })),
|
|
2301
|
+
children: options.strings["tab.mine"]
|
|
2302
|
+
}
|
|
2303
|
+
)
|
|
2304
|
+
] }),
|
|
2305
|
+
state.tab === "send" && /* @__PURE__ */ jsx9(
|
|
2306
|
+
Form,
|
|
2307
|
+
{
|
|
2308
|
+
strings: options.strings,
|
|
2309
|
+
onSubmit: handleSubmit,
|
|
2310
|
+
onCancel: () => rerender({ ...currentState, open: false, status: "idle" }),
|
|
2311
|
+
status: state.status,
|
|
2312
|
+
...state.error !== void 0 && { errorMessage: state.error }
|
|
2313
|
+
}
|
|
2314
|
+
),
|
|
2315
|
+
state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx9(
|
|
2316
|
+
MineList,
|
|
2317
|
+
{
|
|
2318
|
+
api: options.api,
|
|
2319
|
+
externalId,
|
|
2320
|
+
strings: options.strings,
|
|
2321
|
+
onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
|
|
2322
|
+
}
|
|
2323
|
+
),
|
|
2324
|
+
state.tab === "mine" && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx9(
|
|
2325
|
+
ReportDetailView,
|
|
2326
|
+
{
|
|
2327
|
+
api: options.api,
|
|
2328
|
+
externalId,
|
|
2329
|
+
reportId: state.selectedReportId,
|
|
2330
|
+
strings: options.strings,
|
|
2331
|
+
onBack: () => rerender(clearSelected({ ...currentState }))
|
|
2332
|
+
}
|
|
2333
|
+
)
|
|
2334
|
+
]
|
|
1549
2335
|
}
|
|
1550
2336
|
)
|
|
1551
2337
|
] });
|
|
@@ -1561,6 +2347,9 @@ function mountWidget(options) {
|
|
|
1561
2347
|
dispose() {
|
|
1562
2348
|
render(null, mountPoint);
|
|
1563
2349
|
options.host.innerHTML = "";
|
|
2350
|
+
},
|
|
2351
|
+
notifyIdentityChanged() {
|
|
2352
|
+
rerender({ ...currentState });
|
|
1564
2353
|
}
|
|
1565
2354
|
};
|
|
1566
2355
|
}
|
|
@@ -1615,6 +2404,15 @@ function createFeedback(config) {
|
|
|
1615
2404
|
};
|
|
1616
2405
|
if (screenshot) payload.screenshot = screenshot;
|
|
1617
2406
|
if (values.synthetic) payload.synthetic = true;
|
|
2407
|
+
if (user?.id !== void 0 && user.id !== null && user.id !== "") {
|
|
2408
|
+
payload.user = {
|
|
2409
|
+
// The host can pass `id` as a string or number; the backend
|
|
2410
|
+
// stores it as an opaque string. Coerce here to a stable shape.
|
|
2411
|
+
id: String(user.id),
|
|
2412
|
+
...user.email !== void 0 && { email: user.email },
|
|
2413
|
+
...user.name !== void 0 && { name: user.name }
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
1618
2416
|
let finalPayload = payload;
|
|
1619
2417
|
for (const t of transformers) finalPayload = await t(finalPayload);
|
|
1620
2418
|
try {
|
|
@@ -1634,7 +2432,12 @@ function createFeedback(config) {
|
|
|
1634
2432
|
showFAB: config.showFAB ?? true,
|
|
1635
2433
|
onSubmit: async (values) => {
|
|
1636
2434
|
await buildAndSubmit(values);
|
|
1637
|
-
}
|
|
2435
|
+
},
|
|
2436
|
+
api,
|
|
2437
|
+
// Keep this a callback (not a snapshot) so the mount picks up identity
|
|
2438
|
+
// changes that happen after createFeedback() — `notifyIdentityChanged()`
|
|
2439
|
+
// is the trigger for a re-render.
|
|
2440
|
+
getExternalId: () => user?.id !== void 0 && user.id !== null && user.id !== "" ? String(user.id) : void 0
|
|
1638
2441
|
});
|
|
1639
2442
|
const instance = {
|
|
1640
2443
|
show() {
|
|
@@ -1658,6 +2461,7 @@ function createFeedback(config) {
|
|
|
1658
2461
|
},
|
|
1659
2462
|
identify(u) {
|
|
1660
2463
|
user = u;
|
|
2464
|
+
handle.notifyIdentityChanged();
|
|
1661
2465
|
},
|
|
1662
2466
|
setMetadata(kv) {
|
|
1663
2467
|
metadata = { ...metadata, ...kv };
|
|
@@ -1682,4 +2486,4 @@ function createFeedback(config) {
|
|
|
1682
2486
|
export {
|
|
1683
2487
|
createFeedback
|
|
1684
2488
|
};
|
|
1685
|
-
//# sourceMappingURL=chunk-
|
|
2489
|
+
//# sourceMappingURL=chunk-F3FVKCBE.mjs.map
|