@tra-bilisim/report-issue 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/AnnotationEditor-ILMYBTOG.js +379 -0
- package/dist/adapters-axios.d.ts +19 -0
- package/dist/adapters-axios.js +52 -0
- package/dist/chunk-5S66KGBW.js +118 -0
- package/dist/chunk-EXDFVVYA.js +73 -0
- package/dist/chunk-JMQUG5Q7.js +99 -0
- package/dist/chunk-KY2IRP36.js +102 -0
- package/dist/chunk-ZYF6UFBB.js +162 -0
- package/dist/consent-DmS4DxOf.d.ts +135 -0
- package/dist/core.d.ts +16 -0
- package/dist/core.js +4 -0
- package/dist/index.css +594 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +687 -0
- package/dist/screenshot-BQPXCSLD.js +2 -0
- package/package.json +69 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import { createVideoRecorder, CONSENT_VERSION } from './chunk-5S66KGBW.js';
|
|
2
|
+
export { CONSENT_VERSION } from './chunk-5S66KGBW.js';
|
|
3
|
+
import { cn, useReportIssueConfig, Button, useReportIssue } from './chunk-JMQUG5Q7.js';
|
|
4
|
+
export { ReportIssueProvider, useReportIssue, useReportIssueConfig } from './chunk-JMQUG5Q7.js';
|
|
5
|
+
import { getConsoleLogs, getNetworkLogs } from './chunk-EXDFVVYA.js';
|
|
6
|
+
export { getConsoleLogs, getNetworkLogs, patchConsole } from './chunk-EXDFVVYA.js';
|
|
7
|
+
import { toggleVideoMask } from './chunk-ZYF6UFBB.js';
|
|
8
|
+
import { createPortal } from 'react-dom';
|
|
9
|
+
import { X, ShieldCheck, Camera, Video, Film, FileText, Circle, Square, MessageSquareWarning, Check, ChevronDown } from 'lucide-react';
|
|
10
|
+
import { forwardRef, lazy, useCallback, useState, useRef, useEffect, useMemo, Suspense } from 'react';
|
|
11
|
+
import * as RadixDialog from '@radix-ui/react-dialog';
|
|
12
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
13
|
+
import * as RadixCheckbox from '@radix-ui/react-checkbox';
|
|
14
|
+
import * as Popover from '@radix-ui/react-popover';
|
|
15
|
+
|
|
16
|
+
var Dialog = RadixDialog.Root;
|
|
17
|
+
var DialogContent = forwardRef(
|
|
18
|
+
({ size = "md", showClose = true, className, children, ...rest }, ref) => /* @__PURE__ */ jsxs(RadixDialog.Portal, { children: [
|
|
19
|
+
/* @__PURE__ */ jsx(RadixDialog.Overlay, { className: "rpi-dialog__overlay", "data-slot": "dialog-overlay" }),
|
|
20
|
+
/* @__PURE__ */ jsxs(
|
|
21
|
+
RadixDialog.Content,
|
|
22
|
+
{
|
|
23
|
+
ref,
|
|
24
|
+
className: cn(
|
|
25
|
+
"rpi-root",
|
|
26
|
+
"rpi-dialog__content",
|
|
27
|
+
size === "lg" && "rpi-dialog__content--lg",
|
|
28
|
+
size === "xl" && "rpi-dialog__content--xl",
|
|
29
|
+
className
|
|
30
|
+
),
|
|
31
|
+
...rest,
|
|
32
|
+
children: [
|
|
33
|
+
children,
|
|
34
|
+
showClose && /* @__PURE__ */ jsx(RadixDialog.Close, { className: "rpi-dialog__close", "aria-label": "Close", children: /* @__PURE__ */ jsx(X, { size: 16 }) })
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
] })
|
|
39
|
+
);
|
|
40
|
+
DialogContent.displayName = "RpiDialogContent";
|
|
41
|
+
var DialogHeader = ({ children }) => /* @__PURE__ */ jsx("div", { className: "rpi-dialog__header", children });
|
|
42
|
+
var DialogTitle = ({ children, className }) => /* @__PURE__ */ jsx(RadixDialog.Title, { className: cn("rpi-dialog__title", className), children });
|
|
43
|
+
var DialogDescription = ({ children }) => /* @__PURE__ */ jsx(RadixDialog.Description, { className: "rpi-dialog__desc", children });
|
|
44
|
+
var DialogFooter = ({ children }) => /* @__PURE__ */ jsx("div", { className: "rpi-dialog__footer", children });
|
|
45
|
+
var Input = forwardRef(
|
|
46
|
+
({ textarea, className, rows, ...rest }, ref) => {
|
|
47
|
+
if (textarea) {
|
|
48
|
+
return /* @__PURE__ */ jsx(
|
|
49
|
+
"textarea",
|
|
50
|
+
{
|
|
51
|
+
ref,
|
|
52
|
+
rows,
|
|
53
|
+
className: cn("rpi-textarea", className),
|
|
54
|
+
...rest
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return /* @__PURE__ */ jsx(
|
|
59
|
+
"input",
|
|
60
|
+
{
|
|
61
|
+
ref,
|
|
62
|
+
className: cn("rpi-input", className),
|
|
63
|
+
...rest
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
Input.displayName = "RpiInput";
|
|
69
|
+
var Checkbox = ({ id, checked, onCheckedChange, label }) => /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "rpi-check", children: [
|
|
70
|
+
/* @__PURE__ */ jsx(
|
|
71
|
+
RadixCheckbox.Root,
|
|
72
|
+
{
|
|
73
|
+
id,
|
|
74
|
+
checked,
|
|
75
|
+
onCheckedChange: (value) => onCheckedChange?.(value === true),
|
|
76
|
+
className: "rpi-check__box",
|
|
77
|
+
children: /* @__PURE__ */ jsx(RadixCheckbox.Indicator, { children: /* @__PURE__ */ jsx(Check, { size: 13, strokeWidth: 3 }) })
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
label != null && /* @__PURE__ */ jsx("span", { className: "rpi-check__label", children: label })
|
|
81
|
+
] });
|
|
82
|
+
var Select = ({
|
|
83
|
+
options,
|
|
84
|
+
value,
|
|
85
|
+
onChange,
|
|
86
|
+
placeholder,
|
|
87
|
+
isSearchable = false,
|
|
88
|
+
disabled = false,
|
|
89
|
+
t = (k) => k
|
|
90
|
+
}) => {
|
|
91
|
+
const [open, setOpen] = useState(false);
|
|
92
|
+
const [query, setQuery] = useState("");
|
|
93
|
+
const searchRef = useRef(null);
|
|
94
|
+
const selected = options.find((o) => o.value === value);
|
|
95
|
+
const filtered = useMemo(() => {
|
|
96
|
+
if (!isSearchable || !query.trim()) return options;
|
|
97
|
+
const q = query.toLowerCase();
|
|
98
|
+
return options.filter((o) => o.label.toLowerCase().includes(q));
|
|
99
|
+
}, [options, query, isSearchable]);
|
|
100
|
+
const commit = (next) => {
|
|
101
|
+
onChange(next === value ? void 0 : next);
|
|
102
|
+
setOpen(false);
|
|
103
|
+
setQuery("");
|
|
104
|
+
};
|
|
105
|
+
return /* @__PURE__ */ jsxs(
|
|
106
|
+
Popover.Root,
|
|
107
|
+
{
|
|
108
|
+
open,
|
|
109
|
+
onOpenChange: (next) => {
|
|
110
|
+
if (disabled) return;
|
|
111
|
+
setOpen(next);
|
|
112
|
+
if (!next) setQuery("");
|
|
113
|
+
},
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
116
|
+
"button",
|
|
117
|
+
{
|
|
118
|
+
type: "button",
|
|
119
|
+
disabled,
|
|
120
|
+
"data-placeholder": !selected,
|
|
121
|
+
className: "rpi-select__trigger",
|
|
122
|
+
children: [
|
|
123
|
+
/* @__PURE__ */ jsx("span", { children: selected ? selected.label : placeholder ?? "" }),
|
|
124
|
+
/* @__PURE__ */ jsx(ChevronDown, { size: 16 })
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
) }),
|
|
128
|
+
/* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(
|
|
129
|
+
Popover.Content,
|
|
130
|
+
{
|
|
131
|
+
className: "rpi-root rpi-select__panel",
|
|
132
|
+
align: "start",
|
|
133
|
+
sideOffset: 4,
|
|
134
|
+
onOpenAutoFocus: (e) => {
|
|
135
|
+
if (isSearchable) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
searchRef.current?.focus();
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
children: [
|
|
141
|
+
isSearchable && /* @__PURE__ */ jsx(
|
|
142
|
+
"input",
|
|
143
|
+
{
|
|
144
|
+
ref: searchRef,
|
|
145
|
+
value: query,
|
|
146
|
+
onChange: (e) => setQuery(e.target.value),
|
|
147
|
+
placeholder: t("Search"),
|
|
148
|
+
className: "rpi-select__search"
|
|
149
|
+
}
|
|
150
|
+
),
|
|
151
|
+
/* @__PURE__ */ jsx("div", { className: "rpi-select__list", children: filtered.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rpi-select__empty", children: t("No results") }) : filtered.map((o) => /* @__PURE__ */ jsx(
|
|
152
|
+
"button",
|
|
153
|
+
{
|
|
154
|
+
type: "button",
|
|
155
|
+
"data-selected": o.value === value,
|
|
156
|
+
className: cn("rpi-select__option"),
|
|
157
|
+
onClick: () => commit(o.value),
|
|
158
|
+
children: o.label
|
|
159
|
+
},
|
|
160
|
+
String(o.value)
|
|
161
|
+
)) })
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
) })
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
function useReportIssueCapture() {
|
|
170
|
+
const collectConsoleLogs = useCallback(() => getConsoleLogs(), []);
|
|
171
|
+
const collectNetworkLogs = useCallback(() => getNetworkLogs(), []);
|
|
172
|
+
return { collectConsoleLogs, collectNetworkLogs };
|
|
173
|
+
}
|
|
174
|
+
function useVideoRecorder({ onComplete, onRecordingChange, maxDurationMs }) {
|
|
175
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
176
|
+
const [elapsed, setElapsed] = useState(0);
|
|
177
|
+
const controllerRef = useRef(null);
|
|
178
|
+
const onCompleteRef = useRef(onComplete);
|
|
179
|
+
const onRecordingChangeRef = useRef(onRecordingChange);
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
onCompleteRef.current = onComplete;
|
|
182
|
+
}, [onComplete]);
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
onRecordingChangeRef.current = onRecordingChange;
|
|
185
|
+
}, [onRecordingChange]);
|
|
186
|
+
if (!controllerRef.current) {
|
|
187
|
+
controllerRef.current = createVideoRecorder(
|
|
188
|
+
{
|
|
189
|
+
onComplete: (file) => onCompleteRef.current(file),
|
|
190
|
+
onStateChange: (next) => {
|
|
191
|
+
setIsRecording(next);
|
|
192
|
+
onRecordingChangeRef.current?.(next);
|
|
193
|
+
},
|
|
194
|
+
onTick: (seconds) => setElapsed(seconds)
|
|
195
|
+
},
|
|
196
|
+
{ maxDurationMs }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
useEffect(() => () => controllerRef.current?.dispose(), []);
|
|
200
|
+
const start = useCallback(
|
|
201
|
+
() => controllerRef.current.start(),
|
|
202
|
+
[]
|
|
203
|
+
);
|
|
204
|
+
const stop = useCallback(() => controllerRef.current.stop(), []);
|
|
205
|
+
return {
|
|
206
|
+
isRecording,
|
|
207
|
+
elapsed,
|
|
208
|
+
maxDuration: controllerRef.current.maxDurationSeconds,
|
|
209
|
+
start,
|
|
210
|
+
stop
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
var RecordingConsentDialog = ({ open, captureType = "video", onOpenChange, onAccept }) => {
|
|
214
|
+
const { t } = useReportIssueConfig();
|
|
215
|
+
const [checked, setChecked] = useState(false);
|
|
216
|
+
const isScreenshot = captureType === "screenshot";
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (open) setChecked(false);
|
|
219
|
+
}, [open]);
|
|
220
|
+
const handleAccept = () => {
|
|
221
|
+
if (!checked) return;
|
|
222
|
+
onAccept({ accepted: true, version: CONSENT_VERSION, acceptedAt: (/* @__PURE__ */ new Date()).toISOString(), captureType });
|
|
223
|
+
};
|
|
224
|
+
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { "data-report-issue-dialog": true, "data-report-ignore-capture": true, size: "lg", children: [
|
|
225
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
226
|
+
/* @__PURE__ */ jsxs(DialogTitle, { children: [
|
|
227
|
+
/* @__PURE__ */ jsx(ShieldCheck, { size: 20, className: "rpi-icon-primary" }),
|
|
228
|
+
isScreenshot ? t("Screenshot Consent") : t("Screen Recording Consent")
|
|
229
|
+
] }),
|
|
230
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: isScreenshot ? t("Before the screenshot is taken, please review which data is collected and give your consent.") : t("Before the recording starts, please review which data is collected and give your consent.") })
|
|
231
|
+
] }),
|
|
232
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-consent__body", children: [
|
|
233
|
+
/* @__PURE__ */ jsx("p", { children: isScreenshot ? t("To diagnose the issue you reported, a screenshot of this application screen will be captured. Sensitive on-screen data is masked before capture.") : t("To diagnose the issue you reported, a short screen recording of only this tab will be captured. All on-screen data is masked during recording; you may temporarily reveal it.") }),
|
|
234
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-consent__group", children: [
|
|
235
|
+
/* @__PURE__ */ jsx("span", { style: { fontWeight: 500 }, children: t("Data that will be collected:") }),
|
|
236
|
+
/* @__PURE__ */ jsxs("ul", { className: "rpi-consent__list", children: [
|
|
237
|
+
/* @__PURE__ */ jsx("li", { children: isScreenshot ? t("A masked screenshot of this application screen") : t("A masked screen recording limited to this application tab") }),
|
|
238
|
+
/* @__PURE__ */ jsx("li", { children: t("Console and network logs of this session") }),
|
|
239
|
+
/* @__PURE__ */ jsx("li", { children: t("Session metadata (user, roles, browser, page)") })
|
|
240
|
+
] })
|
|
241
|
+
] }),
|
|
242
|
+
/* @__PURE__ */ jsx("p", { className: "rpi-muted", children: t("This data is used solely to diagnose and resolve the reported issue and is shared only with the relevant technical team. You may withdraw your consent by not submitting the report.") })
|
|
243
|
+
] }),
|
|
244
|
+
/* @__PURE__ */ jsx(
|
|
245
|
+
Checkbox,
|
|
246
|
+
{
|
|
247
|
+
id: "rpi-recording-consent",
|
|
248
|
+
checked,
|
|
249
|
+
onCheckedChange: setChecked,
|
|
250
|
+
label: t("I have read the above and give my explicit consent to the processing and sharing of this data.")
|
|
251
|
+
}
|
|
252
|
+
),
|
|
253
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
254
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: t("Cancel") }),
|
|
255
|
+
/* @__PURE__ */ jsx(Button, { disabled: !checked, onClick: handleAccept, children: isScreenshot ? t("Take Screenshot") : t("Start Recording") })
|
|
256
|
+
] })
|
|
257
|
+
] }) });
|
|
258
|
+
};
|
|
259
|
+
var RecordingConsentDialog_default = RecordingConsentDialog;
|
|
260
|
+
var AnnotationEditor = lazy(() => import('./AnnotationEditor-ILMYBTOG.js'));
|
|
261
|
+
var getAppOrigin = () => typeof window !== "undefined" ? window.location.origin : "";
|
|
262
|
+
var toAbsoluteUrl = (value) => {
|
|
263
|
+
if (!value) return "";
|
|
264
|
+
try {
|
|
265
|
+
return new URL(value, getAppOrigin()).href;
|
|
266
|
+
} catch {
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
var ReportIssueDialog = ({ open, onOpenChange }) => {
|
|
271
|
+
const {
|
|
272
|
+
config,
|
|
273
|
+
setIsCapturing,
|
|
274
|
+
isRecording,
|
|
275
|
+
setIsRecording,
|
|
276
|
+
setIsVideoMaskEnabled
|
|
277
|
+
} = useReportIssue();
|
|
278
|
+
const {
|
|
279
|
+
adapter,
|
|
280
|
+
t,
|
|
281
|
+
locale,
|
|
282
|
+
toast,
|
|
283
|
+
environmentResolver,
|
|
284
|
+
getCurrentUrl,
|
|
285
|
+
maxFiles,
|
|
286
|
+
maxFileSizeBytes,
|
|
287
|
+
maxRecordingSeconds
|
|
288
|
+
} = config;
|
|
289
|
+
const { collectConsoleLogs, collectNetworkLogs } = useReportIssueCapture();
|
|
290
|
+
const [view, setView] = useState("form");
|
|
291
|
+
const [editorImage, setEditorImage] = useState(null);
|
|
292
|
+
const [consentOpen, setConsentOpen] = useState(false);
|
|
293
|
+
const consentRef = useRef(null);
|
|
294
|
+
const [screenshotConsentOpen, setScreenshotConsentOpen] = useState(false);
|
|
295
|
+
const screenshotConsentRef = useRef(null);
|
|
296
|
+
const [attachments, setAttachments] = useState([]);
|
|
297
|
+
const [useCurrentPage, setUseCurrentPage] = useState(true);
|
|
298
|
+
const [customPage, setCustomPage] = useState("");
|
|
299
|
+
const [category, setCategory] = useState(null);
|
|
300
|
+
const [categoryOptions, setCategoryOptions] = useState([]);
|
|
301
|
+
const [description, setDescription] = useState("");
|
|
302
|
+
const [submitting, setSubmitting] = useState(false);
|
|
303
|
+
const fileInputRef = useRef(null);
|
|
304
|
+
const currentUrl = getCurrentUrl();
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (!open || !adapter.getCategories) return;
|
|
307
|
+
let active = true;
|
|
308
|
+
Promise.resolve(adapter.getCategories()).then((list) => {
|
|
309
|
+
if (active) setCategoryOptions(Array.isArray(list) ? list : []);
|
|
310
|
+
}).catch(() => void 0);
|
|
311
|
+
return () => {
|
|
312
|
+
active = false;
|
|
313
|
+
};
|
|
314
|
+
}, [open, adapter]);
|
|
315
|
+
const localizedCategoryOptions = useMemo(
|
|
316
|
+
() => [...categoryOptions].map((o) => ({ label: t(o.label), value: o.value })).sort((a, b) => a.label.localeCompare(b.label, locale, { sensitivity: "base" })),
|
|
317
|
+
[categoryOptions, locale, t]
|
|
318
|
+
);
|
|
319
|
+
const pageOptions = useMemo(() => {
|
|
320
|
+
const raw = typeof adapter.pageOptions === "function" ? adapter.pageOptions() : adapter.pageOptions;
|
|
321
|
+
return (raw ?? []).slice().sort((a, b) => a.label.localeCompare(b.label));
|
|
322
|
+
}, [adapter]);
|
|
323
|
+
const detectKind = (file) => {
|
|
324
|
+
if (file.type.startsWith("image/")) return "image";
|
|
325
|
+
if (file.type.startsWith("video/")) return "video";
|
|
326
|
+
return "other";
|
|
327
|
+
};
|
|
328
|
+
const addFiles = useCallback((files, origin = "upload") => {
|
|
329
|
+
setAttachments((prev) => {
|
|
330
|
+
const next = [...prev];
|
|
331
|
+
let limitHit = false;
|
|
332
|
+
files.forEach((file) => {
|
|
333
|
+
if (next.length >= maxFiles) {
|
|
334
|
+
limitHit = true;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (file.size > maxFileSizeBytes) {
|
|
338
|
+
toast.error(t("{name} exceeds the size limit.").replace("{name}", file.name));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
next.push({ id: `${file.name}-${Date.now()}-${Math.random()}`, file, url: URL.createObjectURL(file), kind: detectKind(file), origin });
|
|
342
|
+
});
|
|
343
|
+
if (limitHit) toast.error(t("You can attach at most {count} files.").replace("{count}", String(maxFiles)));
|
|
344
|
+
return next;
|
|
345
|
+
});
|
|
346
|
+
}, [maxFiles, maxFileSizeBytes, t, toast]);
|
|
347
|
+
const removeAttachment = (id) => {
|
|
348
|
+
setAttachments((prev) => {
|
|
349
|
+
const target = prev.find((a) => a.id === id);
|
|
350
|
+
if (target) URL.revokeObjectURL(target.url);
|
|
351
|
+
return prev.filter((a) => a.id !== id);
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
const captureScreenshot = useCallback(async () => {
|
|
355
|
+
setIsCapturing(true);
|
|
356
|
+
try {
|
|
357
|
+
const { captureMaskedScreenshot } = await import('./screenshot-BQPXCSLD.js');
|
|
358
|
+
const dataUrl = await captureMaskedScreenshot();
|
|
359
|
+
setEditorImage(dataUrl);
|
|
360
|
+
await new Promise((resolve) => {
|
|
361
|
+
requestAnimationFrame(() => {
|
|
362
|
+
requestAnimationFrame(() => resolve());
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
setView("editor");
|
|
366
|
+
} catch {
|
|
367
|
+
toast.error(t("Failed to capture screenshot."));
|
|
368
|
+
} finally {
|
|
369
|
+
setIsCapturing(false);
|
|
370
|
+
}
|
|
371
|
+
}, [setIsCapturing, t, toast]);
|
|
372
|
+
const handleScreenshotConsentAccept = useCallback((consent) => {
|
|
373
|
+
screenshotConsentRef.current = consent;
|
|
374
|
+
setScreenshotConsentOpen(false);
|
|
375
|
+
captureScreenshot();
|
|
376
|
+
}, [captureScreenshot]);
|
|
377
|
+
const handleEditorConfirm = useCallback((file) => {
|
|
378
|
+
addFiles([file], "screenshot");
|
|
379
|
+
setEditorImage(null);
|
|
380
|
+
setView("form");
|
|
381
|
+
}, [addFiles]);
|
|
382
|
+
const handleEditorCancel = useCallback(() => {
|
|
383
|
+
setEditorImage(null);
|
|
384
|
+
setView("form");
|
|
385
|
+
}, []);
|
|
386
|
+
const handleRecordingChange = useCallback((next) => {
|
|
387
|
+
setIsRecording(next);
|
|
388
|
+
if (!next) {
|
|
389
|
+
setIsVideoMaskEnabled(false);
|
|
390
|
+
toggleVideoMask(false);
|
|
391
|
+
}
|
|
392
|
+
}, [setIsRecording, setIsVideoMaskEnabled]);
|
|
393
|
+
const { isRecording: recording, elapsed, maxDuration, start, stop } = useVideoRecorder({
|
|
394
|
+
onComplete: (file) => {
|
|
395
|
+
addFiles([file], "recording");
|
|
396
|
+
onOpenChange(true);
|
|
397
|
+
},
|
|
398
|
+
onRecordingChange: handleRecordingChange,
|
|
399
|
+
maxDurationMs: maxRecordingSeconds * 1e3
|
|
400
|
+
});
|
|
401
|
+
const handleStartRecording = useCallback(async () => {
|
|
402
|
+
setIsVideoMaskEnabled(true);
|
|
403
|
+
toggleVideoMask(true);
|
|
404
|
+
onOpenChange(false);
|
|
405
|
+
const result = await start();
|
|
406
|
+
if (result !== "started") {
|
|
407
|
+
handleRecordingChange(false);
|
|
408
|
+
onOpenChange(true);
|
|
409
|
+
const message = result === "unsupported" ? t("Screen recording is only supported on Chrome and Edge browsers.") : result === "wrong-surface" ? t("Only this application tab can be recorded. Please share this tab.") : t("Screen recording permission was denied.");
|
|
410
|
+
toast.error(message);
|
|
411
|
+
}
|
|
412
|
+
}, [start, setIsVideoMaskEnabled, handleRecordingChange, onOpenChange, t, toast]);
|
|
413
|
+
const handleConsentAccept = useCallback((consent) => {
|
|
414
|
+
consentRef.current = consent;
|
|
415
|
+
setConsentOpen(false);
|
|
416
|
+
handleStartRecording();
|
|
417
|
+
}, [handleStartRecording]);
|
|
418
|
+
const resetForm = useCallback(() => {
|
|
419
|
+
setAttachments((prev) => {
|
|
420
|
+
prev.forEach((a) => URL.revokeObjectURL(a.url));
|
|
421
|
+
return [];
|
|
422
|
+
});
|
|
423
|
+
setUseCurrentPage(true);
|
|
424
|
+
setCustomPage("");
|
|
425
|
+
setCategory(null);
|
|
426
|
+
setDescription("");
|
|
427
|
+
setView("form");
|
|
428
|
+
setEditorImage(null);
|
|
429
|
+
consentRef.current = null;
|
|
430
|
+
screenshotConsentRef.current = null;
|
|
431
|
+
}, []);
|
|
432
|
+
const handleSubmit = useCallback(async () => {
|
|
433
|
+
const page = useCurrentPage ? currentUrl : toAbsoluteUrl(customPage.trim());
|
|
434
|
+
if (!page) {
|
|
435
|
+
toast.error(t("Please specify the page."));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
setSubmitting(true);
|
|
439
|
+
try {
|
|
440
|
+
const consoleLogs = collectConsoleLogs();
|
|
441
|
+
const networkLogs = collectNetworkLogs();
|
|
442
|
+
const environment = environmentResolver(page);
|
|
443
|
+
const hasRecording = attachments.some((a) => a.origin === "recording");
|
|
444
|
+
const hasScreenshot = attachments.some((a) => a.origin === "screenshot");
|
|
445
|
+
const recordingConsent = hasRecording ? consentRef.current : null;
|
|
446
|
+
const screenshotConsent = hasScreenshot ? screenshotConsentRef.current : null;
|
|
447
|
+
const baseMetadata = {
|
|
448
|
+
userAgent: navigator.userAgent,
|
|
449
|
+
screenResolution: `${window.screen.width}x${window.screen.height}`,
|
|
450
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
451
|
+
};
|
|
452
|
+
const provided = adapter.getMetadata ? await adapter.getMetadata() : {};
|
|
453
|
+
const metadata = { ...baseMetadata, ...provided };
|
|
454
|
+
const payload = {
|
|
455
|
+
files: attachments.map((a) => a.file),
|
|
456
|
+
page,
|
|
457
|
+
environment,
|
|
458
|
+
categoryId: category,
|
|
459
|
+
description: description.trim() || null,
|
|
460
|
+
consoleLogs,
|
|
461
|
+
networkLogs,
|
|
462
|
+
recordingConsent,
|
|
463
|
+
screenshotConsent,
|
|
464
|
+
metadata
|
|
465
|
+
};
|
|
466
|
+
const formData = new FormData();
|
|
467
|
+
attachments.forEach((a) => formData.append("files", a.file, a.file.name));
|
|
468
|
+
formData.append("page", page);
|
|
469
|
+
formData.append("environment", environment);
|
|
470
|
+
if (category) formData.append("categoryId", String(category));
|
|
471
|
+
if (description.trim()) formData.append("description", description.trim());
|
|
472
|
+
formData.append("consoleLogs", JSON.stringify(consoleLogs));
|
|
473
|
+
formData.append("networkLogs", JSON.stringify(networkLogs));
|
|
474
|
+
if (recordingConsent) formData.append("recordingConsent", JSON.stringify(recordingConsent));
|
|
475
|
+
if (screenshotConsent) formData.append("screenshotConsent", JSON.stringify(screenshotConsent));
|
|
476
|
+
formData.append("metadata", JSON.stringify(metadata));
|
|
477
|
+
const result = await adapter.submit(payload, formData);
|
|
478
|
+
if (result.ok) {
|
|
479
|
+
toast.success(t(result.message ?? "Your report has been submitted. Thank you!"));
|
|
480
|
+
resetForm();
|
|
481
|
+
onOpenChange(false);
|
|
482
|
+
} else {
|
|
483
|
+
toast.error(t(result.message ?? "An error occurred!"));
|
|
484
|
+
}
|
|
485
|
+
} catch {
|
|
486
|
+
toast.error(t("An error occurred!"));
|
|
487
|
+
} finally {
|
|
488
|
+
setSubmitting(false);
|
|
489
|
+
}
|
|
490
|
+
}, [
|
|
491
|
+
attachments,
|
|
492
|
+
useCurrentPage,
|
|
493
|
+
customPage,
|
|
494
|
+
currentUrl,
|
|
495
|
+
category,
|
|
496
|
+
description,
|
|
497
|
+
adapter,
|
|
498
|
+
environmentResolver,
|
|
499
|
+
collectConsoleLogs,
|
|
500
|
+
collectNetworkLogs,
|
|
501
|
+
onOpenChange,
|
|
502
|
+
resetForm,
|
|
503
|
+
t,
|
|
504
|
+
toast
|
|
505
|
+
]);
|
|
506
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
507
|
+
/* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (val) => {
|
|
508
|
+
if (!recording) onOpenChange(val);
|
|
509
|
+
}, children: /* @__PURE__ */ jsx(
|
|
510
|
+
DialogContent,
|
|
511
|
+
{
|
|
512
|
+
"data-report-issue-dialog": true,
|
|
513
|
+
"data-report-ignore-capture": true,
|
|
514
|
+
showClose: false,
|
|
515
|
+
className: view === "editor" ? "rpi-dialog__content--editor" : void 0,
|
|
516
|
+
children: view === "form" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
517
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
518
|
+
/* @__PURE__ */ jsx(DialogTitle, { children: t("Report an Issue") }),
|
|
519
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: t("Describe the problem and attach screenshots or a screen recording.") })
|
|
520
|
+
] }),
|
|
521
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-form-body", children: [
|
|
522
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-col", children: [
|
|
523
|
+
/* @__PURE__ */ jsx("span", { className: "rpi-field-label", children: t("Attachments") }),
|
|
524
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-row-wrap", children: [
|
|
525
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => setScreenshotConsentOpen(true), children: [
|
|
526
|
+
/* @__PURE__ */ jsx(Camera, { size: 16 }),
|
|
527
|
+
t("Take a Screenshot")
|
|
528
|
+
] }),
|
|
529
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => setConsentOpen(true), children: [
|
|
530
|
+
/* @__PURE__ */ jsx(Video, { size: 16 }),
|
|
531
|
+
t("Record Video")
|
|
532
|
+
] }),
|
|
533
|
+
/* @__PURE__ */ jsx(
|
|
534
|
+
"input",
|
|
535
|
+
{
|
|
536
|
+
ref: fileInputRef,
|
|
537
|
+
type: "file",
|
|
538
|
+
multiple: true,
|
|
539
|
+
accept: "image/*,video/*",
|
|
540
|
+
style: { display: "none" },
|
|
541
|
+
onChange: (e) => {
|
|
542
|
+
if (e.target.files) addFiles(Array.from(e.target.files));
|
|
543
|
+
e.target.value = "";
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
)
|
|
547
|
+
] }),
|
|
548
|
+
attachments.length > 0 && /* @__PURE__ */ jsx("div", { className: "rpi-row-wrap", style: { marginTop: 4 }, children: attachments.map((a) => /* @__PURE__ */ jsxs("div", { className: "rpi-attach", children: [
|
|
549
|
+
a.kind === "image" && /* @__PURE__ */ jsx("img", { src: a.url, alt: a.file.name }),
|
|
550
|
+
a.kind === "video" && /* @__PURE__ */ jsx("video", { src: a.url }),
|
|
551
|
+
a.kind === "video" && /* @__PURE__ */ jsx(Film, { size: 24, className: "rpi-attach__badge" }),
|
|
552
|
+
a.kind === "other" && /* @__PURE__ */ jsx(FileText, { size: 28, className: "rpi-muted" }),
|
|
553
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => removeAttachment(a.id), className: "rpi-attach__remove", children: /* @__PURE__ */ jsx(X, { size: 12 }) })
|
|
554
|
+
] }, a.id)) })
|
|
555
|
+
] }),
|
|
556
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-col", children: [
|
|
557
|
+
/* @__PURE__ */ jsx("span", { className: "rpi-field-label", children: t("The page where the issue occurred") }),
|
|
558
|
+
/* @__PURE__ */ jsx(
|
|
559
|
+
Checkbox,
|
|
560
|
+
{
|
|
561
|
+
id: "rpi-use-current-page",
|
|
562
|
+
checked: useCurrentPage,
|
|
563
|
+
onCheckedChange: (val) => {
|
|
564
|
+
setUseCurrentPage(val);
|
|
565
|
+
if (val) setCustomPage("");
|
|
566
|
+
},
|
|
567
|
+
label: `${t("Current page")}: ${currentUrl}`
|
|
568
|
+
}
|
|
569
|
+
),
|
|
570
|
+
pageOptions.length > 0 && /* @__PURE__ */ jsx(
|
|
571
|
+
Select,
|
|
572
|
+
{
|
|
573
|
+
placeholder: t("Enter the page where the issue occurred"),
|
|
574
|
+
options: pageOptions,
|
|
575
|
+
value: customPage || void 0,
|
|
576
|
+
onChange: (val) => setCustomPage(typeof val === "string" ? val : ""),
|
|
577
|
+
isSearchable: true,
|
|
578
|
+
disabled: useCurrentPage,
|
|
579
|
+
t
|
|
580
|
+
}
|
|
581
|
+
)
|
|
582
|
+
] }),
|
|
583
|
+
localizedCategoryOptions.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rpi-col", children: [
|
|
584
|
+
/* @__PURE__ */ jsx("span", { className: "rpi-field-label", children: `${t("Category")} (${t("optional")})` }),
|
|
585
|
+
/* @__PURE__ */ jsx(
|
|
586
|
+
Select,
|
|
587
|
+
{
|
|
588
|
+
placeholder: t("Select"),
|
|
589
|
+
options: localizedCategoryOptions,
|
|
590
|
+
value: category ?? void 0,
|
|
591
|
+
onChange: (val) => setCategory(typeof val === "number" ? val : null),
|
|
592
|
+
t
|
|
593
|
+
}
|
|
594
|
+
)
|
|
595
|
+
] }),
|
|
596
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-col", children: [
|
|
597
|
+
/* @__PURE__ */ jsx("span", { className: "rpi-field-label", children: `${t("Description")} (${t("optional")})` }),
|
|
598
|
+
/* @__PURE__ */ jsx(
|
|
599
|
+
Input,
|
|
600
|
+
{
|
|
601
|
+
textarea: true,
|
|
602
|
+
rows: 3,
|
|
603
|
+
placeholder: t("Describe what happened..."),
|
|
604
|
+
value: description,
|
|
605
|
+
onChange: (e) => setDescription(e.target.value),
|
|
606
|
+
"data-report-mask-ignore": true
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
] })
|
|
610
|
+
] }),
|
|
611
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
612
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: t("Cancel") }),
|
|
613
|
+
/* @__PURE__ */ jsx(Button, { loading: submitting, onClick: handleSubmit, children: t("Submit") })
|
|
614
|
+
] })
|
|
615
|
+
] }) : editorImage && /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
|
|
616
|
+
AnnotationEditor,
|
|
617
|
+
{
|
|
618
|
+
imageDataUrl: editorImage,
|
|
619
|
+
onConfirm: handleEditorConfirm,
|
|
620
|
+
onCancel: handleEditorCancel
|
|
621
|
+
}
|
|
622
|
+
) })
|
|
623
|
+
}
|
|
624
|
+
) }),
|
|
625
|
+
/* @__PURE__ */ jsx(
|
|
626
|
+
RecordingConsentDialog_default,
|
|
627
|
+
{
|
|
628
|
+
open: consentOpen,
|
|
629
|
+
captureType: "video",
|
|
630
|
+
onOpenChange: setConsentOpen,
|
|
631
|
+
onAccept: handleConsentAccept
|
|
632
|
+
}
|
|
633
|
+
),
|
|
634
|
+
/* @__PURE__ */ jsx(
|
|
635
|
+
RecordingConsentDialog_default,
|
|
636
|
+
{
|
|
637
|
+
open: screenshotConsentOpen,
|
|
638
|
+
captureType: "screenshot",
|
|
639
|
+
onOpenChange: setScreenshotConsentOpen,
|
|
640
|
+
onAccept: handleScreenshotConsentAccept
|
|
641
|
+
}
|
|
642
|
+
),
|
|
643
|
+
(isRecording || recording) && createPortal(
|
|
644
|
+
/* @__PURE__ */ jsxs("div", { "data-report-mask-ignore": true, "data-report-ignore-capture": true, className: "rpi-root rpi-rec", children: [
|
|
645
|
+
/* @__PURE__ */ jsxs("div", { className: "rpi-rec__row", children: [
|
|
646
|
+
/* @__PURE__ */ jsxs("span", { className: "rpi-rec__label", children: [
|
|
647
|
+
/* @__PURE__ */ jsx(Circle, { size: 12, className: "rpi-rec__dot" }),
|
|
648
|
+
"REC"
|
|
649
|
+
] }),
|
|
650
|
+
/* @__PURE__ */ jsx("span", { className: "rpi-rec__time", children: `00:${String(elapsed).padStart(2, "0")} / 00:${String(maxDuration).padStart(2, "0")}` })
|
|
651
|
+
] }),
|
|
652
|
+
/* @__PURE__ */ jsxs(Button, { color: "error", size: "sm", onClick: stop, children: [
|
|
653
|
+
/* @__PURE__ */ jsx(Square, { size: 16 }),
|
|
654
|
+
t("Stop")
|
|
655
|
+
] })
|
|
656
|
+
] }),
|
|
657
|
+
document.body
|
|
658
|
+
)
|
|
659
|
+
] });
|
|
660
|
+
};
|
|
661
|
+
var ReportIssueDialog_default = ReportIssueDialog;
|
|
662
|
+
var FloatingReportButton = () => {
|
|
663
|
+
const { isReportOpen, setIsReportOpen, isRecording, config } = useReportIssue();
|
|
664
|
+
const { t } = config;
|
|
665
|
+
return createPortal(
|
|
666
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
667
|
+
!isRecording && !isReportOpen && /* @__PURE__ */ jsx("div", { "data-report-ignore-capture": true, className: "rpi-root rpi-fab", children: /* @__PURE__ */ jsxs(
|
|
668
|
+
Button,
|
|
669
|
+
{
|
|
670
|
+
color: "error",
|
|
671
|
+
onClick: () => setIsReportOpen(true),
|
|
672
|
+
title: t("Report an Issue"),
|
|
673
|
+
children: [
|
|
674
|
+
t("Report an Issue"),
|
|
675
|
+
/* @__PURE__ */ jsx(MessageSquareWarning, { size: 18 }),
|
|
676
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "rpi-fab__dot" })
|
|
677
|
+
]
|
|
678
|
+
}
|
|
679
|
+
) }),
|
|
680
|
+
/* @__PURE__ */ jsx(ReportIssueDialog_default, { open: isReportOpen, onOpenChange: setIsReportOpen })
|
|
681
|
+
] }),
|
|
682
|
+
document.body
|
|
683
|
+
);
|
|
684
|
+
};
|
|
685
|
+
var FloatingReportButton_default = FloatingReportButton;
|
|
686
|
+
|
|
687
|
+
export { FloatingReportButton_default as FloatingReportButton, RecordingConsentDialog_default as RecordingConsentDialog, ReportIssueDialog_default as ReportIssueDialog, useReportIssueCapture, useVideoRecorder };
|