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 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:", screenshotPath || ".feedback-vos");
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,