spaps-issue-reporting-react 0.4.0 → 0.4.2
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/CHANGELOG.md +16 -0
- package/README.md +84 -2
- package/dist/index.d.mts +78 -3
- package/dist/index.d.ts +78 -3
- package/dist/index.js +733 -27
- package/dist/index.mjs +731 -28
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
FloatingIssueReportButton: () => FloatingIssueReportButton,
|
|
34
|
+
IssueReportMessageThread: () => IssueReportMessageThread,
|
|
34
35
|
IssueReportingPageConfig: () => IssueReportingPageConfig,
|
|
35
36
|
IssueReportingProvider: () => IssueReportingProvider,
|
|
36
37
|
ReportModeContext: () => ReportModeContext,
|
|
@@ -44,9 +45,13 @@ __export(index_exports, {
|
|
|
44
45
|
getIssueStatusClassName: () => getIssueStatusClassName,
|
|
45
46
|
isClosedIssueStatus: () => isClosedIssueStatus,
|
|
46
47
|
isOpenIssueStatus: () => isOpenIssueStatus,
|
|
48
|
+
isReporterMessageConflict: () => isReporterMessageConflict,
|
|
47
49
|
issueReportingKeys: () => issueReportingKeys,
|
|
50
|
+
selectReporterVisibleMessages: () => selectReporterVisibleMessages,
|
|
48
51
|
useIssueReporting: () => useIssueReporting,
|
|
49
52
|
useIssueReportingHistory: () => useIssueReportingHistory,
|
|
53
|
+
useIssueReportingMessageMutation: () => useIssueReportingMessageMutation,
|
|
54
|
+
useIssueReportingMessages: () => useIssueReportingMessages,
|
|
50
55
|
useIssueReportingMutations: () => useIssueReportingMutations,
|
|
51
56
|
useIssueReportingStatus: () => useIssueReportingStatus,
|
|
52
57
|
useReportMode: () => useReportMode
|
|
@@ -79,6 +84,13 @@ var import_jsx_runtime = require("react/jsx-runtime");
|
|
|
79
84
|
var LIST_LIMIT = 200;
|
|
80
85
|
var NOTE_MIN_LENGTH = 10;
|
|
81
86
|
var NOTE_MAX_LENGTH = 2e3;
|
|
87
|
+
var ATTACHMENT_MAX_COUNT = 5;
|
|
88
|
+
var ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
89
|
+
var ATTACHMENT_ALLOWED_MIMES = /* @__PURE__ */ new Set([
|
|
90
|
+
"image/png",
|
|
91
|
+
"image/jpeg",
|
|
92
|
+
"image/webp"
|
|
93
|
+
]);
|
|
82
94
|
var DEFAULT_INPUT_MODES = ["text"];
|
|
83
95
|
var DEFAULT_VOICE_PROVIDER = "elevenlabs_scribe_realtime";
|
|
84
96
|
var DEFAULT_VOICE_MODEL_ID = "scribe_v2_realtime";
|
|
@@ -151,13 +163,31 @@ var defaultIssueReportingCopy = {
|
|
|
151
163
|
retryAction: "Retry",
|
|
152
164
|
originHumanLabel: "Human",
|
|
153
165
|
originMachineLabel: "Machine",
|
|
154
|
-
machineOriginFallback: "system"
|
|
166
|
+
machineOriginFallback: "system",
|
|
167
|
+
threadTitle: "Conversation",
|
|
168
|
+
threadDescription: "Messages from the support team about this report, and your replies.",
|
|
169
|
+
threadLoading: "Loading conversation...",
|
|
170
|
+
threadLoadFailed: "Failed to load the conversation",
|
|
171
|
+
threadEmpty: "No messages on this report yet.",
|
|
172
|
+
threadNeedsResponseBadge: "Needs your response",
|
|
173
|
+
threadAuthorOperator: "Support",
|
|
174
|
+
threadAuthorReporter: "You",
|
|
175
|
+
threadKindClarificationRequest: "Question",
|
|
176
|
+
threadKindReporterResponse: "Your reply",
|
|
177
|
+
threadKindFinalResponse: "Resolution",
|
|
178
|
+
threadResponsePlaceholder: "Write your response...",
|
|
179
|
+
threadResponseLabel: "Respond to the support team",
|
|
180
|
+
threadResponseSubmitAction: "Send response",
|
|
181
|
+
threadResponseSubmittingAction: "Sending...",
|
|
182
|
+
threadResponseConflict: "That response was already sent, or the message changed. Refresh and try again.",
|
|
183
|
+
threadResponseFailed: "Failed to send your response"
|
|
155
184
|
};
|
|
156
185
|
var issueReportingKeys = {
|
|
157
186
|
all: ["spaps-issue-reporting"],
|
|
158
187
|
status: (scope) => [...issueReportingKeys.all, "status", scope],
|
|
159
188
|
history: (scope) => [...issueReportingKeys.all, "history", scope],
|
|
160
|
-
detail: (issueReportId) => [...issueReportingKeys.all, "detail", issueReportId]
|
|
189
|
+
detail: (issueReportId) => [...issueReportingKeys.all, "detail", issueReportId],
|
|
190
|
+
messages: (issueReportId) => [...issueReportingKeys.all, "messages", issueReportId]
|
|
161
191
|
};
|
|
162
192
|
function resolvePageUrl(getPageUrl) {
|
|
163
193
|
if (getPageUrl) {
|
|
@@ -327,6 +357,27 @@ function getEntryPointClassName(state) {
|
|
|
327
357
|
}
|
|
328
358
|
return "text-slate-500";
|
|
329
359
|
}
|
|
360
|
+
function selectReporterVisibleMessages(messages) {
|
|
361
|
+
return messages.filter(
|
|
362
|
+
(message) => message.reporter_visible && message.state === "active"
|
|
363
|
+
).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
364
|
+
}
|
|
365
|
+
function isReporterMessageConflict(error) {
|
|
366
|
+
if (!error || typeof error !== "object") {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
const record = error;
|
|
370
|
+
const code = record.code ?? record.error?.code;
|
|
371
|
+
if (typeof code === "string" && code === "ISSUE_REPORT_MESSAGE_CONFLICT") {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
const status = record.status ?? record.statusCode ?? record.status_code ?? void 0;
|
|
375
|
+
if (status === 409) {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
const message = error instanceof Error ? error.message : typeof record.message === "string" ? record.message : "";
|
|
379
|
+
return /ISSUE_REPORT_MESSAGE_CONFLICT/i.test(message) || /\b409\b/.test(message);
|
|
380
|
+
}
|
|
330
381
|
function getIssueNoteLengthMessage(note, copy) {
|
|
331
382
|
if (note.length < NOTE_MIN_LENGTH) {
|
|
332
383
|
return `${NOTE_MIN_LENGTH - note.length} ${copy.noteMinimumSuffix}`;
|
|
@@ -421,18 +472,26 @@ function useIssueReportingMutations() {
|
|
|
421
472
|
const updateMutation = (0, import_react_query.useMutation)({
|
|
422
473
|
mutationFn: ({
|
|
423
474
|
issueReportId,
|
|
424
|
-
note
|
|
425
|
-
|
|
475
|
+
note,
|
|
476
|
+
add_attachment_ids,
|
|
477
|
+
remove_attachment_ids
|
|
478
|
+
}) => client.issueReporting.update(issueReportId, {
|
|
479
|
+
note,
|
|
480
|
+
add_attachment_ids,
|
|
481
|
+
remove_attachment_ids
|
|
482
|
+
}),
|
|
426
483
|
onSuccess: invalidateAll
|
|
427
484
|
});
|
|
428
485
|
const replyMutation = (0, import_react_query.useMutation)({
|
|
429
486
|
mutationFn: ({
|
|
430
487
|
issueReportId,
|
|
431
488
|
note,
|
|
432
|
-
reporterRoleHint
|
|
489
|
+
reporterRoleHint,
|
|
490
|
+
attachment_ids
|
|
433
491
|
}) => client.issueReporting.reply(issueReportId, {
|
|
434
492
|
note,
|
|
435
|
-
reporter_role_hint: reporterRoleHint
|
|
493
|
+
reporter_role_hint: reporterRoleHint,
|
|
494
|
+
attachment_ids
|
|
436
495
|
}),
|
|
437
496
|
onSuccess: invalidateAll
|
|
438
497
|
});
|
|
@@ -442,6 +501,39 @@ function useIssueReportingMutations() {
|
|
|
442
501
|
replyMutation
|
|
443
502
|
};
|
|
444
503
|
}
|
|
504
|
+
function useIssueReportingMessages(issueReportId) {
|
|
505
|
+
const { client, isEligible } = useIssueReporting();
|
|
506
|
+
const listMessages = client.issueReporting.listMessages;
|
|
507
|
+
return (0, import_react_query.useQuery)({
|
|
508
|
+
queryKey: issueReportingKeys.messages(issueReportId ?? "none"),
|
|
509
|
+
queryFn: () => listMessages(issueReportId),
|
|
510
|
+
enabled: isEligible && Boolean(issueReportId) && Boolean(listMessages),
|
|
511
|
+
retry: false
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function useIssueReportingMessageMutation(issueReportId) {
|
|
515
|
+
const queryClient = (0, import_react_query.useQueryClient)();
|
|
516
|
+
const { client } = useIssueReporting();
|
|
517
|
+
return (0, import_react_query.useMutation)({
|
|
518
|
+
mutationFn: (payload) => {
|
|
519
|
+
const submit = client.issueReporting.submitMessage;
|
|
520
|
+
if (!submit || !issueReportId) {
|
|
521
|
+
return Promise.reject(
|
|
522
|
+
new Error("This client does not support submitting messages.")
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
return submit(issueReportId, payload);
|
|
526
|
+
},
|
|
527
|
+
onSuccess: async () => {
|
|
528
|
+
if (!issueReportId) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
await queryClient.invalidateQueries({
|
|
532
|
+
queryKey: issueReportingKeys.messages(issueReportId)
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
}
|
|
445
537
|
function IssueReportingProvider({
|
|
446
538
|
client,
|
|
447
539
|
isEligible,
|
|
@@ -471,6 +563,7 @@ function IssueReportingProvider({
|
|
|
471
563
|
const [scope, setScope] = (0, import_react2.useState)(
|
|
472
564
|
() => resolveInitialScope(defaultScope, allowTenantScope)
|
|
473
565
|
);
|
|
566
|
+
const [needsResponseMap, setNeedsResponseMap] = (0, import_react2.useState)({});
|
|
474
567
|
const [pageConfigs, setPageConfigs] = (0, import_react2.useState)([]);
|
|
475
568
|
const [registeredTargets, setRegisteredTargets] = (0, import_react2.useState)(
|
|
476
569
|
[]
|
|
@@ -495,6 +588,21 @@ function IssueReportingProvider({
|
|
|
495
588
|
(0, import_react2.useEffect)(() => {
|
|
496
589
|
setScope(resolveInitialScope(defaultScope, allowTenantScope));
|
|
497
590
|
}, [allowTenantScope, defaultScope]);
|
|
591
|
+
const setNeedsResponse = (0, import_react2.useCallback)(
|
|
592
|
+
(issueReportId, needsResponse) => {
|
|
593
|
+
setNeedsResponseMap((current) => {
|
|
594
|
+
if (Boolean(current[issueReportId]) === needsResponse) {
|
|
595
|
+
return current;
|
|
596
|
+
}
|
|
597
|
+
return { ...current, [issueReportId]: needsResponse };
|
|
598
|
+
});
|
|
599
|
+
},
|
|
600
|
+
[]
|
|
601
|
+
);
|
|
602
|
+
const needsResponseIssueIds = (0, import_react2.useMemo)(
|
|
603
|
+
() => Object.entries(needsResponseMap).filter(([, needs]) => needs).map(([id]) => id),
|
|
604
|
+
[needsResponseMap]
|
|
605
|
+
);
|
|
498
606
|
const closePopover = (0, import_react2.useCallback)(() => {
|
|
499
607
|
setIsPopoverOpen(false);
|
|
500
608
|
}, []);
|
|
@@ -682,7 +790,9 @@ function IssueReportingProvider({
|
|
|
682
790
|
createMode,
|
|
683
791
|
inputModes: resolvedInputModes,
|
|
684
792
|
defaultInputMode: resolvedDefaultInputMode,
|
|
685
|
-
voice: resolvedVoiceConfig
|
|
793
|
+
voice: resolvedVoiceConfig,
|
|
794
|
+
needsResponseIssueIds,
|
|
795
|
+
setNeedsResponse
|
|
686
796
|
}),
|
|
687
797
|
[
|
|
688
798
|
allowTenantScope,
|
|
@@ -698,6 +808,7 @@ function IssueReportingProvider({
|
|
|
698
808
|
isReportMode,
|
|
699
809
|
mergedCopy,
|
|
700
810
|
modalState,
|
|
811
|
+
needsResponseIssueIds,
|
|
701
812
|
openExistingIssueModal,
|
|
702
813
|
openPageIssueModal,
|
|
703
814
|
openPopover,
|
|
@@ -709,6 +820,7 @@ function IssueReportingProvider({
|
|
|
709
820
|
resolvedVoiceConfig,
|
|
710
821
|
scope,
|
|
711
822
|
selectPanel,
|
|
823
|
+
setNeedsResponse,
|
|
712
824
|
startNewIssue
|
|
713
825
|
]
|
|
714
826
|
);
|
|
@@ -988,6 +1100,7 @@ function IssueReportModalBody({
|
|
|
988
1100
|
error,
|
|
989
1101
|
canUseVoice,
|
|
990
1102
|
canUseText,
|
|
1103
|
+
canUseAttachments,
|
|
991
1104
|
effectiveInputMode,
|
|
992
1105
|
note,
|
|
993
1106
|
normalizedNote,
|
|
@@ -1001,6 +1114,11 @@ function IssueReportModalBody({
|
|
|
1001
1114
|
voiceError,
|
|
1002
1115
|
scribeError,
|
|
1003
1116
|
submitError,
|
|
1117
|
+
existingAttachments,
|
|
1118
|
+
removedExistingIds,
|
|
1119
|
+
pendingFiles,
|
|
1120
|
+
uploadProgress,
|
|
1121
|
+
attachmentValidationErrors,
|
|
1004
1122
|
onRetryHydration,
|
|
1005
1123
|
onClose,
|
|
1006
1124
|
onSelectText,
|
|
@@ -1009,7 +1127,10 @@ function IssueReportModalBody({
|
|
|
1009
1127
|
onStopVoiceInput,
|
|
1010
1128
|
onAppendTranscript,
|
|
1011
1129
|
onNoteChange,
|
|
1012
|
-
onSubmit
|
|
1130
|
+
onSubmit,
|
|
1131
|
+
onAddFiles,
|
|
1132
|
+
onRemoveExistingAttachment,
|
|
1133
|
+
onRemovePendingAttachment
|
|
1013
1134
|
}) {
|
|
1014
1135
|
if (isHydrating) {
|
|
1015
1136
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-5 flex items-center gap-2 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-slate-600", children: [
|
|
@@ -1079,6 +1200,20 @@ function IssueReportModalBody({
|
|
|
1079
1200
|
onSubmit
|
|
1080
1201
|
}
|
|
1081
1202
|
),
|
|
1203
|
+
canUseAttachments && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1204
|
+
AttachmentPicker,
|
|
1205
|
+
{
|
|
1206
|
+
existingAttachments,
|
|
1207
|
+
removedExistingIds,
|
|
1208
|
+
pendingFiles,
|
|
1209
|
+
uploadProgress,
|
|
1210
|
+
validationErrors: attachmentValidationErrors,
|
|
1211
|
+
disabled: isSubmitting,
|
|
1212
|
+
onAddFiles,
|
|
1213
|
+
onRemoveExisting: onRemoveExistingAttachment,
|
|
1214
|
+
onRemovePending: onRemovePendingAttachment
|
|
1215
|
+
}
|
|
1216
|
+
),
|
|
1082
1217
|
submitError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-4 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700", children: submitError }) : null,
|
|
1083
1218
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-6 flex justify-end gap-3", children: [
|
|
1084
1219
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
@@ -1104,7 +1239,8 @@ function IssueReportModalBody({
|
|
|
1104
1239
|
children: isSubmitting ? copy.submittingAction : copy.submitAction
|
|
1105
1240
|
}
|
|
1106
1241
|
)
|
|
1107
|
-
] })
|
|
1242
|
+
] }),
|
|
1243
|
+
mode !== "create" && issue ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportMessageThread, { issueReportId: issue.id }) : null
|
|
1108
1244
|
] });
|
|
1109
1245
|
}
|
|
1110
1246
|
function useIssueReportVoiceCapture({
|
|
@@ -1202,6 +1338,493 @@ function useIssueReportVoiceCapture({
|
|
|
1202
1338
|
appendTranscript
|
|
1203
1339
|
};
|
|
1204
1340
|
}
|
|
1341
|
+
var INITIAL_UPLOAD_PROGRESS = {
|
|
1342
|
+
phase: "idle",
|
|
1343
|
+
uploaded: 0,
|
|
1344
|
+
total: 0
|
|
1345
|
+
};
|
|
1346
|
+
var pendingFileCounter = 0;
|
|
1347
|
+
function nextPendingId() {
|
|
1348
|
+
pendingFileCounter += 1;
|
|
1349
|
+
return `pending-${pendingFileCounter}`;
|
|
1350
|
+
}
|
|
1351
|
+
function formatFileSize(bytes) {
|
|
1352
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1353
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1354
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1355
|
+
}
|
|
1356
|
+
function validateAttachmentFile(file) {
|
|
1357
|
+
if (!ATTACHMENT_ALLOWED_MIMES.has(file.type)) {
|
|
1358
|
+
return `"${file.name}" is not a supported image type (PNG, JPEG, or WebP).`;
|
|
1359
|
+
}
|
|
1360
|
+
if (file.size > ATTACHMENT_MAX_BYTES) {
|
|
1361
|
+
return `"${file.name}" exceeds the 10 MB limit.`;
|
|
1362
|
+
}
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
function AttachmentPicker({
|
|
1366
|
+
existingAttachments,
|
|
1367
|
+
removedExistingIds,
|
|
1368
|
+
pendingFiles,
|
|
1369
|
+
uploadProgress,
|
|
1370
|
+
validationErrors,
|
|
1371
|
+
disabled,
|
|
1372
|
+
onAddFiles,
|
|
1373
|
+
onRemoveExisting,
|
|
1374
|
+
onRemovePending
|
|
1375
|
+
}) {
|
|
1376
|
+
const fileInputRef = (0, import_react5.useRef)(null);
|
|
1377
|
+
const retainedExisting = existingAttachments.filter(
|
|
1378
|
+
(a) => !removedExistingIds.has(a.id)
|
|
1379
|
+
);
|
|
1380
|
+
const totalCount = retainedExisting.length + pendingFiles.length;
|
|
1381
|
+
const canAdd = totalCount < ATTACHMENT_MAX_COUNT && !disabled;
|
|
1382
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-4 space-y-3", children: [
|
|
1383
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between", children: [
|
|
1384
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-slate-500", children: [
|
|
1385
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Image, { className: "h-3.5 w-3.5" }),
|
|
1386
|
+
"Screenshots (",
|
|
1387
|
+
totalCount,
|
|
1388
|
+
"/",
|
|
1389
|
+
ATTACHMENT_MAX_COUNT,
|
|
1390
|
+
")"
|
|
1391
|
+
] }),
|
|
1392
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1393
|
+
"input",
|
|
1394
|
+
{
|
|
1395
|
+
ref: fileInputRef,
|
|
1396
|
+
type: "file",
|
|
1397
|
+
accept: "image/png,image/jpeg,image/webp",
|
|
1398
|
+
multiple: true,
|
|
1399
|
+
className: "hidden",
|
|
1400
|
+
onChange: (e) => {
|
|
1401
|
+
if (e.target.files && e.target.files.length > 0) {
|
|
1402
|
+
onAddFiles(e.target.files);
|
|
1403
|
+
}
|
|
1404
|
+
e.target.value = "";
|
|
1405
|
+
},
|
|
1406
|
+
disabled: !canAdd,
|
|
1407
|
+
"aria-label": "Select screenshot files"
|
|
1408
|
+
}
|
|
1409
|
+
),
|
|
1410
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1411
|
+
"button",
|
|
1412
|
+
{
|
|
1413
|
+
type: "button",
|
|
1414
|
+
className: cn(
|
|
1415
|
+
"rounded-full border px-3 py-1 text-xs font-medium transition",
|
|
1416
|
+
canAdd ? "border-slate-300 text-slate-700 hover:bg-slate-50" : "cursor-not-allowed border-slate-200 text-slate-400"
|
|
1417
|
+
),
|
|
1418
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1419
|
+
disabled: !canAdd,
|
|
1420
|
+
"aria-label": "Add screenshots",
|
|
1421
|
+
children: "Add"
|
|
1422
|
+
}
|
|
1423
|
+
)
|
|
1424
|
+
] }),
|
|
1425
|
+
validationErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "space-y-1", children: validationErrors.map((err, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1426
|
+
"div",
|
|
1427
|
+
{
|
|
1428
|
+
className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
|
|
1429
|
+
role: "alert",
|
|
1430
|
+
children: err
|
|
1431
|
+
},
|
|
1432
|
+
i
|
|
1433
|
+
)) }),
|
|
1434
|
+
uploadProgress.phase === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600", children: [
|
|
1435
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Spinner, { className: "h-3.5 w-3.5 animate-spin" }),
|
|
1436
|
+
"Uploading ",
|
|
1437
|
+
uploadProgress.uploaded,
|
|
1438
|
+
" of ",
|
|
1439
|
+
uploadProgress.total,
|
|
1440
|
+
"..."
|
|
1441
|
+
] }),
|
|
1442
|
+
uploadProgress.phase === "error" && uploadProgress.error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1443
|
+
"div",
|
|
1444
|
+
{
|
|
1445
|
+
className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
|
|
1446
|
+
role: "alert",
|
|
1447
|
+
children: uploadProgress.error
|
|
1448
|
+
}
|
|
1449
|
+
),
|
|
1450
|
+
(retainedExisting.length > 0 || pendingFiles.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-wrap gap-2", children: [
|
|
1451
|
+
retainedExisting.map((att) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1452
|
+
"div",
|
|
1453
|
+
{
|
|
1454
|
+
className: "group relative flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2",
|
|
1455
|
+
children: [
|
|
1456
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Image, { className: "h-4 w-4 flex-shrink-0 text-slate-400" }),
|
|
1457
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "min-w-0", children: [
|
|
1458
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "max-w-[140px] truncate text-xs font-medium text-slate-700", children: att.original_filename }),
|
|
1459
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-xs text-slate-400", children: formatFileSize(att.byte_size) })
|
|
1460
|
+
] }),
|
|
1461
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1462
|
+
"button",
|
|
1463
|
+
{
|
|
1464
|
+
type: "button",
|
|
1465
|
+
className: "ml-1 rounded-full p-0.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700",
|
|
1466
|
+
onClick: () => onRemoveExisting(att.id),
|
|
1467
|
+
disabled,
|
|
1468
|
+
"aria-label": `Remove ${att.original_filename}`,
|
|
1469
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Trash, { className: "h-3.5 w-3.5" })
|
|
1470
|
+
}
|
|
1471
|
+
)
|
|
1472
|
+
]
|
|
1473
|
+
},
|
|
1474
|
+
att.id
|
|
1475
|
+
)),
|
|
1476
|
+
pendingFiles.map((pf) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1477
|
+
"div",
|
|
1478
|
+
{
|
|
1479
|
+
className: "group relative flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2",
|
|
1480
|
+
children: [
|
|
1481
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1482
|
+
"img",
|
|
1483
|
+
{
|
|
1484
|
+
src: pf.previewUrl,
|
|
1485
|
+
alt: pf.file.name,
|
|
1486
|
+
className: "h-8 w-8 flex-shrink-0 rounded object-cover"
|
|
1487
|
+
}
|
|
1488
|
+
),
|
|
1489
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "min-w-0", children: [
|
|
1490
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "max-w-[140px] truncate text-xs font-medium text-slate-700", children: pf.file.name }),
|
|
1491
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-xs text-slate-400", children: formatFileSize(pf.file.size) })
|
|
1492
|
+
] }),
|
|
1493
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1494
|
+
"button",
|
|
1495
|
+
{
|
|
1496
|
+
type: "button",
|
|
1497
|
+
className: "ml-1 rounded-full p-0.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700",
|
|
1498
|
+
onClick: () => onRemovePending(pf.clientId),
|
|
1499
|
+
disabled,
|
|
1500
|
+
"aria-label": `Remove ${pf.file.name}`,
|
|
1501
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Trash, { className: "h-3.5 w-3.5" })
|
|
1502
|
+
}
|
|
1503
|
+
)
|
|
1504
|
+
]
|
|
1505
|
+
},
|
|
1506
|
+
pf.clientId
|
|
1507
|
+
))
|
|
1508
|
+
] })
|
|
1509
|
+
] });
|
|
1510
|
+
}
|
|
1511
|
+
function useAttachmentState(existingAttachments) {
|
|
1512
|
+
const [pendingFiles, setPendingFiles] = (0, import_react5.useState)([]);
|
|
1513
|
+
const [removedExistingIds, setRemovedExistingIds] = (0, import_react5.useState)(
|
|
1514
|
+
/* @__PURE__ */ new Set()
|
|
1515
|
+
);
|
|
1516
|
+
const [validationErrors, setValidationErrors] = (0, import_react5.useState)([]);
|
|
1517
|
+
const [uploadProgress, setUploadProgress] = (0, import_react5.useState)(INITIAL_UPLOAD_PROGRESS);
|
|
1518
|
+
const retainedExistingCount = existingAttachments.filter(
|
|
1519
|
+
(a) => !removedExistingIds.has(a.id)
|
|
1520
|
+
).length;
|
|
1521
|
+
const reset = (0, import_react5.useCallback)(() => {
|
|
1522
|
+
for (const pf of pendingFiles) {
|
|
1523
|
+
URL.revokeObjectURL(pf.previewUrl);
|
|
1524
|
+
}
|
|
1525
|
+
setPendingFiles([]);
|
|
1526
|
+
setRemovedExistingIds(/* @__PURE__ */ new Set());
|
|
1527
|
+
setValidationErrors([]);
|
|
1528
|
+
setUploadProgress(INITIAL_UPLOAD_PROGRESS);
|
|
1529
|
+
}, [pendingFiles]);
|
|
1530
|
+
const addFiles = (0, import_react5.useCallback)(
|
|
1531
|
+
(fileList) => {
|
|
1532
|
+
const currentTotal = retainedExistingCount + pendingFiles.length;
|
|
1533
|
+
const errors = [];
|
|
1534
|
+
const accepted = [];
|
|
1535
|
+
let count = currentTotal;
|
|
1536
|
+
for (let i = 0; i < fileList.length; i++) {
|
|
1537
|
+
const file = fileList[i];
|
|
1538
|
+
if (count >= ATTACHMENT_MAX_COUNT) {
|
|
1539
|
+
errors.push(
|
|
1540
|
+
`"${file.name}" was not added; maximum ${ATTACHMENT_MAX_COUNT} screenshots reached.`
|
|
1541
|
+
);
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
const err = validateAttachmentFile(file);
|
|
1545
|
+
if (err) {
|
|
1546
|
+
errors.push(err);
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
accepted.push({
|
|
1550
|
+
clientId: nextPendingId(),
|
|
1551
|
+
file,
|
|
1552
|
+
previewUrl: URL.createObjectURL(file)
|
|
1553
|
+
});
|
|
1554
|
+
count += 1;
|
|
1555
|
+
}
|
|
1556
|
+
if (accepted.length > 0) {
|
|
1557
|
+
setPendingFiles((prev) => [...prev, ...accepted]);
|
|
1558
|
+
}
|
|
1559
|
+
setValidationErrors(errors);
|
|
1560
|
+
setUploadProgress(INITIAL_UPLOAD_PROGRESS);
|
|
1561
|
+
},
|
|
1562
|
+
[pendingFiles.length, retainedExistingCount]
|
|
1563
|
+
);
|
|
1564
|
+
const removeExisting = (0, import_react5.useCallback)((id) => {
|
|
1565
|
+
setRemovedExistingIds((prev) => /* @__PURE__ */ new Set([...prev, id]));
|
|
1566
|
+
}, []);
|
|
1567
|
+
const removePending = (0, import_react5.useCallback)((clientId) => {
|
|
1568
|
+
setPendingFiles((prev) => {
|
|
1569
|
+
const removed = prev.find((pf) => pf.clientId === clientId);
|
|
1570
|
+
if (removed) {
|
|
1571
|
+
URL.revokeObjectURL(removed.previewUrl);
|
|
1572
|
+
}
|
|
1573
|
+
return prev.filter((pf) => pf.clientId !== clientId);
|
|
1574
|
+
});
|
|
1575
|
+
setUploadProgress(INITIAL_UPLOAD_PROGRESS);
|
|
1576
|
+
}, []);
|
|
1577
|
+
const markPendingUploaded = (0, import_react5.useCallback)((clientId, attachmentId) => {
|
|
1578
|
+
setPendingFiles(
|
|
1579
|
+
(prev) => prev.map(
|
|
1580
|
+
(pf) => pf.clientId === clientId ? { ...pf, uploadedAttachmentId: attachmentId } : pf
|
|
1581
|
+
)
|
|
1582
|
+
);
|
|
1583
|
+
}, []);
|
|
1584
|
+
return {
|
|
1585
|
+
pendingFiles,
|
|
1586
|
+
removedExistingIds,
|
|
1587
|
+
validationErrors,
|
|
1588
|
+
uploadProgress,
|
|
1589
|
+
setUploadProgress,
|
|
1590
|
+
addFiles,
|
|
1591
|
+
removeExisting,
|
|
1592
|
+
removePending,
|
|
1593
|
+
markPendingUploaded,
|
|
1594
|
+
reset
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
var REPORTER_MESSAGE_MIN_LENGTH = 1;
|
|
1598
|
+
var REPORTER_MESSAGE_MAX_LENGTH = 2e3;
|
|
1599
|
+
function generateIdempotencyKey() {
|
|
1600
|
+
const cryptoObj = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
|
|
1601
|
+
if (cryptoObj && typeof cryptoObj.randomUUID === "function") {
|
|
1602
|
+
return `reporter-msg-${cryptoObj.randomUUID()}`;
|
|
1603
|
+
}
|
|
1604
|
+
return `reporter-msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1605
|
+
}
|
|
1606
|
+
function getMessageKindLabel(kind, copy) {
|
|
1607
|
+
switch (kind) {
|
|
1608
|
+
case "clarification_request":
|
|
1609
|
+
return copy.threadKindClarificationRequest;
|
|
1610
|
+
case "reporter_response":
|
|
1611
|
+
return copy.threadKindReporterResponse;
|
|
1612
|
+
case "final_response":
|
|
1613
|
+
return copy.threadKindFinalResponse;
|
|
1614
|
+
default:
|
|
1615
|
+
return kind;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function getMessageKindClassName(kind) {
|
|
1619
|
+
switch (kind) {
|
|
1620
|
+
case "clarification_request":
|
|
1621
|
+
return "bg-amber-100 text-amber-700";
|
|
1622
|
+
case "final_response":
|
|
1623
|
+
return "bg-emerald-100 text-emerald-700";
|
|
1624
|
+
default:
|
|
1625
|
+
return "bg-slate-100 text-slate-600";
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
function MessageBubble({
|
|
1629
|
+
message,
|
|
1630
|
+
copy
|
|
1631
|
+
}) {
|
|
1632
|
+
const isReporter = message.actor.author_type === "reporter";
|
|
1633
|
+
const isFinal = message.kind === "final_response";
|
|
1634
|
+
const authorLabel = isReporter ? copy.threadAuthorReporter : copy.threadAuthorOperator;
|
|
1635
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1636
|
+
"li",
|
|
1637
|
+
{
|
|
1638
|
+
className: cn(
|
|
1639
|
+
"flex flex-col gap-1",
|
|
1640
|
+
isReporter ? "items-end" : "items-start"
|
|
1641
|
+
),
|
|
1642
|
+
children: [
|
|
1643
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
1644
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1645
|
+
"span",
|
|
1646
|
+
{
|
|
1647
|
+
className: cn(
|
|
1648
|
+
"rounded-full px-2 py-0.5 font-medium",
|
|
1649
|
+
BADGE_TEXT,
|
|
1650
|
+
getMessageKindClassName(message.kind)
|
|
1651
|
+
),
|
|
1652
|
+
children: getMessageKindLabel(message.kind, copy)
|
|
1653
|
+
}
|
|
1654
|
+
),
|
|
1655
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: cn("font-medium text-slate-600", LABEL_TEXT), children: authorLabel }),
|
|
1656
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1657
|
+
"time",
|
|
1658
|
+
{
|
|
1659
|
+
className: "text-xs text-slate-400",
|
|
1660
|
+
dateTime: message.created_at,
|
|
1661
|
+
children: formatRelativeTime(message.created_at)
|
|
1662
|
+
}
|
|
1663
|
+
)
|
|
1664
|
+
] }),
|
|
1665
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1666
|
+
"div",
|
|
1667
|
+
{
|
|
1668
|
+
className: cn(
|
|
1669
|
+
"max-w-[85%] whitespace-pre-wrap rounded-2xl border px-3 py-2 text-sm",
|
|
1670
|
+
isReporter ? "border-slate-900 bg-slate-900 text-white" : isFinal ? "border-emerald-200 bg-emerald-50 text-emerald-950" : "border-slate-200 bg-white text-slate-800"
|
|
1671
|
+
),
|
|
1672
|
+
children: message.body
|
|
1673
|
+
}
|
|
1674
|
+
)
|
|
1675
|
+
]
|
|
1676
|
+
}
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
function ReporterResponseComposer({
|
|
1680
|
+
issueReportId,
|
|
1681
|
+
copy
|
|
1682
|
+
}) {
|
|
1683
|
+
const mutation = useIssueReportingMessageMutation(issueReportId);
|
|
1684
|
+
const idempotencyKeyRef = (0, import_react5.useRef)(generateIdempotencyKey());
|
|
1685
|
+
const [body, setBody] = (0, import_react5.useState)("");
|
|
1686
|
+
const [submitError, setSubmitError] = (0, import_react5.useState)(null);
|
|
1687
|
+
const normalized = body.trim();
|
|
1688
|
+
const isValid = normalized.length >= REPORTER_MESSAGE_MIN_LENGTH && normalized.length <= REPORTER_MESSAGE_MAX_LENGTH;
|
|
1689
|
+
const isSubmitting = mutation.isPending;
|
|
1690
|
+
const handleSubmit = async () => {
|
|
1691
|
+
if (!isValid || isSubmitting) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
setSubmitError(null);
|
|
1695
|
+
try {
|
|
1696
|
+
await mutation.mutateAsync({
|
|
1697
|
+
body: normalized,
|
|
1698
|
+
idempotency_key: idempotencyKeyRef.current
|
|
1699
|
+
});
|
|
1700
|
+
setBody("");
|
|
1701
|
+
idempotencyKeyRef.current = generateIdempotencyKey();
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
if (isReporterMessageConflict(error)) {
|
|
1704
|
+
setSubmitError(copy.threadResponseConflict);
|
|
1705
|
+
idempotencyKeyRef.current = generateIdempotencyKey();
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
setSubmitError(resolveErrorMessage(error, copy.threadResponseFailed));
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-3 space-y-2", children: [
|
|
1712
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1713
|
+
"label",
|
|
1714
|
+
{
|
|
1715
|
+
htmlFor: "issue-report-thread-response",
|
|
1716
|
+
className: cn(
|
|
1717
|
+
"block font-medium uppercase tracking-wide text-slate-500",
|
|
1718
|
+
LABEL_TEXT
|
|
1719
|
+
),
|
|
1720
|
+
children: copy.threadResponseLabel
|
|
1721
|
+
}
|
|
1722
|
+
),
|
|
1723
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1724
|
+
"textarea",
|
|
1725
|
+
{
|
|
1726
|
+
id: "issue-report-thread-response",
|
|
1727
|
+
value: body,
|
|
1728
|
+
onChange: (event) => setBody(event.target.value),
|
|
1729
|
+
onKeyDown: (event) => {
|
|
1730
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
1731
|
+
event.preventDefault();
|
|
1732
|
+
void handleSubmit();
|
|
1733
|
+
}
|
|
1734
|
+
},
|
|
1735
|
+
placeholder: copy.threadResponsePlaceholder,
|
|
1736
|
+
className: "h-24 w-full resize-none rounded-2xl border border-slate-300 px-3 py-2 text-sm text-slate-800 outline-none transition focus:border-slate-500 focus:ring-2 focus:ring-slate-200",
|
|
1737
|
+
disabled: isSubmitting
|
|
1738
|
+
}
|
|
1739
|
+
),
|
|
1740
|
+
submitError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1741
|
+
"div",
|
|
1742
|
+
{
|
|
1743
|
+
className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
|
|
1744
|
+
role: "alert",
|
|
1745
|
+
children: submitError
|
|
1746
|
+
}
|
|
1747
|
+
) : null,
|
|
1748
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1749
|
+
"button",
|
|
1750
|
+
{
|
|
1751
|
+
type: "button",
|
|
1752
|
+
className: cn(
|
|
1753
|
+
"rounded-full px-4 py-2 text-sm font-semibold text-white transition",
|
|
1754
|
+
isValid && !isSubmitting ? "bg-slate-900 hover:bg-slate-800" : "cursor-not-allowed bg-slate-300"
|
|
1755
|
+
),
|
|
1756
|
+
onClick: () => void handleSubmit(),
|
|
1757
|
+
disabled: !isValid || isSubmitting,
|
|
1758
|
+
children: isSubmitting ? copy.threadResponseSubmittingAction : copy.threadResponseSubmitAction
|
|
1759
|
+
}
|
|
1760
|
+
) })
|
|
1761
|
+
] });
|
|
1762
|
+
}
|
|
1763
|
+
function IssueReportMessageThread({
|
|
1764
|
+
issueReportId
|
|
1765
|
+
}) {
|
|
1766
|
+
const { copy, client, setNeedsResponse } = useIssueReporting();
|
|
1767
|
+
const supportsMessages = Boolean(client.issueReporting.listMessages);
|
|
1768
|
+
const supportsSubmit = Boolean(client.issueReporting.submitMessage);
|
|
1769
|
+
const query = useIssueReportingMessages(issueReportId);
|
|
1770
|
+
const needsResponse = query.data?.needs_response ?? false;
|
|
1771
|
+
(0, import_react5.useEffect)(() => {
|
|
1772
|
+
setNeedsResponse(issueReportId, needsResponse);
|
|
1773
|
+
return () => {
|
|
1774
|
+
setNeedsResponse(issueReportId, false);
|
|
1775
|
+
};
|
|
1776
|
+
}, [issueReportId, needsResponse, setNeedsResponse]);
|
|
1777
|
+
if (!supportsMessages) {
|
|
1778
|
+
return null;
|
|
1779
|
+
}
|
|
1780
|
+
const visibleMessages = selectReporterVisibleMessages(query.data?.items ?? []);
|
|
1781
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1782
|
+
"section",
|
|
1783
|
+
{
|
|
1784
|
+
"aria-label": copy.threadTitle,
|
|
1785
|
+
className: "mt-6 border-t border-slate-100 pt-5",
|
|
1786
|
+
children: [
|
|
1787
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between gap-2", children: [
|
|
1788
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "text-sm font-semibold text-slate-900", children: copy.threadTitle }),
|
|
1789
|
+
needsResponse ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1790
|
+
"span",
|
|
1791
|
+
{
|
|
1792
|
+
className: cn(
|
|
1793
|
+
"rounded-full px-2 py-0.5 font-medium",
|
|
1794
|
+
BADGE_TEXT,
|
|
1795
|
+
"bg-amber-100 text-amber-700"
|
|
1796
|
+
),
|
|
1797
|
+
children: copy.threadNeedsResponseBadge
|
|
1798
|
+
}
|
|
1799
|
+
) : null
|
|
1800
|
+
] }),
|
|
1801
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mt-1 text-xs text-slate-500", children: copy.threadDescription }),
|
|
1802
|
+
query.isPending ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-4 flex items-center gap-2 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-slate-600", children: [
|
|
1803
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Spinner, { className: "h-4 w-4 animate-spin" }),
|
|
1804
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: copy.threadLoading })
|
|
1805
|
+
] }) : query.error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-4 space-y-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-4 text-sm text-rose-700", children: [
|
|
1806
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: resolveErrorMessage(query.error, copy.threadLoadFailed) }),
|
|
1807
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1808
|
+
"button",
|
|
1809
|
+
{
|
|
1810
|
+
type: "button",
|
|
1811
|
+
className: "rounded-full border border-rose-300 px-3 py-1 font-medium transition hover:bg-rose-100",
|
|
1812
|
+
onClick: () => void query.refetch(),
|
|
1813
|
+
children: copy.retryAction
|
|
1814
|
+
}
|
|
1815
|
+
)
|
|
1816
|
+
] }) : visibleMessages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-4 rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-6 text-center text-sm text-slate-500", children: copy.threadEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { className: "mt-4 space-y-3", children: visibleMessages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MessageBubble, { message, copy }, message.id)) }),
|
|
1817
|
+
supportsSubmit ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1818
|
+
ReporterResponseComposer,
|
|
1819
|
+
{
|
|
1820
|
+
issueReportId,
|
|
1821
|
+
copy
|
|
1822
|
+
}
|
|
1823
|
+
) : null
|
|
1824
|
+
]
|
|
1825
|
+
}
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1205
1828
|
function IssueReportModeBanner() {
|
|
1206
1829
|
const { copy } = useIssueReporting();
|
|
1207
1830
|
const reportMode = useReportMode();
|
|
@@ -1502,7 +2125,13 @@ function IssueReportModal() {
|
|
|
1502
2125
|
const { isOpen, mode, issue, target, isHydrating, error } = modalState;
|
|
1503
2126
|
const canUseVoice = mode === "create" && inputModes.includes("voice");
|
|
1504
2127
|
const canUseText = mode !== "create" || inputModes.includes("text");
|
|
1505
|
-
const
|
|
2128
|
+
const canUseAttachments = Boolean(client.issueReporting.uploadAttachment);
|
|
2129
|
+
const existingAttachments = (0, import_react5.useMemo)(
|
|
2130
|
+
() => mode === "edit" && issue ? issue.attachments ?? [] : [],
|
|
2131
|
+
[issue, mode]
|
|
2132
|
+
);
|
|
2133
|
+
const attachmentState = useAttachmentState(existingAttachments);
|
|
2134
|
+
const isSubmitting = createMutation.isPending || updateMutation.isPending || replyMutation.isPending || attachmentState.uploadProgress.phase === "uploading";
|
|
1506
2135
|
const {
|
|
1507
2136
|
inputMode,
|
|
1508
2137
|
setInputMode,
|
|
@@ -1526,6 +2155,7 @@ function IssueReportModal() {
|
|
|
1526
2155
|
(0, import_react5.useEffect)(() => {
|
|
1527
2156
|
if (!isOpen) {
|
|
1528
2157
|
resetVoiceCapture();
|
|
2158
|
+
attachmentState.reset();
|
|
1529
2159
|
setNote("");
|
|
1530
2160
|
setSubmitError(null);
|
|
1531
2161
|
return;
|
|
@@ -1536,6 +2166,7 @@ function IssueReportModal() {
|
|
|
1536
2166
|
setNote("");
|
|
1537
2167
|
}
|
|
1538
2168
|
resetVoiceCapture();
|
|
2169
|
+
attachmentState.reset();
|
|
1539
2170
|
setSubmitError(null);
|
|
1540
2171
|
}, [isOpen, issue, mode, resetVoiceCapture]);
|
|
1541
2172
|
const effectiveInputMode = mode !== "create" ? "text" : canUseVoice && inputMode === "voice" ? "voice" : "text";
|
|
@@ -1544,6 +2175,7 @@ function IssueReportModal() {
|
|
|
1544
2175
|
const title = mode === "create" ? `${copy.createTitlePrefix}: ${target?.component_label ?? ""}` : mode === "edit" ? `${copy.editTitlePrefix}: ${target?.component_label ?? ""}` : `${copy.replyTitlePrefix}: ${target?.component_label ?? ""}`;
|
|
1545
2176
|
const handleCloseModal = () => {
|
|
1546
2177
|
resetVoiceCapture();
|
|
2178
|
+
attachmentState.reset();
|
|
1547
2179
|
closeModal();
|
|
1548
2180
|
};
|
|
1549
2181
|
const handleStartVoiceInput = async () => {
|
|
@@ -1555,6 +2187,38 @@ function IssueReportModal() {
|
|
|
1555
2187
|
const handleAppendTranscript = () => {
|
|
1556
2188
|
appendTranscript(setNote);
|
|
1557
2189
|
};
|
|
2190
|
+
const uploadPendingFiles = async () => {
|
|
2191
|
+
const upload = client.issueReporting.uploadAttachment;
|
|
2192
|
+
if (!upload || attachmentState.pendingFiles.length === 0) {
|
|
2193
|
+
return [];
|
|
2194
|
+
}
|
|
2195
|
+
const files = attachmentState.pendingFiles;
|
|
2196
|
+
attachmentState.setUploadProgress({
|
|
2197
|
+
phase: "uploading",
|
|
2198
|
+
uploaded: 0,
|
|
2199
|
+
total: files.length
|
|
2200
|
+
});
|
|
2201
|
+
const ids = [];
|
|
2202
|
+
for (let i = 0; i < files.length; i++) {
|
|
2203
|
+
const pf = files[i];
|
|
2204
|
+
const attachmentId = pf.uploadedAttachmentId ?? (await upload(pf.file, { filename: pf.file.name })).id;
|
|
2205
|
+
if (!pf.uploadedAttachmentId) {
|
|
2206
|
+
attachmentState.markPendingUploaded(pf.clientId, attachmentId);
|
|
2207
|
+
}
|
|
2208
|
+
ids.push(attachmentId);
|
|
2209
|
+
attachmentState.setUploadProgress({
|
|
2210
|
+
phase: "uploading",
|
|
2211
|
+
uploaded: i + 1,
|
|
2212
|
+
total: files.length
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
attachmentState.setUploadProgress({
|
|
2216
|
+
phase: "done",
|
|
2217
|
+
uploaded: files.length,
|
|
2218
|
+
total: files.length
|
|
2219
|
+
});
|
|
2220
|
+
return ids;
|
|
2221
|
+
};
|
|
1558
2222
|
const handleSubmit = async () => {
|
|
1559
2223
|
if (!target || !isValid || isSubmitting) {
|
|
1560
2224
|
return;
|
|
@@ -1562,6 +2226,10 @@ function IssueReportModal() {
|
|
|
1562
2226
|
setSubmitError(null);
|
|
1563
2227
|
try {
|
|
1564
2228
|
const noteForSubmit = normalizedNote;
|
|
2229
|
+
let newAttachmentIds = [];
|
|
2230
|
+
if (canUseAttachments && attachmentState.pendingFiles.length > 0) {
|
|
2231
|
+
newAttachmentIds = await uploadPendingFiles();
|
|
2232
|
+
}
|
|
1565
2233
|
const effectiveVoiceMetadata = effectiveInputMode === "voice" ? voiceSubmitMetadata ?? {
|
|
1566
2234
|
provider: voice.provider,
|
|
1567
2235
|
token: "",
|
|
@@ -1586,24 +2254,37 @@ function IssueReportModal() {
|
|
|
1586
2254
|
}
|
|
1587
2255
|
} : target,
|
|
1588
2256
|
note: noteForSubmit,
|
|
1589
|
-
reporter_role_hint: reporterRoleHint
|
|
2257
|
+
reporter_role_hint: reporterRoleHint,
|
|
2258
|
+
attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0
|
|
1590
2259
|
});
|
|
1591
2260
|
} else if (mode === "edit" && issue) {
|
|
2261
|
+
const removedIds = [...attachmentState.removedExistingIds];
|
|
1592
2262
|
await updateMutation.mutateAsync({
|
|
1593
2263
|
issueReportId: issue.id,
|
|
1594
|
-
note: noteForSubmit
|
|
2264
|
+
note: noteForSubmit,
|
|
2265
|
+
add_attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0,
|
|
2266
|
+
remove_attachment_ids: removedIds.length > 0 ? removedIds : void 0
|
|
1595
2267
|
});
|
|
1596
2268
|
} else if (mode === "reply" && issue) {
|
|
1597
2269
|
await replyMutation.mutateAsync({
|
|
1598
2270
|
issueReportId: issue.id,
|
|
1599
2271
|
note: noteForSubmit,
|
|
1600
|
-
reporterRoleHint
|
|
2272
|
+
reporterRoleHint,
|
|
2273
|
+
attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0
|
|
1601
2274
|
});
|
|
1602
2275
|
}
|
|
1603
2276
|
resetVoiceCapture();
|
|
2277
|
+
attachmentState.reset();
|
|
1604
2278
|
closeModal();
|
|
1605
2279
|
} catch (submissionError) {
|
|
1606
|
-
|
|
2280
|
+
const message = resolveErrorMessage(
|
|
2281
|
+
submissionError,
|
|
2282
|
+
"Failed to submit issue report"
|
|
2283
|
+
);
|
|
2284
|
+
attachmentState.setUploadProgress(
|
|
2285
|
+
(current) => current.phase === "uploading" ? { ...current, phase: "error", error: message } : current
|
|
2286
|
+
);
|
|
2287
|
+
setSubmitError(message);
|
|
1607
2288
|
}
|
|
1608
2289
|
};
|
|
1609
2290
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Root, { open: isOpen, onOpenChange: (open) => !open && handleCloseModal(), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Dialog.Portal, { children: [
|
|
@@ -1612,7 +2293,7 @@ function IssueReportModal() {
|
|
|
1612
2293
|
Dialog.Content,
|
|
1613
2294
|
{
|
|
1614
2295
|
className: cn(
|
|
1615
|
-
"fixed left-1/2 top-1/2 max-w-xl -translate-x-1/2 -translate-y-1/2 border border-slate-200 bg-white p-6 focus:outline-none",
|
|
2296
|
+
"fixed left-1/2 top-1/2 max-h-[90vh] max-w-xl -translate-x-1/2 -translate-y-1/2 overflow-y-auto border border-slate-200 bg-white p-6 focus:outline-none",
|
|
1616
2297
|
Z_MODAL_CONTENT,
|
|
1617
2298
|
MODAL_WIDTH,
|
|
1618
2299
|
MODAL_RADIUS,
|
|
@@ -1638,6 +2319,7 @@ function IssueReportModal() {
|
|
|
1638
2319
|
error,
|
|
1639
2320
|
canUseVoice,
|
|
1640
2321
|
canUseText,
|
|
2322
|
+
canUseAttachments,
|
|
1641
2323
|
effectiveInputMode,
|
|
1642
2324
|
note,
|
|
1643
2325
|
normalizedNote,
|
|
@@ -1651,6 +2333,11 @@ function IssueReportModal() {
|
|
|
1651
2333
|
voiceError,
|
|
1652
2334
|
scribeError,
|
|
1653
2335
|
submitError,
|
|
2336
|
+
existingAttachments,
|
|
2337
|
+
removedExistingIds: attachmentState.removedExistingIds,
|
|
2338
|
+
pendingFiles: attachmentState.pendingFiles,
|
|
2339
|
+
uploadProgress: attachmentState.uploadProgress,
|
|
2340
|
+
attachmentValidationErrors: attachmentState.validationErrors,
|
|
1654
2341
|
onRetryHydration: () => void retryModalHydration(),
|
|
1655
2342
|
onClose: handleCloseModal,
|
|
1656
2343
|
onSelectText: () => setInputMode("text"),
|
|
@@ -1659,7 +2346,10 @@ function IssueReportModal() {
|
|
|
1659
2346
|
onStopVoiceInput: handleStopVoiceInput,
|
|
1660
2347
|
onAppendTranscript: handleAppendTranscript,
|
|
1661
2348
|
onNoteChange: setNote,
|
|
1662
|
-
onSubmit: () => void handleSubmit()
|
|
2349
|
+
onSubmit: () => void handleSubmit(),
|
|
2350
|
+
onAddFiles: attachmentState.addFiles,
|
|
2351
|
+
onRemoveExistingAttachment: attachmentState.removeExisting,
|
|
2352
|
+
onRemovePendingAttachment: attachmentState.removePending
|
|
1663
2353
|
}
|
|
1664
2354
|
),
|
|
1665
2355
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Close, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
@@ -1686,33 +2376,44 @@ function FloatingIssueReportButton({
|
|
|
1686
2376
|
isReportMode,
|
|
1687
2377
|
isPopoverOpen,
|
|
1688
2378
|
openPopover,
|
|
1689
|
-
closePopover
|
|
2379
|
+
closePopover,
|
|
2380
|
+
needsResponseIssueIds
|
|
1690
2381
|
} = useIssueReporting();
|
|
1691
2382
|
const status = useIssueReportingStatus();
|
|
1692
2383
|
const entryPointState = getEntryPointState(status.data);
|
|
2384
|
+
const needsResponse = needsResponseIssueIds.length > 0;
|
|
1693
2385
|
if (!isEligible) {
|
|
1694
2386
|
return null;
|
|
1695
2387
|
}
|
|
1696
2388
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
1697
2389
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportModeBanner, {}),
|
|
1698
|
-
!isReportMode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cn("fixed bottom-12 right-4", Z_FLOATING_BUTTON, positionClassName), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportPopover, { children: /* @__PURE__ */ (0, import_jsx_runtime2.
|
|
2390
|
+
!isReportMode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cn("fixed bottom-12 right-4", Z_FLOATING_BUTTON, positionClassName), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportPopover, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1699
2391
|
"button",
|
|
1700
2392
|
{
|
|
1701
2393
|
type: "button",
|
|
1702
|
-
"aria-label": copy.entryAriaLabel,
|
|
2394
|
+
"aria-label": needsResponse ? `${copy.entryAriaLabel} (${copy.threadNeedsResponseBadge})` : copy.entryAriaLabel,
|
|
1703
2395
|
onClick: () => isPopoverOpen ? closePopover() : openPopover(),
|
|
1704
2396
|
className: cn(
|
|
1705
|
-
"flex h-12 w-12 items-center justify-center rounded-full border border-slate-200 bg-white shadow-lg transition hover:-translate-y-0.5 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-slate-300",
|
|
2397
|
+
"relative flex h-12 w-12 items-center justify-center rounded-full border border-slate-200 bg-white shadow-lg transition hover:-translate-y-0.5 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-slate-300",
|
|
1706
2398
|
status.isPending && "animate-pulse",
|
|
1707
2399
|
className
|
|
1708
2400
|
),
|
|
1709
|
-
children:
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2401
|
+
children: [
|
|
2402
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2403
|
+
import_react4.BugBeetle,
|
|
2404
|
+
{
|
|
2405
|
+
className: cn("h-6 w-6", getEntryPointClassName(entryPointState)),
|
|
2406
|
+
weight: "fill"
|
|
2407
|
+
}
|
|
2408
|
+
),
|
|
2409
|
+
needsResponse ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2410
|
+
"span",
|
|
2411
|
+
{
|
|
2412
|
+
"data-testid": "issue-report-needs-response-badge",
|
|
2413
|
+
className: "absolute -right-0.5 -top-0.5 h-3 w-3 rounded-full border-2 border-white bg-amber-500"
|
|
2414
|
+
}
|
|
2415
|
+
) : null
|
|
2416
|
+
]
|
|
1716
2417
|
}
|
|
1717
2418
|
) }) }) : null,
|
|
1718
2419
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportModal, {})
|
|
@@ -1765,6 +2466,7 @@ function ReportableSection({
|
|
|
1765
2466
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1766
2467
|
0 && (module.exports = {
|
|
1767
2468
|
FloatingIssueReportButton,
|
|
2469
|
+
IssueReportMessageThread,
|
|
1768
2470
|
IssueReportingPageConfig,
|
|
1769
2471
|
IssueReportingProvider,
|
|
1770
2472
|
ReportModeContext,
|
|
@@ -1778,9 +2480,13 @@ function ReportableSection({
|
|
|
1778
2480
|
getIssueStatusClassName,
|
|
1779
2481
|
isClosedIssueStatus,
|
|
1780
2482
|
isOpenIssueStatus,
|
|
2483
|
+
isReporterMessageConflict,
|
|
1781
2484
|
issueReportingKeys,
|
|
2485
|
+
selectReporterVisibleMessages,
|
|
1782
2486
|
useIssueReporting,
|
|
1783
2487
|
useIssueReportingHistory,
|
|
2488
|
+
useIssueReportingMessageMutation,
|
|
2489
|
+
useIssueReportingMessages,
|
|
1784
2490
|
useIssueReportingMutations,
|
|
1785
2491
|
useIssueReportingStatus,
|
|
1786
2492
|
useReportMode
|