Files
outline/shared/utils/csv.ts
Copilot 133ec073be Add CSV export for member list (#10803)
* Initial plan

* Add CSV export functionality to members page

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Align Export CSV button to the right

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Address code review feedback: improve type safety, error handling, and date formatting

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Improve CSV utility and date handling consistency

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Improve error messages and fix useCallback dependencies

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Add comprehensive tests for CSV utility

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Refactor: reduce limit to 100, replace lastActiveIp with role, extract ExportCSV component

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Improve type safety and extract pagination constant

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* refactor

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-05 09:42:36 -05:00

98 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* oxlint-disable no-control-regex */
/**
* Helper class for CSV operations.
*/
export class CSVHelper {
/**
* Sanitizes a value for CSV output.
*
* @param value The value to sanitize.
* @returns The sanitized value.
*/
public static sanitizeValue(value: string): string {
if (!value) {
return "";
}
return (
value
.toString()
// Formula triggers
.replace(/^([+\-=@<>±÷×])/u, "'$1")
// Control characters (excluding tab, newline, and carriage return)
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/gu, "")
// Zero-width spaces
.replace(/[\u200B-\u200D\uFEFF]/g, "")
// Bidirectional control
.replace(/[\u202A-\u202E\u2066-\u2069]/g, "")
);
}
/**
* Escapes a CSV field value by wrapping it in quotes if necessary.
*
* @param value The value to escape.
* @returns The escaped value.
*/
public static escapeCSVField(value: unknown): string {
if (value === null || value === undefined) {
return "";
}
const stringValue = String(value);
// If the value contains comma, quote, or newline, wrap it in quotes and escape internal quotes
if (
stringValue.includes(",") ||
stringValue.includes('"') ||
stringValue.includes("\n")
) {
return `"${stringValue.replace(/"/g, '""')}"`;
}
return stringValue;
}
/**
* Converts an array of objects to CSV format.
*
* @param data Array of objects to convert.
* @param headers Array of header names in the desired order.
* @returns CSV string.
*/
public static convertToCSV<T extends Record<string, unknown>>(
data: T[],
headers: (keyof T)[]
): string {
if (data.length === 0) {
return (
headers
.map((h) => String(h))
.map((h) => this.escapeCSVField(this.sanitizeValue(h)))
.join(",") + "\n"
);
}
// Create header row
const headerRow = headers
.map((h) => String(h))
.map((h) => this.escapeCSVField(this.sanitizeValue(h)))
.join(",");
// Create data rows
const dataRows = data.map((row) =>
headers
.map((header) => {
const value = row[header];
const stringValue =
value === null || value === undefined ? "" : String(value);
return this.escapeCSVField(this.sanitizeValue(stringValue));
})
.join(",")
);
return [headerRow, ...dataRows].join("\n");
}
}