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.mjs
CHANGED
|
@@ -6,13 +6,15 @@ import {
|
|
|
6
6
|
BugBeetle,
|
|
7
7
|
CheckCircle,
|
|
8
8
|
Circle,
|
|
9
|
+
Image,
|
|
9
10
|
Microphone,
|
|
10
11
|
Spinner,
|
|
11
12
|
TextT,
|
|
13
|
+
Trash,
|
|
12
14
|
X
|
|
13
15
|
} from "@phosphor-icons/react";
|
|
14
16
|
import { formatDistanceToNow } from "date-fns";
|
|
15
|
-
import React2, { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
|
|
17
|
+
import React2, { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
|
|
16
18
|
|
|
17
19
|
// src/provider.tsx
|
|
18
20
|
import {
|
|
@@ -44,6 +46,13 @@ import { jsx } from "react/jsx-runtime";
|
|
|
44
46
|
var LIST_LIMIT = 200;
|
|
45
47
|
var NOTE_MIN_LENGTH = 10;
|
|
46
48
|
var NOTE_MAX_LENGTH = 2e3;
|
|
49
|
+
var ATTACHMENT_MAX_COUNT = 5;
|
|
50
|
+
var ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
51
|
+
var ATTACHMENT_ALLOWED_MIMES = /* @__PURE__ */ new Set([
|
|
52
|
+
"image/png",
|
|
53
|
+
"image/jpeg",
|
|
54
|
+
"image/webp"
|
|
55
|
+
]);
|
|
47
56
|
var DEFAULT_INPUT_MODES = ["text"];
|
|
48
57
|
var DEFAULT_VOICE_PROVIDER = "elevenlabs_scribe_realtime";
|
|
49
58
|
var DEFAULT_VOICE_MODEL_ID = "scribe_v2_realtime";
|
|
@@ -116,13 +125,31 @@ var defaultIssueReportingCopy = {
|
|
|
116
125
|
retryAction: "Retry",
|
|
117
126
|
originHumanLabel: "Human",
|
|
118
127
|
originMachineLabel: "Machine",
|
|
119
|
-
machineOriginFallback: "system"
|
|
128
|
+
machineOriginFallback: "system",
|
|
129
|
+
threadTitle: "Conversation",
|
|
130
|
+
threadDescription: "Messages from the support team about this report, and your replies.",
|
|
131
|
+
threadLoading: "Loading conversation...",
|
|
132
|
+
threadLoadFailed: "Failed to load the conversation",
|
|
133
|
+
threadEmpty: "No messages on this report yet.",
|
|
134
|
+
threadNeedsResponseBadge: "Needs your response",
|
|
135
|
+
threadAuthorOperator: "Support",
|
|
136
|
+
threadAuthorReporter: "You",
|
|
137
|
+
threadKindClarificationRequest: "Question",
|
|
138
|
+
threadKindReporterResponse: "Your reply",
|
|
139
|
+
threadKindFinalResponse: "Resolution",
|
|
140
|
+
threadResponsePlaceholder: "Write your response...",
|
|
141
|
+
threadResponseLabel: "Respond to the support team",
|
|
142
|
+
threadResponseSubmitAction: "Send response",
|
|
143
|
+
threadResponseSubmittingAction: "Sending...",
|
|
144
|
+
threadResponseConflict: "That response was already sent, or the message changed. Refresh and try again.",
|
|
145
|
+
threadResponseFailed: "Failed to send your response"
|
|
120
146
|
};
|
|
121
147
|
var issueReportingKeys = {
|
|
122
148
|
all: ["spaps-issue-reporting"],
|
|
123
149
|
status: (scope) => [...issueReportingKeys.all, "status", scope],
|
|
124
150
|
history: (scope) => [...issueReportingKeys.all, "history", scope],
|
|
125
|
-
detail: (issueReportId) => [...issueReportingKeys.all, "detail", issueReportId]
|
|
151
|
+
detail: (issueReportId) => [...issueReportingKeys.all, "detail", issueReportId],
|
|
152
|
+
messages: (issueReportId) => [...issueReportingKeys.all, "messages", issueReportId]
|
|
126
153
|
};
|
|
127
154
|
function resolvePageUrl(getPageUrl) {
|
|
128
155
|
if (getPageUrl) {
|
|
@@ -292,6 +319,27 @@ function getEntryPointClassName(state) {
|
|
|
292
319
|
}
|
|
293
320
|
return "text-slate-500";
|
|
294
321
|
}
|
|
322
|
+
function selectReporterVisibleMessages(messages) {
|
|
323
|
+
return messages.filter(
|
|
324
|
+
(message) => message.reporter_visible && message.state === "active"
|
|
325
|
+
).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
326
|
+
}
|
|
327
|
+
function isReporterMessageConflict(error) {
|
|
328
|
+
if (!error || typeof error !== "object") {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
const record = error;
|
|
332
|
+
const code = record.code ?? record.error?.code;
|
|
333
|
+
if (typeof code === "string" && code === "ISSUE_REPORT_MESSAGE_CONFLICT") {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
const status = record.status ?? record.statusCode ?? record.status_code ?? void 0;
|
|
337
|
+
if (status === 409) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
const message = error instanceof Error ? error.message : typeof record.message === "string" ? record.message : "";
|
|
341
|
+
return /ISSUE_REPORT_MESSAGE_CONFLICT/i.test(message) || /\b409\b/.test(message);
|
|
342
|
+
}
|
|
295
343
|
function getIssueNoteLengthMessage(note, copy) {
|
|
296
344
|
if (note.length < NOTE_MIN_LENGTH) {
|
|
297
345
|
return `${NOTE_MIN_LENGTH - note.length} ${copy.noteMinimumSuffix}`;
|
|
@@ -386,18 +434,26 @@ function useIssueReportingMutations() {
|
|
|
386
434
|
const updateMutation = useMutation({
|
|
387
435
|
mutationFn: ({
|
|
388
436
|
issueReportId,
|
|
389
|
-
note
|
|
390
|
-
|
|
437
|
+
note,
|
|
438
|
+
add_attachment_ids,
|
|
439
|
+
remove_attachment_ids
|
|
440
|
+
}) => client.issueReporting.update(issueReportId, {
|
|
441
|
+
note,
|
|
442
|
+
add_attachment_ids,
|
|
443
|
+
remove_attachment_ids
|
|
444
|
+
}),
|
|
391
445
|
onSuccess: invalidateAll
|
|
392
446
|
});
|
|
393
447
|
const replyMutation = useMutation({
|
|
394
448
|
mutationFn: ({
|
|
395
449
|
issueReportId,
|
|
396
450
|
note,
|
|
397
|
-
reporterRoleHint
|
|
451
|
+
reporterRoleHint,
|
|
452
|
+
attachment_ids
|
|
398
453
|
}) => client.issueReporting.reply(issueReportId, {
|
|
399
454
|
note,
|
|
400
|
-
reporter_role_hint: reporterRoleHint
|
|
455
|
+
reporter_role_hint: reporterRoleHint,
|
|
456
|
+
attachment_ids
|
|
401
457
|
}),
|
|
402
458
|
onSuccess: invalidateAll
|
|
403
459
|
});
|
|
@@ -407,6 +463,39 @@ function useIssueReportingMutations() {
|
|
|
407
463
|
replyMutation
|
|
408
464
|
};
|
|
409
465
|
}
|
|
466
|
+
function useIssueReportingMessages(issueReportId) {
|
|
467
|
+
const { client, isEligible } = useIssueReporting();
|
|
468
|
+
const listMessages = client.issueReporting.listMessages;
|
|
469
|
+
return useQuery({
|
|
470
|
+
queryKey: issueReportingKeys.messages(issueReportId ?? "none"),
|
|
471
|
+
queryFn: () => listMessages(issueReportId),
|
|
472
|
+
enabled: isEligible && Boolean(issueReportId) && Boolean(listMessages),
|
|
473
|
+
retry: false
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function useIssueReportingMessageMutation(issueReportId) {
|
|
477
|
+
const queryClient = useQueryClient();
|
|
478
|
+
const { client } = useIssueReporting();
|
|
479
|
+
return useMutation({
|
|
480
|
+
mutationFn: (payload) => {
|
|
481
|
+
const submit = client.issueReporting.submitMessage;
|
|
482
|
+
if (!submit || !issueReportId) {
|
|
483
|
+
return Promise.reject(
|
|
484
|
+
new Error("This client does not support submitting messages.")
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
return submit(issueReportId, payload);
|
|
488
|
+
},
|
|
489
|
+
onSuccess: async () => {
|
|
490
|
+
if (!issueReportId) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
await queryClient.invalidateQueries({
|
|
494
|
+
queryKey: issueReportingKeys.messages(issueReportId)
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
410
499
|
function IssueReportingProvider({
|
|
411
500
|
client,
|
|
412
501
|
isEligible,
|
|
@@ -436,6 +525,7 @@ function IssueReportingProvider({
|
|
|
436
525
|
const [scope, setScope] = useState(
|
|
437
526
|
() => resolveInitialScope(defaultScope, allowTenantScope)
|
|
438
527
|
);
|
|
528
|
+
const [needsResponseMap, setNeedsResponseMap] = useState({});
|
|
439
529
|
const [pageConfigs, setPageConfigs] = useState([]);
|
|
440
530
|
const [registeredTargets, setRegisteredTargets] = useState(
|
|
441
531
|
[]
|
|
@@ -460,6 +550,21 @@ function IssueReportingProvider({
|
|
|
460
550
|
useEffect(() => {
|
|
461
551
|
setScope(resolveInitialScope(defaultScope, allowTenantScope));
|
|
462
552
|
}, [allowTenantScope, defaultScope]);
|
|
553
|
+
const setNeedsResponse = useCallback(
|
|
554
|
+
(issueReportId, needsResponse) => {
|
|
555
|
+
setNeedsResponseMap((current) => {
|
|
556
|
+
if (Boolean(current[issueReportId]) === needsResponse) {
|
|
557
|
+
return current;
|
|
558
|
+
}
|
|
559
|
+
return { ...current, [issueReportId]: needsResponse };
|
|
560
|
+
});
|
|
561
|
+
},
|
|
562
|
+
[]
|
|
563
|
+
);
|
|
564
|
+
const needsResponseIssueIds = useMemo(
|
|
565
|
+
() => Object.entries(needsResponseMap).filter(([, needs]) => needs).map(([id]) => id),
|
|
566
|
+
[needsResponseMap]
|
|
567
|
+
);
|
|
463
568
|
const closePopover = useCallback(() => {
|
|
464
569
|
setIsPopoverOpen(false);
|
|
465
570
|
}, []);
|
|
@@ -647,7 +752,9 @@ function IssueReportingProvider({
|
|
|
647
752
|
createMode,
|
|
648
753
|
inputModes: resolvedInputModes,
|
|
649
754
|
defaultInputMode: resolvedDefaultInputMode,
|
|
650
|
-
voice: resolvedVoiceConfig
|
|
755
|
+
voice: resolvedVoiceConfig,
|
|
756
|
+
needsResponseIssueIds,
|
|
757
|
+
setNeedsResponse
|
|
651
758
|
}),
|
|
652
759
|
[
|
|
653
760
|
allowTenantScope,
|
|
@@ -663,6 +770,7 @@ function IssueReportingProvider({
|
|
|
663
770
|
isReportMode,
|
|
664
771
|
mergedCopy,
|
|
665
772
|
modalState,
|
|
773
|
+
needsResponseIssueIds,
|
|
666
774
|
openExistingIssueModal,
|
|
667
775
|
openPageIssueModal,
|
|
668
776
|
openPopover,
|
|
@@ -674,6 +782,7 @@ function IssueReportingProvider({
|
|
|
674
782
|
resolvedVoiceConfig,
|
|
675
783
|
scope,
|
|
676
784
|
selectPanel,
|
|
785
|
+
setNeedsResponse,
|
|
677
786
|
startNewIssue
|
|
678
787
|
]
|
|
679
788
|
);
|
|
@@ -953,6 +1062,7 @@ function IssueReportModalBody({
|
|
|
953
1062
|
error,
|
|
954
1063
|
canUseVoice,
|
|
955
1064
|
canUseText,
|
|
1065
|
+
canUseAttachments,
|
|
956
1066
|
effectiveInputMode,
|
|
957
1067
|
note,
|
|
958
1068
|
normalizedNote,
|
|
@@ -966,6 +1076,11 @@ function IssueReportModalBody({
|
|
|
966
1076
|
voiceError,
|
|
967
1077
|
scribeError,
|
|
968
1078
|
submitError,
|
|
1079
|
+
existingAttachments,
|
|
1080
|
+
removedExistingIds,
|
|
1081
|
+
pendingFiles,
|
|
1082
|
+
uploadProgress,
|
|
1083
|
+
attachmentValidationErrors,
|
|
969
1084
|
onRetryHydration,
|
|
970
1085
|
onClose,
|
|
971
1086
|
onSelectText,
|
|
@@ -974,7 +1089,10 @@ function IssueReportModalBody({
|
|
|
974
1089
|
onStopVoiceInput,
|
|
975
1090
|
onAppendTranscript,
|
|
976
1091
|
onNoteChange,
|
|
977
|
-
onSubmit
|
|
1092
|
+
onSubmit,
|
|
1093
|
+
onAddFiles,
|
|
1094
|
+
onRemoveExistingAttachment,
|
|
1095
|
+
onRemovePendingAttachment
|
|
978
1096
|
}) {
|
|
979
1097
|
if (isHydrating) {
|
|
980
1098
|
return /* @__PURE__ */ 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: [
|
|
@@ -1044,6 +1162,20 @@ function IssueReportModalBody({
|
|
|
1044
1162
|
onSubmit
|
|
1045
1163
|
}
|
|
1046
1164
|
),
|
|
1165
|
+
canUseAttachments && /* @__PURE__ */ jsx2(
|
|
1166
|
+
AttachmentPicker,
|
|
1167
|
+
{
|
|
1168
|
+
existingAttachments,
|
|
1169
|
+
removedExistingIds,
|
|
1170
|
+
pendingFiles,
|
|
1171
|
+
uploadProgress,
|
|
1172
|
+
validationErrors: attachmentValidationErrors,
|
|
1173
|
+
disabled: isSubmitting,
|
|
1174
|
+
onAddFiles,
|
|
1175
|
+
onRemoveExisting: onRemoveExistingAttachment,
|
|
1176
|
+
onRemovePending: onRemovePendingAttachment
|
|
1177
|
+
}
|
|
1178
|
+
),
|
|
1047
1179
|
submitError ? /* @__PURE__ */ jsx2("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,
|
|
1048
1180
|
/* @__PURE__ */ jsxs("div", { className: "mt-6 flex justify-end gap-3", children: [
|
|
1049
1181
|
/* @__PURE__ */ jsx2(
|
|
@@ -1069,7 +1201,8 @@ function IssueReportModalBody({
|
|
|
1069
1201
|
children: isSubmitting ? copy.submittingAction : copy.submitAction
|
|
1070
1202
|
}
|
|
1071
1203
|
)
|
|
1072
|
-
] })
|
|
1204
|
+
] }),
|
|
1205
|
+
mode !== "create" && issue ? /* @__PURE__ */ jsx2(IssueReportMessageThread, { issueReportId: issue.id }) : null
|
|
1073
1206
|
] });
|
|
1074
1207
|
}
|
|
1075
1208
|
function useIssueReportVoiceCapture({
|
|
@@ -1167,6 +1300,493 @@ function useIssueReportVoiceCapture({
|
|
|
1167
1300
|
appendTranscript
|
|
1168
1301
|
};
|
|
1169
1302
|
}
|
|
1303
|
+
var INITIAL_UPLOAD_PROGRESS = {
|
|
1304
|
+
phase: "idle",
|
|
1305
|
+
uploaded: 0,
|
|
1306
|
+
total: 0
|
|
1307
|
+
};
|
|
1308
|
+
var pendingFileCounter = 0;
|
|
1309
|
+
function nextPendingId() {
|
|
1310
|
+
pendingFileCounter += 1;
|
|
1311
|
+
return `pending-${pendingFileCounter}`;
|
|
1312
|
+
}
|
|
1313
|
+
function formatFileSize(bytes) {
|
|
1314
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1315
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1316
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1317
|
+
}
|
|
1318
|
+
function validateAttachmentFile(file) {
|
|
1319
|
+
if (!ATTACHMENT_ALLOWED_MIMES.has(file.type)) {
|
|
1320
|
+
return `"${file.name}" is not a supported image type (PNG, JPEG, or WebP).`;
|
|
1321
|
+
}
|
|
1322
|
+
if (file.size > ATTACHMENT_MAX_BYTES) {
|
|
1323
|
+
return `"${file.name}" exceeds the 10 MB limit.`;
|
|
1324
|
+
}
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
function AttachmentPicker({
|
|
1328
|
+
existingAttachments,
|
|
1329
|
+
removedExistingIds,
|
|
1330
|
+
pendingFiles,
|
|
1331
|
+
uploadProgress,
|
|
1332
|
+
validationErrors,
|
|
1333
|
+
disabled,
|
|
1334
|
+
onAddFiles,
|
|
1335
|
+
onRemoveExisting,
|
|
1336
|
+
onRemovePending
|
|
1337
|
+
}) {
|
|
1338
|
+
const fileInputRef = useRef2(null);
|
|
1339
|
+
const retainedExisting = existingAttachments.filter(
|
|
1340
|
+
(a) => !removedExistingIds.has(a.id)
|
|
1341
|
+
);
|
|
1342
|
+
const totalCount = retainedExisting.length + pendingFiles.length;
|
|
1343
|
+
const canAdd = totalCount < ATTACHMENT_MAX_COUNT && !disabled;
|
|
1344
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-3", children: [
|
|
1345
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1346
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-slate-500", children: [
|
|
1347
|
+
/* @__PURE__ */ jsx2(Image, { className: "h-3.5 w-3.5" }),
|
|
1348
|
+
"Screenshots (",
|
|
1349
|
+
totalCount,
|
|
1350
|
+
"/",
|
|
1351
|
+
ATTACHMENT_MAX_COUNT,
|
|
1352
|
+
")"
|
|
1353
|
+
] }),
|
|
1354
|
+
/* @__PURE__ */ jsx2(
|
|
1355
|
+
"input",
|
|
1356
|
+
{
|
|
1357
|
+
ref: fileInputRef,
|
|
1358
|
+
type: "file",
|
|
1359
|
+
accept: "image/png,image/jpeg,image/webp",
|
|
1360
|
+
multiple: true,
|
|
1361
|
+
className: "hidden",
|
|
1362
|
+
onChange: (e) => {
|
|
1363
|
+
if (e.target.files && e.target.files.length > 0) {
|
|
1364
|
+
onAddFiles(e.target.files);
|
|
1365
|
+
}
|
|
1366
|
+
e.target.value = "";
|
|
1367
|
+
},
|
|
1368
|
+
disabled: !canAdd,
|
|
1369
|
+
"aria-label": "Select screenshot files"
|
|
1370
|
+
}
|
|
1371
|
+
),
|
|
1372
|
+
/* @__PURE__ */ jsx2(
|
|
1373
|
+
"button",
|
|
1374
|
+
{
|
|
1375
|
+
type: "button",
|
|
1376
|
+
className: cn(
|
|
1377
|
+
"rounded-full border px-3 py-1 text-xs font-medium transition",
|
|
1378
|
+
canAdd ? "border-slate-300 text-slate-700 hover:bg-slate-50" : "cursor-not-allowed border-slate-200 text-slate-400"
|
|
1379
|
+
),
|
|
1380
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1381
|
+
disabled: !canAdd,
|
|
1382
|
+
"aria-label": "Add screenshots",
|
|
1383
|
+
children: "Add"
|
|
1384
|
+
}
|
|
1385
|
+
)
|
|
1386
|
+
] }),
|
|
1387
|
+
validationErrors.length > 0 && /* @__PURE__ */ jsx2("div", { className: "space-y-1", children: validationErrors.map((err, i) => /* @__PURE__ */ jsx2(
|
|
1388
|
+
"div",
|
|
1389
|
+
{
|
|
1390
|
+
className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
|
|
1391
|
+
role: "alert",
|
|
1392
|
+
children: err
|
|
1393
|
+
},
|
|
1394
|
+
i
|
|
1395
|
+
)) }),
|
|
1396
|
+
uploadProgress.phase === "uploading" && /* @__PURE__ */ 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: [
|
|
1397
|
+
/* @__PURE__ */ jsx2(Spinner, { className: "h-3.5 w-3.5 animate-spin" }),
|
|
1398
|
+
"Uploading ",
|
|
1399
|
+
uploadProgress.uploaded,
|
|
1400
|
+
" of ",
|
|
1401
|
+
uploadProgress.total,
|
|
1402
|
+
"..."
|
|
1403
|
+
] }),
|
|
1404
|
+
uploadProgress.phase === "error" && uploadProgress.error && /* @__PURE__ */ jsx2(
|
|
1405
|
+
"div",
|
|
1406
|
+
{
|
|
1407
|
+
className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
|
|
1408
|
+
role: "alert",
|
|
1409
|
+
children: uploadProgress.error
|
|
1410
|
+
}
|
|
1411
|
+
),
|
|
1412
|
+
(retainedExisting.length > 0 || pendingFiles.length > 0) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
1413
|
+
retainedExisting.map((att) => /* @__PURE__ */ jsxs(
|
|
1414
|
+
"div",
|
|
1415
|
+
{
|
|
1416
|
+
className: "group relative flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2",
|
|
1417
|
+
children: [
|
|
1418
|
+
/* @__PURE__ */ jsx2(Image, { className: "h-4 w-4 flex-shrink-0 text-slate-400" }),
|
|
1419
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
1420
|
+
/* @__PURE__ */ jsx2("div", { className: "max-w-[140px] truncate text-xs font-medium text-slate-700", children: att.original_filename }),
|
|
1421
|
+
/* @__PURE__ */ jsx2("div", { className: "text-xs text-slate-400", children: formatFileSize(att.byte_size) })
|
|
1422
|
+
] }),
|
|
1423
|
+
/* @__PURE__ */ jsx2(
|
|
1424
|
+
"button",
|
|
1425
|
+
{
|
|
1426
|
+
type: "button",
|
|
1427
|
+
className: "ml-1 rounded-full p-0.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700",
|
|
1428
|
+
onClick: () => onRemoveExisting(att.id),
|
|
1429
|
+
disabled,
|
|
1430
|
+
"aria-label": `Remove ${att.original_filename}`,
|
|
1431
|
+
children: /* @__PURE__ */ jsx2(Trash, { className: "h-3.5 w-3.5" })
|
|
1432
|
+
}
|
|
1433
|
+
)
|
|
1434
|
+
]
|
|
1435
|
+
},
|
|
1436
|
+
att.id
|
|
1437
|
+
)),
|
|
1438
|
+
pendingFiles.map((pf) => /* @__PURE__ */ jsxs(
|
|
1439
|
+
"div",
|
|
1440
|
+
{
|
|
1441
|
+
className: "group relative flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2",
|
|
1442
|
+
children: [
|
|
1443
|
+
/* @__PURE__ */ jsx2(
|
|
1444
|
+
"img",
|
|
1445
|
+
{
|
|
1446
|
+
src: pf.previewUrl,
|
|
1447
|
+
alt: pf.file.name,
|
|
1448
|
+
className: "h-8 w-8 flex-shrink-0 rounded object-cover"
|
|
1449
|
+
}
|
|
1450
|
+
),
|
|
1451
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
1452
|
+
/* @__PURE__ */ jsx2("div", { className: "max-w-[140px] truncate text-xs font-medium text-slate-700", children: pf.file.name }),
|
|
1453
|
+
/* @__PURE__ */ jsx2("div", { className: "text-xs text-slate-400", children: formatFileSize(pf.file.size) })
|
|
1454
|
+
] }),
|
|
1455
|
+
/* @__PURE__ */ jsx2(
|
|
1456
|
+
"button",
|
|
1457
|
+
{
|
|
1458
|
+
type: "button",
|
|
1459
|
+
className: "ml-1 rounded-full p-0.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700",
|
|
1460
|
+
onClick: () => onRemovePending(pf.clientId),
|
|
1461
|
+
disabled,
|
|
1462
|
+
"aria-label": `Remove ${pf.file.name}`,
|
|
1463
|
+
children: /* @__PURE__ */ jsx2(Trash, { className: "h-3.5 w-3.5" })
|
|
1464
|
+
}
|
|
1465
|
+
)
|
|
1466
|
+
]
|
|
1467
|
+
},
|
|
1468
|
+
pf.clientId
|
|
1469
|
+
))
|
|
1470
|
+
] })
|
|
1471
|
+
] });
|
|
1472
|
+
}
|
|
1473
|
+
function useAttachmentState(existingAttachments) {
|
|
1474
|
+
const [pendingFiles, setPendingFiles] = useState2([]);
|
|
1475
|
+
const [removedExistingIds, setRemovedExistingIds] = useState2(
|
|
1476
|
+
/* @__PURE__ */ new Set()
|
|
1477
|
+
);
|
|
1478
|
+
const [validationErrors, setValidationErrors] = useState2([]);
|
|
1479
|
+
const [uploadProgress, setUploadProgress] = useState2(INITIAL_UPLOAD_PROGRESS);
|
|
1480
|
+
const retainedExistingCount = existingAttachments.filter(
|
|
1481
|
+
(a) => !removedExistingIds.has(a.id)
|
|
1482
|
+
).length;
|
|
1483
|
+
const reset = useCallback2(() => {
|
|
1484
|
+
for (const pf of pendingFiles) {
|
|
1485
|
+
URL.revokeObjectURL(pf.previewUrl);
|
|
1486
|
+
}
|
|
1487
|
+
setPendingFiles([]);
|
|
1488
|
+
setRemovedExistingIds(/* @__PURE__ */ new Set());
|
|
1489
|
+
setValidationErrors([]);
|
|
1490
|
+
setUploadProgress(INITIAL_UPLOAD_PROGRESS);
|
|
1491
|
+
}, [pendingFiles]);
|
|
1492
|
+
const addFiles = useCallback2(
|
|
1493
|
+
(fileList) => {
|
|
1494
|
+
const currentTotal = retainedExistingCount + pendingFiles.length;
|
|
1495
|
+
const errors = [];
|
|
1496
|
+
const accepted = [];
|
|
1497
|
+
let count = currentTotal;
|
|
1498
|
+
for (let i = 0; i < fileList.length; i++) {
|
|
1499
|
+
const file = fileList[i];
|
|
1500
|
+
if (count >= ATTACHMENT_MAX_COUNT) {
|
|
1501
|
+
errors.push(
|
|
1502
|
+
`"${file.name}" was not added; maximum ${ATTACHMENT_MAX_COUNT} screenshots reached.`
|
|
1503
|
+
);
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
const err = validateAttachmentFile(file);
|
|
1507
|
+
if (err) {
|
|
1508
|
+
errors.push(err);
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
accepted.push({
|
|
1512
|
+
clientId: nextPendingId(),
|
|
1513
|
+
file,
|
|
1514
|
+
previewUrl: URL.createObjectURL(file)
|
|
1515
|
+
});
|
|
1516
|
+
count += 1;
|
|
1517
|
+
}
|
|
1518
|
+
if (accepted.length > 0) {
|
|
1519
|
+
setPendingFiles((prev) => [...prev, ...accepted]);
|
|
1520
|
+
}
|
|
1521
|
+
setValidationErrors(errors);
|
|
1522
|
+
setUploadProgress(INITIAL_UPLOAD_PROGRESS);
|
|
1523
|
+
},
|
|
1524
|
+
[pendingFiles.length, retainedExistingCount]
|
|
1525
|
+
);
|
|
1526
|
+
const removeExisting = useCallback2((id) => {
|
|
1527
|
+
setRemovedExistingIds((prev) => /* @__PURE__ */ new Set([...prev, id]));
|
|
1528
|
+
}, []);
|
|
1529
|
+
const removePending = useCallback2((clientId) => {
|
|
1530
|
+
setPendingFiles((prev) => {
|
|
1531
|
+
const removed = prev.find((pf) => pf.clientId === clientId);
|
|
1532
|
+
if (removed) {
|
|
1533
|
+
URL.revokeObjectURL(removed.previewUrl);
|
|
1534
|
+
}
|
|
1535
|
+
return prev.filter((pf) => pf.clientId !== clientId);
|
|
1536
|
+
});
|
|
1537
|
+
setUploadProgress(INITIAL_UPLOAD_PROGRESS);
|
|
1538
|
+
}, []);
|
|
1539
|
+
const markPendingUploaded = useCallback2((clientId, attachmentId) => {
|
|
1540
|
+
setPendingFiles(
|
|
1541
|
+
(prev) => prev.map(
|
|
1542
|
+
(pf) => pf.clientId === clientId ? { ...pf, uploadedAttachmentId: attachmentId } : pf
|
|
1543
|
+
)
|
|
1544
|
+
);
|
|
1545
|
+
}, []);
|
|
1546
|
+
return {
|
|
1547
|
+
pendingFiles,
|
|
1548
|
+
removedExistingIds,
|
|
1549
|
+
validationErrors,
|
|
1550
|
+
uploadProgress,
|
|
1551
|
+
setUploadProgress,
|
|
1552
|
+
addFiles,
|
|
1553
|
+
removeExisting,
|
|
1554
|
+
removePending,
|
|
1555
|
+
markPendingUploaded,
|
|
1556
|
+
reset
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
var REPORTER_MESSAGE_MIN_LENGTH = 1;
|
|
1560
|
+
var REPORTER_MESSAGE_MAX_LENGTH = 2e3;
|
|
1561
|
+
function generateIdempotencyKey() {
|
|
1562
|
+
const cryptoObj = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
|
|
1563
|
+
if (cryptoObj && typeof cryptoObj.randomUUID === "function") {
|
|
1564
|
+
return `reporter-msg-${cryptoObj.randomUUID()}`;
|
|
1565
|
+
}
|
|
1566
|
+
return `reporter-msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1567
|
+
}
|
|
1568
|
+
function getMessageKindLabel(kind, copy) {
|
|
1569
|
+
switch (kind) {
|
|
1570
|
+
case "clarification_request":
|
|
1571
|
+
return copy.threadKindClarificationRequest;
|
|
1572
|
+
case "reporter_response":
|
|
1573
|
+
return copy.threadKindReporterResponse;
|
|
1574
|
+
case "final_response":
|
|
1575
|
+
return copy.threadKindFinalResponse;
|
|
1576
|
+
default:
|
|
1577
|
+
return kind;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
function getMessageKindClassName(kind) {
|
|
1581
|
+
switch (kind) {
|
|
1582
|
+
case "clarification_request":
|
|
1583
|
+
return "bg-amber-100 text-amber-700";
|
|
1584
|
+
case "final_response":
|
|
1585
|
+
return "bg-emerald-100 text-emerald-700";
|
|
1586
|
+
default:
|
|
1587
|
+
return "bg-slate-100 text-slate-600";
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
function MessageBubble({
|
|
1591
|
+
message,
|
|
1592
|
+
copy
|
|
1593
|
+
}) {
|
|
1594
|
+
const isReporter = message.actor.author_type === "reporter";
|
|
1595
|
+
const isFinal = message.kind === "final_response";
|
|
1596
|
+
const authorLabel = isReporter ? copy.threadAuthorReporter : copy.threadAuthorOperator;
|
|
1597
|
+
return /* @__PURE__ */ jsxs(
|
|
1598
|
+
"li",
|
|
1599
|
+
{
|
|
1600
|
+
className: cn(
|
|
1601
|
+
"flex flex-col gap-1",
|
|
1602
|
+
isReporter ? "items-end" : "items-start"
|
|
1603
|
+
),
|
|
1604
|
+
children: [
|
|
1605
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1606
|
+
/* @__PURE__ */ jsx2(
|
|
1607
|
+
"span",
|
|
1608
|
+
{
|
|
1609
|
+
className: cn(
|
|
1610
|
+
"rounded-full px-2 py-0.5 font-medium",
|
|
1611
|
+
BADGE_TEXT,
|
|
1612
|
+
getMessageKindClassName(message.kind)
|
|
1613
|
+
),
|
|
1614
|
+
children: getMessageKindLabel(message.kind, copy)
|
|
1615
|
+
}
|
|
1616
|
+
),
|
|
1617
|
+
/* @__PURE__ */ jsx2("span", { className: cn("font-medium text-slate-600", LABEL_TEXT), children: authorLabel }),
|
|
1618
|
+
/* @__PURE__ */ jsx2(
|
|
1619
|
+
"time",
|
|
1620
|
+
{
|
|
1621
|
+
className: "text-xs text-slate-400",
|
|
1622
|
+
dateTime: message.created_at,
|
|
1623
|
+
children: formatRelativeTime(message.created_at)
|
|
1624
|
+
}
|
|
1625
|
+
)
|
|
1626
|
+
] }),
|
|
1627
|
+
/* @__PURE__ */ jsx2(
|
|
1628
|
+
"div",
|
|
1629
|
+
{
|
|
1630
|
+
className: cn(
|
|
1631
|
+
"max-w-[85%] whitespace-pre-wrap rounded-2xl border px-3 py-2 text-sm",
|
|
1632
|
+
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"
|
|
1633
|
+
),
|
|
1634
|
+
children: message.body
|
|
1635
|
+
}
|
|
1636
|
+
)
|
|
1637
|
+
]
|
|
1638
|
+
}
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
function ReporterResponseComposer({
|
|
1642
|
+
issueReportId,
|
|
1643
|
+
copy
|
|
1644
|
+
}) {
|
|
1645
|
+
const mutation = useIssueReportingMessageMutation(issueReportId);
|
|
1646
|
+
const idempotencyKeyRef = useRef2(generateIdempotencyKey());
|
|
1647
|
+
const [body, setBody] = useState2("");
|
|
1648
|
+
const [submitError, setSubmitError] = useState2(null);
|
|
1649
|
+
const normalized = body.trim();
|
|
1650
|
+
const isValid = normalized.length >= REPORTER_MESSAGE_MIN_LENGTH && normalized.length <= REPORTER_MESSAGE_MAX_LENGTH;
|
|
1651
|
+
const isSubmitting = mutation.isPending;
|
|
1652
|
+
const handleSubmit = async () => {
|
|
1653
|
+
if (!isValid || isSubmitting) {
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
setSubmitError(null);
|
|
1657
|
+
try {
|
|
1658
|
+
await mutation.mutateAsync({
|
|
1659
|
+
body: normalized,
|
|
1660
|
+
idempotency_key: idempotencyKeyRef.current
|
|
1661
|
+
});
|
|
1662
|
+
setBody("");
|
|
1663
|
+
idempotencyKeyRef.current = generateIdempotencyKey();
|
|
1664
|
+
} catch (error) {
|
|
1665
|
+
if (isReporterMessageConflict(error)) {
|
|
1666
|
+
setSubmitError(copy.threadResponseConflict);
|
|
1667
|
+
idempotencyKeyRef.current = generateIdempotencyKey();
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
setSubmitError(resolveErrorMessage(error, copy.threadResponseFailed));
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-3 space-y-2", children: [
|
|
1674
|
+
/* @__PURE__ */ jsx2(
|
|
1675
|
+
"label",
|
|
1676
|
+
{
|
|
1677
|
+
htmlFor: "issue-report-thread-response",
|
|
1678
|
+
className: cn(
|
|
1679
|
+
"block font-medium uppercase tracking-wide text-slate-500",
|
|
1680
|
+
LABEL_TEXT
|
|
1681
|
+
),
|
|
1682
|
+
children: copy.threadResponseLabel
|
|
1683
|
+
}
|
|
1684
|
+
),
|
|
1685
|
+
/* @__PURE__ */ jsx2(
|
|
1686
|
+
"textarea",
|
|
1687
|
+
{
|
|
1688
|
+
id: "issue-report-thread-response",
|
|
1689
|
+
value: body,
|
|
1690
|
+
onChange: (event) => setBody(event.target.value),
|
|
1691
|
+
onKeyDown: (event) => {
|
|
1692
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
1693
|
+
event.preventDefault();
|
|
1694
|
+
void handleSubmit();
|
|
1695
|
+
}
|
|
1696
|
+
},
|
|
1697
|
+
placeholder: copy.threadResponsePlaceholder,
|
|
1698
|
+
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",
|
|
1699
|
+
disabled: isSubmitting
|
|
1700
|
+
}
|
|
1701
|
+
),
|
|
1702
|
+
submitError ? /* @__PURE__ */ jsx2(
|
|
1703
|
+
"div",
|
|
1704
|
+
{
|
|
1705
|
+
className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
|
|
1706
|
+
role: "alert",
|
|
1707
|
+
children: submitError
|
|
1708
|
+
}
|
|
1709
|
+
) : null,
|
|
1710
|
+
/* @__PURE__ */ jsx2("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx2(
|
|
1711
|
+
"button",
|
|
1712
|
+
{
|
|
1713
|
+
type: "button",
|
|
1714
|
+
className: cn(
|
|
1715
|
+
"rounded-full px-4 py-2 text-sm font-semibold text-white transition",
|
|
1716
|
+
isValid && !isSubmitting ? "bg-slate-900 hover:bg-slate-800" : "cursor-not-allowed bg-slate-300"
|
|
1717
|
+
),
|
|
1718
|
+
onClick: () => void handleSubmit(),
|
|
1719
|
+
disabled: !isValid || isSubmitting,
|
|
1720
|
+
children: isSubmitting ? copy.threadResponseSubmittingAction : copy.threadResponseSubmitAction
|
|
1721
|
+
}
|
|
1722
|
+
) })
|
|
1723
|
+
] });
|
|
1724
|
+
}
|
|
1725
|
+
function IssueReportMessageThread({
|
|
1726
|
+
issueReportId
|
|
1727
|
+
}) {
|
|
1728
|
+
const { copy, client, setNeedsResponse } = useIssueReporting();
|
|
1729
|
+
const supportsMessages = Boolean(client.issueReporting.listMessages);
|
|
1730
|
+
const supportsSubmit = Boolean(client.issueReporting.submitMessage);
|
|
1731
|
+
const query = useIssueReportingMessages(issueReportId);
|
|
1732
|
+
const needsResponse = query.data?.needs_response ?? false;
|
|
1733
|
+
useEffect2(() => {
|
|
1734
|
+
setNeedsResponse(issueReportId, needsResponse);
|
|
1735
|
+
return () => {
|
|
1736
|
+
setNeedsResponse(issueReportId, false);
|
|
1737
|
+
};
|
|
1738
|
+
}, [issueReportId, needsResponse, setNeedsResponse]);
|
|
1739
|
+
if (!supportsMessages) {
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
const visibleMessages = selectReporterVisibleMessages(query.data?.items ?? []);
|
|
1743
|
+
return /* @__PURE__ */ jsxs(
|
|
1744
|
+
"section",
|
|
1745
|
+
{
|
|
1746
|
+
"aria-label": copy.threadTitle,
|
|
1747
|
+
className: "mt-6 border-t border-slate-100 pt-5",
|
|
1748
|
+
children: [
|
|
1749
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
1750
|
+
/* @__PURE__ */ jsx2("h3", { className: "text-sm font-semibold text-slate-900", children: copy.threadTitle }),
|
|
1751
|
+
needsResponse ? /* @__PURE__ */ jsx2(
|
|
1752
|
+
"span",
|
|
1753
|
+
{
|
|
1754
|
+
className: cn(
|
|
1755
|
+
"rounded-full px-2 py-0.5 font-medium",
|
|
1756
|
+
BADGE_TEXT,
|
|
1757
|
+
"bg-amber-100 text-amber-700"
|
|
1758
|
+
),
|
|
1759
|
+
children: copy.threadNeedsResponseBadge
|
|
1760
|
+
}
|
|
1761
|
+
) : null
|
|
1762
|
+
] }),
|
|
1763
|
+
/* @__PURE__ */ jsx2("p", { className: "mt-1 text-xs text-slate-500", children: copy.threadDescription }),
|
|
1764
|
+
query.isPending ? /* @__PURE__ */ 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: [
|
|
1765
|
+
/* @__PURE__ */ jsx2(Spinner, { className: "h-4 w-4 animate-spin" }),
|
|
1766
|
+
/* @__PURE__ */ jsx2("span", { children: copy.threadLoading })
|
|
1767
|
+
] }) : query.error ? /* @__PURE__ */ 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: [
|
|
1768
|
+
/* @__PURE__ */ jsx2("div", { children: resolveErrorMessage(query.error, copy.threadLoadFailed) }),
|
|
1769
|
+
/* @__PURE__ */ jsx2(
|
|
1770
|
+
"button",
|
|
1771
|
+
{
|
|
1772
|
+
type: "button",
|
|
1773
|
+
className: "rounded-full border border-rose-300 px-3 py-1 font-medium transition hover:bg-rose-100",
|
|
1774
|
+
onClick: () => void query.refetch(),
|
|
1775
|
+
children: copy.retryAction
|
|
1776
|
+
}
|
|
1777
|
+
)
|
|
1778
|
+
] }) : visibleMessages.length === 0 ? /* @__PURE__ */ jsx2("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__ */ jsx2("ul", { className: "mt-4 space-y-3", children: visibleMessages.map((message) => /* @__PURE__ */ jsx2(MessageBubble, { message, copy }, message.id)) }),
|
|
1779
|
+
supportsSubmit ? /* @__PURE__ */ jsx2(
|
|
1780
|
+
ReporterResponseComposer,
|
|
1781
|
+
{
|
|
1782
|
+
issueReportId,
|
|
1783
|
+
copy
|
|
1784
|
+
}
|
|
1785
|
+
) : null
|
|
1786
|
+
]
|
|
1787
|
+
}
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1170
1790
|
function IssueReportModeBanner() {
|
|
1171
1791
|
const { copy } = useIssueReporting();
|
|
1172
1792
|
const reportMode = useReportMode();
|
|
@@ -1467,7 +2087,13 @@ function IssueReportModal() {
|
|
|
1467
2087
|
const { isOpen, mode, issue, target, isHydrating, error } = modalState;
|
|
1468
2088
|
const canUseVoice = mode === "create" && inputModes.includes("voice");
|
|
1469
2089
|
const canUseText = mode !== "create" || inputModes.includes("text");
|
|
1470
|
-
const
|
|
2090
|
+
const canUseAttachments = Boolean(client.issueReporting.uploadAttachment);
|
|
2091
|
+
const existingAttachments = useMemo2(
|
|
2092
|
+
() => mode === "edit" && issue ? issue.attachments ?? [] : [],
|
|
2093
|
+
[issue, mode]
|
|
2094
|
+
);
|
|
2095
|
+
const attachmentState = useAttachmentState(existingAttachments);
|
|
2096
|
+
const isSubmitting = createMutation.isPending || updateMutation.isPending || replyMutation.isPending || attachmentState.uploadProgress.phase === "uploading";
|
|
1471
2097
|
const {
|
|
1472
2098
|
inputMode,
|
|
1473
2099
|
setInputMode,
|
|
@@ -1491,6 +2117,7 @@ function IssueReportModal() {
|
|
|
1491
2117
|
useEffect2(() => {
|
|
1492
2118
|
if (!isOpen) {
|
|
1493
2119
|
resetVoiceCapture();
|
|
2120
|
+
attachmentState.reset();
|
|
1494
2121
|
setNote("");
|
|
1495
2122
|
setSubmitError(null);
|
|
1496
2123
|
return;
|
|
@@ -1501,6 +2128,7 @@ function IssueReportModal() {
|
|
|
1501
2128
|
setNote("");
|
|
1502
2129
|
}
|
|
1503
2130
|
resetVoiceCapture();
|
|
2131
|
+
attachmentState.reset();
|
|
1504
2132
|
setSubmitError(null);
|
|
1505
2133
|
}, [isOpen, issue, mode, resetVoiceCapture]);
|
|
1506
2134
|
const effectiveInputMode = mode !== "create" ? "text" : canUseVoice && inputMode === "voice" ? "voice" : "text";
|
|
@@ -1509,6 +2137,7 @@ function IssueReportModal() {
|
|
|
1509
2137
|
const title = mode === "create" ? `${copy.createTitlePrefix}: ${target?.component_label ?? ""}` : mode === "edit" ? `${copy.editTitlePrefix}: ${target?.component_label ?? ""}` : `${copy.replyTitlePrefix}: ${target?.component_label ?? ""}`;
|
|
1510
2138
|
const handleCloseModal = () => {
|
|
1511
2139
|
resetVoiceCapture();
|
|
2140
|
+
attachmentState.reset();
|
|
1512
2141
|
closeModal();
|
|
1513
2142
|
};
|
|
1514
2143
|
const handleStartVoiceInput = async () => {
|
|
@@ -1520,6 +2149,38 @@ function IssueReportModal() {
|
|
|
1520
2149
|
const handleAppendTranscript = () => {
|
|
1521
2150
|
appendTranscript(setNote);
|
|
1522
2151
|
};
|
|
2152
|
+
const uploadPendingFiles = async () => {
|
|
2153
|
+
const upload = client.issueReporting.uploadAttachment;
|
|
2154
|
+
if (!upload || attachmentState.pendingFiles.length === 0) {
|
|
2155
|
+
return [];
|
|
2156
|
+
}
|
|
2157
|
+
const files = attachmentState.pendingFiles;
|
|
2158
|
+
attachmentState.setUploadProgress({
|
|
2159
|
+
phase: "uploading",
|
|
2160
|
+
uploaded: 0,
|
|
2161
|
+
total: files.length
|
|
2162
|
+
});
|
|
2163
|
+
const ids = [];
|
|
2164
|
+
for (let i = 0; i < files.length; i++) {
|
|
2165
|
+
const pf = files[i];
|
|
2166
|
+
const attachmentId = pf.uploadedAttachmentId ?? (await upload(pf.file, { filename: pf.file.name })).id;
|
|
2167
|
+
if (!pf.uploadedAttachmentId) {
|
|
2168
|
+
attachmentState.markPendingUploaded(pf.clientId, attachmentId);
|
|
2169
|
+
}
|
|
2170
|
+
ids.push(attachmentId);
|
|
2171
|
+
attachmentState.setUploadProgress({
|
|
2172
|
+
phase: "uploading",
|
|
2173
|
+
uploaded: i + 1,
|
|
2174
|
+
total: files.length
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
attachmentState.setUploadProgress({
|
|
2178
|
+
phase: "done",
|
|
2179
|
+
uploaded: files.length,
|
|
2180
|
+
total: files.length
|
|
2181
|
+
});
|
|
2182
|
+
return ids;
|
|
2183
|
+
};
|
|
1523
2184
|
const handleSubmit = async () => {
|
|
1524
2185
|
if (!target || !isValid || isSubmitting) {
|
|
1525
2186
|
return;
|
|
@@ -1527,6 +2188,10 @@ function IssueReportModal() {
|
|
|
1527
2188
|
setSubmitError(null);
|
|
1528
2189
|
try {
|
|
1529
2190
|
const noteForSubmit = normalizedNote;
|
|
2191
|
+
let newAttachmentIds = [];
|
|
2192
|
+
if (canUseAttachments && attachmentState.pendingFiles.length > 0) {
|
|
2193
|
+
newAttachmentIds = await uploadPendingFiles();
|
|
2194
|
+
}
|
|
1530
2195
|
const effectiveVoiceMetadata = effectiveInputMode === "voice" ? voiceSubmitMetadata ?? {
|
|
1531
2196
|
provider: voice.provider,
|
|
1532
2197
|
token: "",
|
|
@@ -1551,24 +2216,37 @@ function IssueReportModal() {
|
|
|
1551
2216
|
}
|
|
1552
2217
|
} : target,
|
|
1553
2218
|
note: noteForSubmit,
|
|
1554
|
-
reporter_role_hint: reporterRoleHint
|
|
2219
|
+
reporter_role_hint: reporterRoleHint,
|
|
2220
|
+
attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0
|
|
1555
2221
|
});
|
|
1556
2222
|
} else if (mode === "edit" && issue) {
|
|
2223
|
+
const removedIds = [...attachmentState.removedExistingIds];
|
|
1557
2224
|
await updateMutation.mutateAsync({
|
|
1558
2225
|
issueReportId: issue.id,
|
|
1559
|
-
note: noteForSubmit
|
|
2226
|
+
note: noteForSubmit,
|
|
2227
|
+
add_attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0,
|
|
2228
|
+
remove_attachment_ids: removedIds.length > 0 ? removedIds : void 0
|
|
1560
2229
|
});
|
|
1561
2230
|
} else if (mode === "reply" && issue) {
|
|
1562
2231
|
await replyMutation.mutateAsync({
|
|
1563
2232
|
issueReportId: issue.id,
|
|
1564
2233
|
note: noteForSubmit,
|
|
1565
|
-
reporterRoleHint
|
|
2234
|
+
reporterRoleHint,
|
|
2235
|
+
attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0
|
|
1566
2236
|
});
|
|
1567
2237
|
}
|
|
1568
2238
|
resetVoiceCapture();
|
|
2239
|
+
attachmentState.reset();
|
|
1569
2240
|
closeModal();
|
|
1570
2241
|
} catch (submissionError) {
|
|
1571
|
-
|
|
2242
|
+
const message = resolveErrorMessage(
|
|
2243
|
+
submissionError,
|
|
2244
|
+
"Failed to submit issue report"
|
|
2245
|
+
);
|
|
2246
|
+
attachmentState.setUploadProgress(
|
|
2247
|
+
(current) => current.phase === "uploading" ? { ...current, phase: "error", error: message } : current
|
|
2248
|
+
);
|
|
2249
|
+
setSubmitError(message);
|
|
1572
2250
|
}
|
|
1573
2251
|
};
|
|
1574
2252
|
return /* @__PURE__ */ jsx2(Dialog.Root, { open: isOpen, onOpenChange: (open) => !open && handleCloseModal(), children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
|
|
@@ -1577,7 +2255,7 @@ function IssueReportModal() {
|
|
|
1577
2255
|
Dialog.Content,
|
|
1578
2256
|
{
|
|
1579
2257
|
className: cn(
|
|
1580
|
-
"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",
|
|
2258
|
+
"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",
|
|
1581
2259
|
Z_MODAL_CONTENT,
|
|
1582
2260
|
MODAL_WIDTH,
|
|
1583
2261
|
MODAL_RADIUS,
|
|
@@ -1603,6 +2281,7 @@ function IssueReportModal() {
|
|
|
1603
2281
|
error,
|
|
1604
2282
|
canUseVoice,
|
|
1605
2283
|
canUseText,
|
|
2284
|
+
canUseAttachments,
|
|
1606
2285
|
effectiveInputMode,
|
|
1607
2286
|
note,
|
|
1608
2287
|
normalizedNote,
|
|
@@ -1616,6 +2295,11 @@ function IssueReportModal() {
|
|
|
1616
2295
|
voiceError,
|
|
1617
2296
|
scribeError,
|
|
1618
2297
|
submitError,
|
|
2298
|
+
existingAttachments,
|
|
2299
|
+
removedExistingIds: attachmentState.removedExistingIds,
|
|
2300
|
+
pendingFiles: attachmentState.pendingFiles,
|
|
2301
|
+
uploadProgress: attachmentState.uploadProgress,
|
|
2302
|
+
attachmentValidationErrors: attachmentState.validationErrors,
|
|
1619
2303
|
onRetryHydration: () => void retryModalHydration(),
|
|
1620
2304
|
onClose: handleCloseModal,
|
|
1621
2305
|
onSelectText: () => setInputMode("text"),
|
|
@@ -1624,7 +2308,10 @@ function IssueReportModal() {
|
|
|
1624
2308
|
onStopVoiceInput: handleStopVoiceInput,
|
|
1625
2309
|
onAppendTranscript: handleAppendTranscript,
|
|
1626
2310
|
onNoteChange: setNote,
|
|
1627
|
-
onSubmit: () => void handleSubmit()
|
|
2311
|
+
onSubmit: () => void handleSubmit(),
|
|
2312
|
+
onAddFiles: attachmentState.addFiles,
|
|
2313
|
+
onRemoveExistingAttachment: attachmentState.removeExisting,
|
|
2314
|
+
onRemovePendingAttachment: attachmentState.removePending
|
|
1628
2315
|
}
|
|
1629
2316
|
),
|
|
1630
2317
|
/* @__PURE__ */ jsx2(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx2(
|
|
@@ -1651,33 +2338,44 @@ function FloatingIssueReportButton({
|
|
|
1651
2338
|
isReportMode,
|
|
1652
2339
|
isPopoverOpen,
|
|
1653
2340
|
openPopover,
|
|
1654
|
-
closePopover
|
|
2341
|
+
closePopover,
|
|
2342
|
+
needsResponseIssueIds
|
|
1655
2343
|
} = useIssueReporting();
|
|
1656
2344
|
const status = useIssueReportingStatus();
|
|
1657
2345
|
const entryPointState = getEntryPointState(status.data);
|
|
2346
|
+
const needsResponse = needsResponseIssueIds.length > 0;
|
|
1658
2347
|
if (!isEligible) {
|
|
1659
2348
|
return null;
|
|
1660
2349
|
}
|
|
1661
2350
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1662
2351
|
/* @__PURE__ */ jsx2(IssueReportModeBanner, {}),
|
|
1663
|
-
!isReportMode ? /* @__PURE__ */ jsx2("div", { className: cn("fixed bottom-12 right-4", Z_FLOATING_BUTTON, positionClassName), children: /* @__PURE__ */ jsx2(IssueReportPopover, { children: /* @__PURE__ */
|
|
2352
|
+
!isReportMode ? /* @__PURE__ */ jsx2("div", { className: cn("fixed bottom-12 right-4", Z_FLOATING_BUTTON, positionClassName), children: /* @__PURE__ */ jsx2(IssueReportPopover, { children: /* @__PURE__ */ jsxs(
|
|
1664
2353
|
"button",
|
|
1665
2354
|
{
|
|
1666
2355
|
type: "button",
|
|
1667
|
-
"aria-label": copy.entryAriaLabel,
|
|
2356
|
+
"aria-label": needsResponse ? `${copy.entryAriaLabel} (${copy.threadNeedsResponseBadge})` : copy.entryAriaLabel,
|
|
1668
2357
|
onClick: () => isPopoverOpen ? closePopover() : openPopover(),
|
|
1669
2358
|
className: cn(
|
|
1670
|
-
"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",
|
|
2359
|
+
"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",
|
|
1671
2360
|
status.isPending && "animate-pulse",
|
|
1672
2361
|
className
|
|
1673
2362
|
),
|
|
1674
|
-
children:
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
2363
|
+
children: [
|
|
2364
|
+
/* @__PURE__ */ jsx2(
|
|
2365
|
+
BugBeetle,
|
|
2366
|
+
{
|
|
2367
|
+
className: cn("h-6 w-6", getEntryPointClassName(entryPointState)),
|
|
2368
|
+
weight: "fill"
|
|
2369
|
+
}
|
|
2370
|
+
),
|
|
2371
|
+
needsResponse ? /* @__PURE__ */ jsx2(
|
|
2372
|
+
"span",
|
|
2373
|
+
{
|
|
2374
|
+
"data-testid": "issue-report-needs-response-badge",
|
|
2375
|
+
className: "absolute -right-0.5 -top-0.5 h-3 w-3 rounded-full border-2 border-white bg-amber-500"
|
|
2376
|
+
}
|
|
2377
|
+
) : null
|
|
2378
|
+
]
|
|
1681
2379
|
}
|
|
1682
2380
|
) }) }) : null,
|
|
1683
2381
|
/* @__PURE__ */ jsx2(IssueReportModal, {})
|
|
@@ -1729,6 +2427,7 @@ function ReportableSection({
|
|
|
1729
2427
|
}
|
|
1730
2428
|
export {
|
|
1731
2429
|
FloatingIssueReportButton,
|
|
2430
|
+
IssueReportMessageThread,
|
|
1732
2431
|
IssueReportingPageConfig,
|
|
1733
2432
|
IssueReportingProvider,
|
|
1734
2433
|
ReportModeContext,
|
|
@@ -1742,9 +2441,13 @@ export {
|
|
|
1742
2441
|
getIssueStatusClassName,
|
|
1743
2442
|
isClosedIssueStatus,
|
|
1744
2443
|
isOpenIssueStatus,
|
|
2444
|
+
isReporterMessageConflict,
|
|
1745
2445
|
issueReportingKeys,
|
|
2446
|
+
selectReporterVisibleMessages,
|
|
1746
2447
|
useIssueReporting,
|
|
1747
2448
|
useIssueReportingHistory,
|
|
2449
|
+
useIssueReportingMessageMutation,
|
|
2450
|
+
useIssueReportingMessages,
|
|
1748
2451
|
useIssueReportingMutations,
|
|
1749
2452
|
useIssueReportingStatus,
|
|
1750
2453
|
useReportMode
|