Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"use client";

import type { UseTRPCQueryResult } from "node_modules/@trpc/react-query/dist/getQueryKey.d-CruH3ncI.mjs";
import type { JSONArray, JSONObject } from "node_modules/superjson/dist/types";
Comment on lines +3 to +4
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix import paths to use package names directly.

Importing from specific distribution paths within node_modules is fragile and will break when package versions change.

🔎 Proposed fix
-import type { UseTRPCQueryResult } from "node_modules/@trpc/react-query/dist/getQueryKey.d-CruH3ncI.mjs";
-import type { JSONArray, JSONObject } from "node_modules/superjson/dist/types";
+import type { UseTRPCQueryResult } from "@trpc/react-query";
+import type { JSONArray, JSONObject } from "superjson";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { UseTRPCQueryResult } from "node_modules/@trpc/react-query/dist/getQueryKey.d-CruH3ncI.mjs";
import type { JSONArray, JSONObject } from "node_modules/superjson/dist/types";
import type { UseTRPCQueryResult } from "@trpc/react-query";
import type { JSONArray, JSONObject } from "superjson";
🤖 Prompt for AI Agents
In src/app/_components/common/exportToCSVButton/ExportToCSVButton.tsx around
lines 3 to 4, the imports reference deep node_modules distribution paths which
are brittle; replace them with direct package imports (e.g., import type {
UseTRPCQueryResult } from "@trpc/react-query" or the correct @trpc package
exported type, and import type { JSONArray, JSONObject } from "superjson"),
update any named type names to match the package exports, save and run
TypeScript/type-check to confirm the correct module names and adjust if the
types live under a different exported entrypoint.

import Button from "@/app/_components/common/button/Button";
import Grid from "@/assets/icons/grid";
import { notify } from "@/lib/notifications";

/**
* Helper function.
* Creates a CSV download function.
* @param tableData Data used during download
* @returns A function that initiates CSV file download when called
*/
export function downloadCSVWithoutEndpoint(tableData: JSONArray): (...args: any[]) => void {
return () => {
let csvFileString = ""; //Will hold the CSV file contents as a string to convert to blob later

for (const key in tableData[0] as JSONObject) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add validation for empty array.

Accessing tableData[0] without checking if the array is non-empty will cause a runtime error when exporting empty data.

🔎 Proposed fix
 export function downloadCSVWithoutEndpoint(tableData: JSONArray): (...args: any[]) => void {
   return () => {
+    if (tableData.length === 0) {
+      notify.error("No data to export");
+      return;
+    }
+
     let csvFileString = ""; //Will hold the CSV file contents as a string to convert to blob later
🤖 Prompt for AI Agents
In src/app/_components/common/exportToCSVButton/ExportToCSVButton.tsx around
line 19, the code accesses tableData[0] without checking if tableData is
non-empty which will throw for empty arrays; add a guard that checks if
tableData is an array and has length > 0 before iterating its keys (return
early, disable export, or show a suitable message/error when empty), and ensure
subsequent logic handles the empty case safely.

//Keys in first row (JSON) become file column headers
csvFileString = csvFileString + key + ",";
}

csvFileString = csvFileString.slice(0, -1); //Remove the last ,
csvFileString = csvFileString + "\n"; //Add a newline

for (let jsonObject of tableData) {
jsonObject = jsonObject as JSONObject; //Assert its type for TS
for (const key in jsonObject) {
csvFileString = csvFileString + jsonObject[key] + ",";
}
csvFileString = csvFileString.slice(0, -1); //Remove the last ,
csvFileString = csvFileString + "\n"; //Add a newline
}
Comment on lines +19 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Escape CSV special characters to prevent malformed output.

Values containing commas, quotes, or newlines will break the CSV format. Each cell value must be properly escaped according to RFC 4180 (wrap in quotes if it contains comma/quote/newline, and escape quotes by doubling them).

🔎 Proposed fix with CSV escaping helper

Add this helper function before downloadCSVWithoutEndpoint:

function escapeCSVValue(value: unknown): string {
  if (value == null) return "";
  
  const str = String(value);
  
  // If value contains comma, quote, or newline, wrap in quotes and escape internal quotes
  if (str.includes(",") || str.includes('"') || str.includes("\n")) {
    return `"${str.replace(/"/g, '""')}"`;
  }
  
  return str;
}

Then update the CSV generation logic:

     for (const key in tableData[0] as JSONObject) {
       //Keys in first row (JSON) become file column headers
-      csvFileString = csvFileString + key + ",";
+      csvFileString = csvFileString + escapeCSVValue(key) + ",";
     }

     csvFileString = csvFileString.slice(0, -1); //Remove the last ,
     csvFileString = csvFileString + "\n"; //Add a newline

     for (let jsonObject of tableData) {
       jsonObject = jsonObject as JSONObject; //Assert its type for TS
       for (const key in jsonObject) {
-        csvFileString = csvFileString + jsonObject[key] + ",";
+        csvFileString = csvFileString + escapeCSVValue(jsonObject[key]) + ",";
       }
       csvFileString = csvFileString.slice(0, -1); //Remove the last ,
       csvFileString = csvFileString + "\n"; //Add a newline
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/_components/common/exportToCSVButton/ExportToCSVButton.tsx around
lines 19 to 34, CSV output is not escaping values so commas, quotes, or newlines
will corrupt the file; add a helper (e.g., escapeCSVValue) that returns an empty
string for null/undefined, converts the value to string, doubles internal quotes
and wraps the string in quotes if it contains a comma, quote, or newline, then
use this helper when appending both headers and each cell value instead of
concatenating raw keys/values and continue trimming trailing commas and adding
newlines as before.


const blob = new Blob([csvFileString], { type: "text/csv" }); //Turn CSV string into blob
const downloadURL = URL.createObjectURL(blob); //Turn blob into a URL
const downloadElement = document.createElement("a");
downloadElement.href = downloadURL;
document.body.appendChild(downloadElement);
downloadElement.click(); //Force the element to click, causing the file download
Comment on lines +38 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add filename to the download anchor element.

Without a download attribute, the browser will generate a generic filename. Specify a meaningful default filename for better UX.

🔎 Proposed fix
     const blob = new Blob([csvFileString], { type: "text/csv" }); //Turn CSV string into blob
     const downloadURL = URL.createObjectURL(blob); //Turn blob into a URL
     const downloadElement = document.createElement("a");
     downloadElement.href = downloadURL;
+    downloadElement.download = `export-${new Date().toISOString().slice(0, 10)}.csv`;
     document.body.appendChild(downloadElement);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const downloadElement = document.createElement("a");
downloadElement.href = downloadURL;
document.body.appendChild(downloadElement);
downloadElement.click(); //Force the element to click, causing the file download
const downloadElement = document.createElement("a");
downloadElement.href = downloadURL;
downloadElement.download = `export-${new Date().toISOString().slice(0, 10)}.csv`;
document.body.appendChild(downloadElement);
downloadElement.click(); //Force the element to click, causing the file download
🤖 Prompt for AI Agents
In src/app/_components/common/exportToCSVButton/ExportToCSVButton.tsx around
lines 38 to 41, the created anchor lacks a download attribute so the browser
chooses a generic filename; set a meaningful default filename (e.g., from props,
fallback to `export.csv` or include a timestamp) by assigning
downloadElement.download = filename before calling click(), then proceed to
click, remove the element and revoke the object URL if used to ensure cleanup.

URL.revokeObjectURL(downloadURL); //Remove the URL for the blob
document.body.removeChild(downloadElement); //Remove the temp element
};
}

/**
* Helper function.
* Feeds table data to downloadCSVWithoutEndpoint.
* Use if a query's table data does not need to be altered.
* @param endpointQuery A TPRC query to extract table data using
* @returns A function that initiates CSV file download when called
*/
export function downloadCSVWithEndpoint<TData, TError>(
endpointQuery: UseTRPCQueryResult<TData, TError>,
): (...args: any[]) => void {
return async () => {
const result = await endpointQuery.refetch(); //Call the passed query function

if (result.error) {
//There's an error
notify.error("Query failed!");
} else if (Array.isArray(result.data) && typeof result.data[0] === "object") {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add explicit empty array validation.

While typeof result.data[0] === "object" will be false for empty arrays (since typeof undefined is "undefined"), explicitly checking array length improves code clarity and prevents potential issues.

🔎 Proposed fix
-    } else if (Array.isArray(result.data) && typeof result.data[0] === "object") {
+    } else if (Array.isArray(result.data) && result.data.length > 0 && typeof result.data[0] === "object") {
       //result.data is an array of objects
       const jsonArray = result.data as JSONArray; //Assert that this is a JSONArray

Or handle empty arrays explicitly:

-    } else if (Array.isArray(result.data) && typeof result.data[0] === "object") {
+    } else if (Array.isArray(result.data) && result.data.length > 0 && typeof result.data[0] === "object") {
       //result.data is an array of objects
       const jsonArray = result.data as JSONArray; //Assert that this is a JSONArray
       const downloadCSVFunc = downloadCSVWithoutEndpoint(jsonArray); //Call other helper to get downloader function
 
       downloadCSVFunc(); //Run the downloader function
+    } else if (Array.isArray(result.data) && result.data.length === 0) {
+      notify.warning("No data to export");
     } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (Array.isArray(result.data) && typeof result.data[0] === "object") {
} else if (Array.isArray(result.data) && result.data.length > 0 && typeof result.data[0] === "object") {
🤖 Prompt for AI Agents
In src/app/_components/common/exportToCSVButton/ExportToCSVButton.tsx around
line 63, the branch uses Array.isArray(result.data) && typeof result.data[0] ===
"object" but doesn't explicitly guard against empty arrays; change the condition
to check length (e.g., Array.isArray(result.data) && result.data.length > 0 &&
typeof result.data[0] === "object") and add an explicit handler for empty arrays
(result.data.length === 0) to return/skip CSV generation or show an appropriate
no-data response so empty arrays are handled clearly and safely.

//result.data is an array of objects
const jsonArray = result.data as JSONArray; //Assert that this is a JSONArray
const downloadCSVFunc = downloadCSVWithoutEndpoint(jsonArray); //Call other helper to get downloader function

downloadCSVFunc(); //Run the downloader function
} else {
//result.data is NOT an array of objects
notify.error("Query returned invalid data!");
}
};
}

interface ExportToCSVButtonProps {
downloadFunction?: (...args: any[]) => void; //Variable is of type function
}

export default function ExportToCSVButton({ downloadFunction }: ExportToCSVButtonProps) {
return (
<Button
text="Export to CSV File"
variant="secondary"
icon={<Grid />}
onClick={downloadFunction}
/>
);
}