Skip to content
Merged
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
22 changes: 22 additions & 0 deletions web/oss/src/components/TestcasesTableNew/utils/groupColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,21 @@ export function getLeafColumnName(key: string): string {
return key.substring(lastDotIndex + 1)
}

/**
* Check if a column is an expanded column (came from object expansion)
* Expanded columns have parentKey property set
*/
function isExpandedColumn(col: Column): boolean {
return "parentKey" in col && typeof (col as any).parentKey === "string"
}

/**
* Recursively group columns into nested structure
* This handles deeply nested paths like "a.b.c.d" by creating nested group headers
* Respects maxDepth to limit nesting for performance
*
* IMPORTANT: Only groups columns that came from object expansion (have parentKey).
* Columns with dots in their flat key names (e.g., "agents.md") are NOT grouped.
*/
function groupColumnsRecursive<T>(
columns: Column[],
Expand All @@ -98,6 +109,17 @@ function groupColumnsRecursive<T>(

// First pass: categorize columns into groups or standalone (leaf columns)
columns.forEach((col) => {
// Only group columns that came from object expansion (have parentKey)
// Flat columns with dots in their names (e.g., "agents.md") should NOT be grouped
if (!isExpandedColumn(col) && currentDepth === 0) {
// Top-level flat column - render as-is, even if it has dots
result.push({
...createColumnDef(col, col.name),
__order: orderCounter++,
} as ColumnType<T> & {__order: number})
return
}

// Get the relative key (remove parent path prefix if present)
const relativeKey = parentPath ? col.key.substring(parentPath.length + 1) : col.key
const parsed = parseGroupedColumnKey(relativeKey)
Expand Down
85 changes: 80 additions & 5 deletions web/oss/src/components/pages/testset/modals/CreateTestset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,81 @@ const CreateTestset: React.FC<Props> = ({setCurrent, onCancel}) => {
const [previewData, setPreviewData] = useState<GenericObject[]>([])
const setRefreshTrigger = useSetAtom(testsetsRefreshTriggerAtom)

/**
* Parse CSV text properly handling quoted fields with embedded newlines and commas
*/
const parseCSVRows = (text: string): string[][] => {
const rows: string[][] = []
let currentRow: string[] = []
let currentField = ""
let inQuotes = false
let i = 0

while (i < text.length) {
const char = text[i]

if (inQuotes) {
if (char === '"') {
// Check for escaped quote ("")
if (i + 1 < text.length && text[i + 1] === '"') {
currentField += '"'
i += 2
continue
}
// End of quoted field
inQuotes = false
i++
continue
}
// Inside quotes - add character as-is (including newlines)
currentField += char
i++
} else {
if (char === '"') {
// Start of quoted field
inQuotes = true
i++
} else if (char === ",") {
// Field separator
currentRow.push(currentField.trim())
currentField = ""
i++
} else if (char === "\n" || (char === "\r" && text[i + 1] === "\n")) {
// Row separator
currentRow.push(currentField.trim())
if (currentRow.some((field) => field !== "")) {
rows.push(currentRow)
}
currentRow = []
currentField = ""
i += char === "\r" ? 2 : 1
} else if (char === "\r") {
// Handle standalone \r as row separator
currentRow.push(currentField.trim())
if (currentRow.some((field) => field !== "")) {
rows.push(currentRow)
}
currentRow = []
currentField = ""
i++
} else {
currentField += char
i++
}
}
}

// Handle last field and row
if (currentField || currentRow.length > 0) {
currentRow.push(currentField.trim())
if (currentRow.some((field) => field !== "")) {
rows.push(currentRow)
}
}

return rows
}

const parseFileForPreview = async (
file: File,
fileType: "CSV" | "JSON",
Expand All @@ -132,12 +207,12 @@ const CreateTestset: React.FC<Props> = ({setCurrent, onCancel}) => {
return parsed.slice(0, maxPreviewRows)
}
} else {
const lines = text.split("\n").filter((line) => line.trim())
if (lines.length > 0) {
const headers = lines[0].split(",").map((h) => h.trim())
const csvRows = parseCSVRows(text)
if (csvRows.length > 0) {
const headers = csvRows[0]
const rows: GenericObject[] = []
for (let i = 1; i < Math.min(lines.length, maxPreviewRows + 1); i++) {
const values = lines[i].split(",").map((v) => v.trim())
for (let i = 1; i < Math.min(csvRows.length, maxPreviewRows + 1); i++) {
const values = csvRows[i]
const row: GenericObject = {}
headers.forEach((header, idx) => {
row[header] = values[idx] || ""
Expand Down
9 changes: 8 additions & 1 deletion web/oss/src/state/entities/testcase/testcaseEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -976,12 +976,19 @@ export const testcaseCellAtomFamily = atomFamily(
return undefined
}

// First, try direct key access (handles flat keys with dots like "agents.md")
// This is important because column names can legitimately contain dots
const directValue = (entity as Record<string, unknown>)[column]
if (directValue !== undefined) {
return directValue
}

// Handle nested paths (e.g., "VMs_previous_RFP.event")
// We need to parse JSON strings for nested access
const parts = column.split(".")

if (parts.length === 1) {
// Simple top-level access
// Simple top-level access - already tried above, return undefined
return get(entity, column)
}

Expand Down