feedback-vos 1.0.27 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +313 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +315 -7
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +41 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ChatTeardropDots, ArrowLeft, X, Trash, Camera, CircleNotch } from 'phosphor-react';
|
|
1
|
+
import { ChatTeardropDots, ArrowLeft, X, Paperclip, Trash, Camera, CircleNotch } from 'phosphor-react';
|
|
2
2
|
import { Popover } from '@headlessui/react';
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
5
5
|
import html2canvas from 'html2canvas';
|
|
6
6
|
|
|
@@ -130,8 +130,210 @@ function ScreenshotButton({
|
|
|
130
130
|
}
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
|
+
var DEFAULT_MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
134
|
+
var DEFAULT_MAX_TOTAL_SIZE = 20 * 1024 * 1024;
|
|
135
|
+
function FileUploadButton({
|
|
136
|
+
files,
|
|
137
|
+
onFilesChanged,
|
|
138
|
+
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
|
139
|
+
maxTotalSize = DEFAULT_MAX_TOTAL_SIZE,
|
|
140
|
+
acceptedTypes = "image/*,.pdf,.doc,.docx,.txt",
|
|
141
|
+
language = "en"
|
|
142
|
+
}) {
|
|
143
|
+
const fileInputRef = useRef(null);
|
|
144
|
+
const [error, setError] = useState(null);
|
|
145
|
+
const translations2 = {
|
|
146
|
+
en: {
|
|
147
|
+
upload: "Upload file",
|
|
148
|
+
remove: "Remove file",
|
|
149
|
+
fileTooLarge: "File is too large",
|
|
150
|
+
totalSizeExceeded: "Total file size exceeded",
|
|
151
|
+
maxFileSize: "Max file size:",
|
|
152
|
+
maxTotalSize: "Max total size:",
|
|
153
|
+
files: "files"
|
|
154
|
+
},
|
|
155
|
+
nl: {
|
|
156
|
+
upload: "Bestand uploaden",
|
|
157
|
+
remove: "Bestand verwijderen",
|
|
158
|
+
fileTooLarge: "Bestand is te groot",
|
|
159
|
+
totalSizeExceeded: "Totale bestandsgrootte overschreden",
|
|
160
|
+
maxFileSize: "Max bestandsgrootte:",
|
|
161
|
+
maxTotalSize: "Max totale grootte:",
|
|
162
|
+
files: "bestanden"
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const t = translations2[language];
|
|
166
|
+
function formatFileSize(bytes) {
|
|
167
|
+
if (bytes < 1024) return bytes + " B";
|
|
168
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
169
|
+
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
|
170
|
+
}
|
|
171
|
+
function handleFileSelect(e) {
|
|
172
|
+
setError(null);
|
|
173
|
+
const selectedFiles = Array.from(e.target.files || []);
|
|
174
|
+
if (selectedFiles.length === 0) return;
|
|
175
|
+
const newFiles = [];
|
|
176
|
+
let totalSize = files.reduce((sum, f) => sum + f.file.size, 0);
|
|
177
|
+
for (const file of selectedFiles) {
|
|
178
|
+
if (file.size > maxFileSize) {
|
|
179
|
+
setError(`${t.fileTooLarge} (${file.name}): ${formatFileSize(file.size)} > ${formatFileSize(maxFileSize)}`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (totalSize + file.size > maxTotalSize) {
|
|
183
|
+
setError(`${t.totalSizeExceeded} (${formatFileSize(maxTotalSize)})`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
let preview;
|
|
187
|
+
if (file.type.startsWith("image/")) {
|
|
188
|
+
preview = URL.createObjectURL(file);
|
|
189
|
+
}
|
|
190
|
+
newFiles.push({
|
|
191
|
+
file,
|
|
192
|
+
preview,
|
|
193
|
+
id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
|
|
194
|
+
});
|
|
195
|
+
totalSize += file.size;
|
|
196
|
+
}
|
|
197
|
+
if (newFiles.length > 0) {
|
|
198
|
+
onFilesChanged([...files, ...newFiles]);
|
|
199
|
+
}
|
|
200
|
+
if (fileInputRef.current) {
|
|
201
|
+
fileInputRef.current.value = "";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function handleRemoveFile(id) {
|
|
205
|
+
const fileToRemove = files.find((f) => f.id === id);
|
|
206
|
+
if (fileToRemove?.preview) {
|
|
207
|
+
URL.revokeObjectURL(fileToRemove.preview);
|
|
208
|
+
}
|
|
209
|
+
onFilesChanged(files.filter((f) => f.id !== id));
|
|
210
|
+
setError(null);
|
|
211
|
+
}
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
return () => {
|
|
214
|
+
files.forEach((file) => {
|
|
215
|
+
if (file.preview) {
|
|
216
|
+
URL.revokeObjectURL(file.preview);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
}, [files]);
|
|
221
|
+
function handleButtonClick() {
|
|
222
|
+
fileInputRef.current?.click();
|
|
223
|
+
}
|
|
224
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
225
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
|
|
226
|
+
/* @__PURE__ */ jsx(
|
|
227
|
+
"button",
|
|
228
|
+
{
|
|
229
|
+
type: "button",
|
|
230
|
+
className: "p-2 bg-zinc-800 rounded-md border-transparent hover:bg-zinc-700\n transitions-colors focus:outline-none focus:ring-2\n focus:ring-offset-2 focus:ring-offset-zinc-900 focus:ring-brand-500",
|
|
231
|
+
onClick: handleButtonClick,
|
|
232
|
+
title: t.upload,
|
|
233
|
+
children: /* @__PURE__ */ jsx(Paperclip, { weight: "bold", className: "w-6 h-6" })
|
|
234
|
+
}
|
|
235
|
+
),
|
|
236
|
+
/* @__PURE__ */ jsx(
|
|
237
|
+
"input",
|
|
238
|
+
{
|
|
239
|
+
ref: fileInputRef,
|
|
240
|
+
type: "file",
|
|
241
|
+
multiple: true,
|
|
242
|
+
accept: acceptedTypes,
|
|
243
|
+
onChange: handleFileSelect,
|
|
244
|
+
className: "hidden"
|
|
245
|
+
}
|
|
246
|
+
),
|
|
247
|
+
files.map((uploadedFile) => /* @__PURE__ */ jsxs(
|
|
248
|
+
"div",
|
|
249
|
+
{
|
|
250
|
+
className: "flex items-center gap-1 bg-zinc-800 rounded-md px-2 py-1 text-xs",
|
|
251
|
+
children: [
|
|
252
|
+
uploadedFile.preview ? /* @__PURE__ */ jsx(
|
|
253
|
+
"img",
|
|
254
|
+
{
|
|
255
|
+
src: uploadedFile.preview,
|
|
256
|
+
alt: uploadedFile.file.name,
|
|
257
|
+
className: "w-6 h-6 object-cover rounded"
|
|
258
|
+
}
|
|
259
|
+
) : /* @__PURE__ */ jsx(Paperclip, { className: "w-4 h-4" }),
|
|
260
|
+
/* @__PURE__ */ jsx("span", { className: "text-zinc-300 max-w-[100px] truncate", title: uploadedFile.file.name, children: uploadedFile.file.name }),
|
|
261
|
+
/* @__PURE__ */ jsx(
|
|
262
|
+
"button",
|
|
263
|
+
{
|
|
264
|
+
type: "button",
|
|
265
|
+
onClick: () => handleRemoveFile(uploadedFile.id),
|
|
266
|
+
className: "text-zinc-400 hover:text-zinc-100 transition-colors",
|
|
267
|
+
title: t.remove,
|
|
268
|
+
children: /* @__PURE__ */ jsx(X, { weight: "bold", className: "w-4 h-4" })
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
]
|
|
272
|
+
},
|
|
273
|
+
uploadedFile.id
|
|
274
|
+
))
|
|
275
|
+
] }),
|
|
276
|
+
error && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-400", children: error }),
|
|
277
|
+
files.length > 0 && /* @__PURE__ */ jsxs("p", { className: "text-xs text-zinc-400", children: [
|
|
278
|
+
files.length,
|
|
279
|
+
" ",
|
|
280
|
+
t.files,
|
|
281
|
+
" (",
|
|
282
|
+
formatFileSize(files.reduce((sum, f) => sum + f.file.size, 0)),
|
|
283
|
+
" / ",
|
|
284
|
+
formatFileSize(maxTotalSize),
|
|
285
|
+
")"
|
|
286
|
+
] })
|
|
287
|
+
] });
|
|
288
|
+
}
|
|
133
289
|
|
|
134
290
|
// src/lib/integrations/github.ts
|
|
291
|
+
async function uploadFileToRepo(token, owner, repo, file, folderPath, defaultBranch) {
|
|
292
|
+
const base64Content = await new Promise((resolve, reject) => {
|
|
293
|
+
const reader = new FileReader();
|
|
294
|
+
reader.onload = () => {
|
|
295
|
+
const result = reader.result;
|
|
296
|
+
const base64 = result.includes(",") ? result.split(",")[1] : result;
|
|
297
|
+
resolve(base64);
|
|
298
|
+
};
|
|
299
|
+
reader.onerror = reject;
|
|
300
|
+
reader.readAsDataURL(file);
|
|
301
|
+
});
|
|
302
|
+
const timestamp = Date.now();
|
|
303
|
+
const randomId = Math.random().toString(36).substring(2, 9);
|
|
304
|
+
const fileExtension = file.name.split(".").pop() || "bin";
|
|
305
|
+
const filename = `feedback-${timestamp}-${randomId}.${fileExtension}`;
|
|
306
|
+
const path = `${folderPath}/${filename}`;
|
|
307
|
+
const uploadUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`;
|
|
308
|
+
const response = await fetch(uploadUrl, {
|
|
309
|
+
method: "PUT",
|
|
310
|
+
headers: {
|
|
311
|
+
"Authorization": `Bearer ${token}`,
|
|
312
|
+
"Accept": "application/vnd.github.v3+json",
|
|
313
|
+
"Content-Type": "application/json",
|
|
314
|
+
"User-Agent": "feedback-vos"
|
|
315
|
+
},
|
|
316
|
+
body: JSON.stringify({
|
|
317
|
+
message: `Add feedback file: ${filename}`,
|
|
318
|
+
content: base64Content,
|
|
319
|
+
branch: defaultBranch
|
|
320
|
+
})
|
|
321
|
+
});
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
const errorData = await response.json().catch(() => ({}));
|
|
324
|
+
const errorMessage = errorData.message || "Unknown error";
|
|
325
|
+
if (response.status === 409) {
|
|
326
|
+
throw new Error(`File already exists: ${filename}`);
|
|
327
|
+
} else if (response.status === 403) {
|
|
328
|
+
throw new Error(`Permission denied for file: ${filename}`);
|
|
329
|
+
} else if (response.status === 422) {
|
|
330
|
+
throw new Error(`File too large or invalid: ${filename} - ${errorMessage}`);
|
|
331
|
+
}
|
|
332
|
+
throw new Error(`Failed to upload file ${filename} (${response.status}): ${errorMessage}`);
|
|
333
|
+
}
|
|
334
|
+
const rawUrl = `https://github.com/${owner}/${repo}/blob/${defaultBranch}/${path}?raw=true`;
|
|
335
|
+
return rawUrl;
|
|
336
|
+
}
|
|
135
337
|
async function uploadScreenshotToRepo(token, owner, repo, screenshot, screenshotPath) {
|
|
136
338
|
const compressedScreenshot = await compressScreenshot(screenshot, 1920, 0.7);
|
|
137
339
|
const base64Data = compressedScreenshot.split(",")[1];
|
|
@@ -306,7 +508,7 @@ async function verifyRepositoryAccess(token, owner, repo) {
|
|
|
306
508
|
}
|
|
307
509
|
async function sendToGitHub(config, data) {
|
|
308
510
|
const { token, owner, repo, screenshotPath } = config;
|
|
309
|
-
const { type, comment, screenshot } = data;
|
|
511
|
+
const { type, comment, screenshot, files } = data;
|
|
310
512
|
if (!token || !owner || !repo) {
|
|
311
513
|
throw new Error("GitHub configuration is incomplete. Please provide token, owner, and repo.");
|
|
312
514
|
}
|
|
@@ -329,6 +531,55 @@ Please verify:
|
|
|
329
531
|
Please enable Issues in repository Settings \u2192 General \u2192 Features \u2192 Issues`
|
|
330
532
|
);
|
|
331
533
|
}
|
|
534
|
+
const repoResponse = await fetch(
|
|
535
|
+
`https://api.github.com/repos/${owner}/${repo}`,
|
|
536
|
+
{
|
|
537
|
+
headers: {
|
|
538
|
+
"Authorization": `Bearer ${token}`,
|
|
539
|
+
"Accept": "application/vnd.github.v3+json",
|
|
540
|
+
"User-Agent": "feedback-vos"
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
let defaultBranch = "main";
|
|
545
|
+
if (repoResponse.ok) {
|
|
546
|
+
const repoData = await repoResponse.json();
|
|
547
|
+
defaultBranch = repoData.default_branch || "main";
|
|
548
|
+
}
|
|
549
|
+
const folderPath = screenshotPath || ".feedback-vos";
|
|
550
|
+
const folderCheckUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${folderPath}`;
|
|
551
|
+
try {
|
|
552
|
+
const folderCheck = await fetch(folderCheckUrl, {
|
|
553
|
+
headers: {
|
|
554
|
+
"Authorization": `Bearer ${token}`,
|
|
555
|
+
"Accept": "application/vnd.github.v3+json",
|
|
556
|
+
"User-Agent": "feedback-vos"
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
if (folderCheck.status === 404) {
|
|
560
|
+
const readmeContent = btoa("# Feedback Files\n\nThis folder contains files from user feedback.\n");
|
|
561
|
+
try {
|
|
562
|
+
await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${folderPath}/README.md`, {
|
|
563
|
+
method: "PUT",
|
|
564
|
+
headers: {
|
|
565
|
+
"Authorization": `Bearer ${token}`,
|
|
566
|
+
"Accept": "application/vnd.github.v3+json",
|
|
567
|
+
"Content-Type": "application/json",
|
|
568
|
+
"User-Agent": "feedback-vos"
|
|
569
|
+
},
|
|
570
|
+
body: JSON.stringify({
|
|
571
|
+
message: "Create feedback files folder",
|
|
572
|
+
content: readmeContent,
|
|
573
|
+
branch: defaultBranch
|
|
574
|
+
})
|
|
575
|
+
});
|
|
576
|
+
} catch (folderCreateError) {
|
|
577
|
+
console.warn("Could not create folder, proceeding with upload:", folderCreateError);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} catch (folderError) {
|
|
581
|
+
console.warn("Could not verify/create files folder:", folderError);
|
|
582
|
+
}
|
|
332
583
|
const title = `[${type}] Feedback`;
|
|
333
584
|
const ABSOLUTE_MAX_LENGTH = 65536;
|
|
334
585
|
const BASE_BODY_LENGTH = 50;
|
|
@@ -343,7 +594,7 @@ ${limitedComment}`;
|
|
|
343
594
|
if (screenshot) {
|
|
344
595
|
try {
|
|
345
596
|
console.log("Uploading screenshot to repository...");
|
|
346
|
-
console.log("Screenshot path:",
|
|
597
|
+
console.log("Screenshot path:", folderPath);
|
|
347
598
|
const screenshotUrl = await uploadScreenshotToRepo(token, owner, repo, screenshot, screenshotPath);
|
|
348
599
|
console.log("Screenshot uploaded successfully, URL:", screenshotUrl);
|
|
349
600
|
body += `
|
|
@@ -359,6 +610,44 @@ ${limitedComment}`;
|
|
|
359
610
|
console.log("Body length after upload failure:", body.length);
|
|
360
611
|
}
|
|
361
612
|
}
|
|
613
|
+
if (files && files.length > 0) {
|
|
614
|
+
const uploadedFileUrls = [];
|
|
615
|
+
const failedFiles = [];
|
|
616
|
+
for (const uploadedFile of files) {
|
|
617
|
+
try {
|
|
618
|
+
console.log(`Uploading file: ${uploadedFile.file.name}`);
|
|
619
|
+
const fileUrl = await uploadFileToRepo(
|
|
620
|
+
token,
|
|
621
|
+
owner,
|
|
622
|
+
repo,
|
|
623
|
+
uploadedFile.file,
|
|
624
|
+
folderPath,
|
|
625
|
+
defaultBranch
|
|
626
|
+
);
|
|
627
|
+
uploadedFileUrls.push(fileUrl);
|
|
628
|
+
console.log(`File uploaded successfully: ${fileUrl}`);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error(`Failed to upload file ${uploadedFile.file.name}:`, error);
|
|
631
|
+
failedFiles.push(uploadedFile.file.name);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (uploadedFileUrls.length > 0) {
|
|
635
|
+
body += `
|
|
636
|
+
|
|
637
|
+
**Uploaded Files:**
|
|
638
|
+
`;
|
|
639
|
+
uploadedFileUrls.forEach((url2, index) => {
|
|
640
|
+
const fileName = files[index]?.file.name || "Unknown";
|
|
641
|
+
body += `- [${fileName}](${url2})
|
|
642
|
+
`;
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
if (failedFiles.length > 0) {
|
|
646
|
+
body += `
|
|
647
|
+
|
|
648
|
+
**Failed to upload:** ${failedFiles.join(", ")}`;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
362
651
|
const SAFE_MAX_LENGTH = 65e3;
|
|
363
652
|
console.log(`Issue body length before final check: ${body.length} characters`);
|
|
364
653
|
if (body.length > SAFE_MAX_LENGTH) {
|
|
@@ -515,9 +804,19 @@ function FeedbackContentStep({
|
|
|
515
804
|
const t = getTranslations(language);
|
|
516
805
|
const feedbackTypes = getFeedbackTypes(language);
|
|
517
806
|
const [screenshot, setScreenshot] = useState(null);
|
|
807
|
+
const [uploadedFiles, setUploadedFiles] = useState([]);
|
|
518
808
|
const feedbackTypeData = feedbackTypes[feedbackType];
|
|
519
809
|
const [comment, setComment] = useState("");
|
|
520
810
|
const [isSendingFeedback, setIsSendingFeedback] = useState(false);
|
|
811
|
+
useEffect(() => {
|
|
812
|
+
return () => {
|
|
813
|
+
uploadedFiles.forEach((file) => {
|
|
814
|
+
if (file.preview) {
|
|
815
|
+
URL.revokeObjectURL(file.preview);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
};
|
|
819
|
+
}, [uploadedFiles]);
|
|
521
820
|
async function handleSubmitFeedback(e) {
|
|
522
821
|
e.preventDefault();
|
|
523
822
|
setIsSendingFeedback(true);
|
|
@@ -525,7 +824,8 @@ function FeedbackContentStep({
|
|
|
525
824
|
const feedbackData = {
|
|
526
825
|
type: feedbackType,
|
|
527
826
|
comment,
|
|
528
|
-
screenshot
|
|
827
|
+
screenshot,
|
|
828
|
+
files: uploadedFiles
|
|
529
829
|
};
|
|
530
830
|
if (integration === "github") {
|
|
531
831
|
await sendToGitHub(githubConfig, feedbackData);
|
|
@@ -546,12 +846,12 @@ function FeedbackContentStep({
|
|
|
546
846
|
"button",
|
|
547
847
|
{
|
|
548
848
|
type: "button",
|
|
549
|
-
className: "absolute top-5 left-5 text-zinc-400 hover:text-zinc-100",
|
|
849
|
+
className: "absolute top-5 left-5 text-zinc-400 hover:text-zinc-100 z-10",
|
|
550
850
|
onClick: onFeedbackRestartRequest,
|
|
551
851
|
children: /* @__PURE__ */ jsx(ArrowLeft, { weight: "bold", className: "w-4 h-4" })
|
|
552
852
|
}
|
|
553
853
|
),
|
|
554
|
-
/* @__PURE__ */ jsxs("span", { className: "text-xl leading-6 flex items-center gap-2 mt-2", children: [
|
|
854
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xl leading-6 flex items-center gap-2 mt-2 pl-10", children: [
|
|
555
855
|
/* @__PURE__ */ jsx(
|
|
556
856
|
"img",
|
|
557
857
|
{
|
|
@@ -573,6 +873,14 @@ function FeedbackContentStep({
|
|
|
573
873
|
onChange: (e) => setComment(e.target.value)
|
|
574
874
|
}
|
|
575
875
|
),
|
|
876
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(
|
|
877
|
+
FileUploadButton,
|
|
878
|
+
{
|
|
879
|
+
files: uploadedFiles,
|
|
880
|
+
onFilesChanged: setUploadedFiles,
|
|
881
|
+
language
|
|
882
|
+
}
|
|
883
|
+
) }),
|
|
576
884
|
/* @__PURE__ */ jsxs("footer", { className: " flex gap-2 mt-2", children: [
|
|
577
885
|
/* @__PURE__ */ jsx(
|
|
578
886
|
ScreenshotButton,
|