Skip to content

Commit 80e046a

Browse files
Merge pull request #3368 from Agenta-AI/frontend-fix/fix-preview-table-in-testset-upload-flow
[Frontend/fix] Improve preview table in file upload flow for new testset creation
2 parents d4e0bea + 9369f19 commit 80e046a

File tree

3 files changed

+110
-6
lines changed

3 files changed

+110
-6
lines changed

web/oss/src/components/TestcasesTableNew/utils/groupColumns.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,21 @@ export function getLeafColumnName(key: string): string {
7272
return key.substring(lastDotIndex + 1)
7373
}
7474

75+
/**
76+
* Check if a column is an expanded column (came from object expansion)
77+
* Expanded columns have parentKey property set
78+
*/
79+
function isExpandedColumn(col: Column): boolean {
80+
return "parentKey" in col && typeof (col as any).parentKey === "string"
81+
}
82+
7583
/**
7684
* Recursively group columns into nested structure
7785
* This handles deeply nested paths like "a.b.c.d" by creating nested group headers
7886
* Respects maxDepth to limit nesting for performance
87+
*
88+
* IMPORTANT: Only groups columns that came from object expansion (have parentKey).
89+
* Columns with dots in their flat key names (e.g., "agents.md") are NOT grouped.
7990
*/
8091
function groupColumnsRecursive<T>(
8192
columns: Column[],
@@ -98,6 +109,17 @@ function groupColumnsRecursive<T>(
98109

99110
// First pass: categorize columns into groups or standalone (leaf columns)
100111
columns.forEach((col) => {
112+
// Only group columns that came from object expansion (have parentKey)
113+
// Flat columns with dots in their names (e.g., "agents.md") should NOT be grouped
114+
if (!isExpandedColumn(col) && currentDepth === 0) {
115+
// Top-level flat column - render as-is, even if it has dots
116+
result.push({
117+
...createColumnDef(col, col.name),
118+
__order: orderCounter++,
119+
} as ColumnType<T> & {__order: number})
120+
return
121+
}
122+
101123
// Get the relative key (remove parent path prefix if present)
102124
const relativeKey = parentPath ? col.key.substring(parentPath.length + 1) : col.key
103125
const parsed = parseGroupedColumnKey(relativeKey)

web/oss/src/components/pages/testset/modals/CreateTestset.tsx

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,81 @@ const CreateTestset: React.FC<Props> = ({setCurrent, onCancel}) => {
119119
const [previewData, setPreviewData] = useState<GenericObject[]>([])
120120
const setRefreshTrigger = useSetAtom(testsetsRefreshTriggerAtom)
121121

122+
/**
123+
* Parse CSV text properly handling quoted fields with embedded newlines and commas
124+
*/
125+
const parseCSVRows = (text: string): string[][] => {
126+
const rows: string[][] = []
127+
let currentRow: string[] = []
128+
let currentField = ""
129+
let inQuotes = false
130+
let i = 0
131+
132+
while (i < text.length) {
133+
const char = text[i]
134+
135+
if (inQuotes) {
136+
if (char === '"') {
137+
// Check for escaped quote ("")
138+
if (i + 1 < text.length && text[i + 1] === '"') {
139+
currentField += '"'
140+
i += 2
141+
continue
142+
}
143+
// End of quoted field
144+
inQuotes = false
145+
i++
146+
continue
147+
}
148+
// Inside quotes - add character as-is (including newlines)
149+
currentField += char
150+
i++
151+
} else {
152+
if (char === '"') {
153+
// Start of quoted field
154+
inQuotes = true
155+
i++
156+
} else if (char === ",") {
157+
// Field separator
158+
currentRow.push(currentField.trim())
159+
currentField = ""
160+
i++
161+
} else if (char === "\n" || (char === "\r" && text[i + 1] === "\n")) {
162+
// Row separator
163+
currentRow.push(currentField.trim())
164+
if (currentRow.some((field) => field !== "")) {
165+
rows.push(currentRow)
166+
}
167+
currentRow = []
168+
currentField = ""
169+
i += char === "\r" ? 2 : 1
170+
} else if (char === "\r") {
171+
// Handle standalone \r as row separator
172+
currentRow.push(currentField.trim())
173+
if (currentRow.some((field) => field !== "")) {
174+
rows.push(currentRow)
175+
}
176+
currentRow = []
177+
currentField = ""
178+
i++
179+
} else {
180+
currentField += char
181+
i++
182+
}
183+
}
184+
}
185+
186+
// Handle last field and row
187+
if (currentField || currentRow.length > 0) {
188+
currentRow.push(currentField.trim())
189+
if (currentRow.some((field) => field !== "")) {
190+
rows.push(currentRow)
191+
}
192+
}
193+
194+
return rows
195+
}
196+
122197
const parseFileForPreview = async (
123198
file: File,
124199
fileType: "CSV" | "JSON",
@@ -132,12 +207,12 @@ const CreateTestset: React.FC<Props> = ({setCurrent, onCancel}) => {
132207
return parsed.slice(0, maxPreviewRows)
133208
}
134209
} else {
135-
const lines = text.split("\n").filter((line) => line.trim())
136-
if (lines.length > 0) {
137-
const headers = lines[0].split(",").map((h) => h.trim())
210+
const csvRows = parseCSVRows(text)
211+
if (csvRows.length > 0) {
212+
const headers = csvRows[0]
138213
const rows: GenericObject[] = []
139-
for (let i = 1; i < Math.min(lines.length, maxPreviewRows + 1); i++) {
140-
const values = lines[i].split(",").map((v) => v.trim())
214+
for (let i = 1; i < Math.min(csvRows.length, maxPreviewRows + 1); i++) {
215+
const values = csvRows[i]
141216
const row: GenericObject = {}
142217
headers.forEach((header, idx) => {
143218
row[header] = values[idx] || ""

web/oss/src/state/entities/testcase/testcaseEntity.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,12 +976,19 @@ export const testcaseCellAtomFamily = atomFamily(
976976
return undefined
977977
}
978978

979+
// First, try direct key access (handles flat keys with dots like "agents.md")
980+
// This is important because column names can legitimately contain dots
981+
const directValue = (entity as Record<string, unknown>)[column]
982+
if (directValue !== undefined) {
983+
return directValue
984+
}
985+
979986
// Handle nested paths (e.g., "VMs_previous_RFP.event")
980987
// We need to parse JSON strings for nested access
981988
const parts = column.split(".")
982989

983990
if (parts.length === 1) {
984-
// Simple top-level access
991+
// Simple top-level access - already tried above, return undefined
985992
return get(entity, column)
986993
}
987994

0 commit comments

Comments
 (0)