diff --git a/frontend/components/Item/View/table/data-table-dropdown.vue b/frontend/components/Item/View/table/data-table-dropdown.vue index a8e67eac9..53fc92f6e 100644 --- a/frontend/components/Item/View/table/data-table-dropdown.vue +++ b/frontend/components/Item/View/table/data-table-dropdown.vue @@ -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(); @@ -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[], columns: Column[]) => { // 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"); diff --git a/frontend/global.d.ts b/frontend/global.d.ts index 69699fecd..7ad74a5d9 100644 --- a/frontend/global.d.ts +++ b/frontend/global.d.ts @@ -2,13 +2,13 @@ declare module "#app" { interface NuxtApp { - $otelEnabled: boolean; + $otelEnabled: boolean; } } declare module "vue" { interface ComponentCustomProperties { - $otelEnabled: boolean; + $otelEnabled: boolean; } } export {}; diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts index 83ca1af0b..1055b3e50 100644 --- a/frontend/lib/utils.ts +++ b/frontend/lib/utils.ts @@ -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; + } + // Escape double quotes + str = str.replace(/"/g, '""'); + + // Wrap in double quotes + return `"${str}"`; +}