Skip to content
Open
Show file tree
Hide file tree
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
17 changes: 3 additions & 14 deletions frontend/components/Item/View/table/data-table-dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import { toast } from "~/components/ui/sonner";
import { useDialog } from "@/components/ui/dialog-provider";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import { formatValueAsCsvField } from "~/lib/utils";

const { t } = useI18n();
const api = useUserApi();
Expand Down Expand Up @@ -59,28 +60,16 @@
items.forEach(item => window.open(`/item/${item}`, "_blank"));
};

const escapeCsvField = (value: unknown): string => {
let str = String(value ?? "");
// Mitigate formula injection
if (/^[=+\-@]/.test(str)) {
str = "'" + str;
}
// Escape double quotes
str = str.replace(/"/g, '""');
// Wrap in double quotes
return `"${str}"`;
};

const downloadCsv = (items: Row<ItemSummary>[], columns: Column<ItemSummary>[]) => {
// get enabled columns
const enabledColumns = columns.filter(c => c.id !== undefined && c.getIsVisible() && c.getCanHide()).map(c => c.id);

// create CSV header (escaped)
const header = enabledColumns.map(escapeCsvField).join(",");
const header = enabledColumns.map(formatValueAsCsvField).join(",");

// map each item to a row matching enabled columns order, escaping each field
const rows = items.map(item =>
enabledColumns.map(col => escapeCsvField(item.original[col as keyof ItemSummary])).join(",")
enabledColumns.map(col => formatValueAsCsvField(item.original[col as keyof ItemSummary])).join(",")
);

const csv = [header, ...rows].join("\n");
Expand Down
4 changes: 2 additions & 2 deletions frontend/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

declare module "#app" {
interface NuxtApp {
$otelEnabled: boolean;
$otelEnabled: boolean;
}
}

declare module "vue" {
interface ComponentCustomProperties {
$otelEnabled: boolean;
$otelEnabled: boolean;
}
}
export {};
46 changes: 46 additions & 0 deletions frontend/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,49 @@ export function getContrastTextColor(bgColor: string): string {
}

export const camelToSnakeCase = (str: string) => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);

export function getNameOrValue(value: unknown): string | unknown {
const nameEvaluation: string | false =
value !== null &&
value !== undefined &&
typeof value === "object" &&
"name" in value &&
typeof value.name === "string" &&
value.name;

return nameEvaluation ? nameEvaluation : value;
}

export function formatArrayAsString(value: unknown): string {
let str = "";

if (Array.isArray(value)) {
value.map(item => {
str = `${str}${str ? "," : ""}${formatArrayAsString(item)}`;
});
} else {
str = String(getNameOrValue(value) ?? "");
}

return str;
}

export function formatValueAsCsvField(value: unknown): string {
let str = "";

if (Array.isArray(value)) {
str = formatArrayAsString(value);
} else {
str = String(getNameOrValue(value) ?? "");
}

// Mitigate formula injection
if (/^[\t\r\n ]*[=+\-@]/.test(str)) {
str = "'" + str;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
// Escape double quotes
str = str.replace(/"/g, '""');

// Wrap in double quotes
return `"${str}"`;
}
Loading