-
Notifications
You must be signed in to change notification settings - Fork 0
SANC-56-export-to-csv-button-functionality #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"; | ||||||||||||||||||||
| 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) { | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add validation for empty array. Accessing 🔎 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 |
||||||||||||||||||||
| //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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 helperAdd this helper function before 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
}
🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add filename to the download anchor element. Without a 🔎 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| 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") { | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add explicit empty array validation. While 🔎 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 JSONArrayOr 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| //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} | ||||||||||||||||||||
| /> | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix import paths to use package names directly.
Importing from specific distribution paths within
node_modulesis fragile and will break when package versions change.🔎 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents