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