diff --git a/src/app/dfSlice.tsx b/src/app/dfSlice.tsx index 70c63cac..d3ca4d22 100644 --- a/src/app/dfSlice.tsx +++ b/src/app/dfSlice.tsx @@ -655,11 +655,12 @@ export const dataFormulatorSlice = createSlice({ if (t.id == tableId) { // Update metadata type inference based on new data let newMetadata = { ...t.metadata }; + const safeNewRows = newRows.filter(Boolean); for (let name of t.names) { - if (newRows.length > 0 && name in newRows[0]) { + if (safeNewRows.length > 0 && name in safeNewRows[0]) { newMetadata[name] = { ...newMetadata[name], - type: inferTypeFromValueArray(newRows.map(r => r[name])), + type: inferTypeFromValueArray(safeNewRows.map(r => r[name])), }; } } @@ -667,7 +668,7 @@ export const dataFormulatorSlice = createSlice({ const updatedSource = t.source ? { ...t.source, lastRefreshed: Date.now() } : undefined; // Use provided content hash (from backend for virtual/DB tables) or compute locally // For virtual tables, backend hash reflects full table; for stream tables, compute from actual rows - const newContentHash = providedContentHash || computeContentHash(newRows, t.names); + const newContentHash = providedContentHash || computeContentHash(safeNewRows, t.names); return { ...t, rows: newRows, metadata: newMetadata, source: updatedSource, contentHash: newContentHash }; } return t; @@ -684,16 +685,17 @@ export const dataFormulatorSlice = createSlice({ const newRows = update.rows; const providedContentHash = update.contentHash; let newMetadata = { ...t.metadata }; + const safeNewRows = newRows.filter(Boolean); for (let name of t.names) { - if (newRows.length > 0 && name in newRows[0]) { + if (safeNewRows.length > 0 && name in safeNewRows[0]) { newMetadata[name] = { ...newMetadata[name], - type: inferTypeFromValueArray(newRows.map(r => r[name])), + type: inferTypeFromValueArray(safeNewRows.map(r => r[name])), }; } } const updatedSource = t.source ? { ...t.source, lastRefreshed: Date.now() } : undefined; - const newContentHash = providedContentHash || computeContentHash(newRows, t.names); + const newContentHash = providedContentHash || computeContentHash(safeNewRows, t.names); return { ...t, rows: newRows, metadata: newMetadata, source: updatedSource, contentHash: newContentHash }; }); }, @@ -751,7 +753,7 @@ export const dataFormulatorSlice = createSlice({ } // Create new rows with the column positioned after the first parent - let newRows = table.rows.map((row, i) => { + let newRows = table.rows.filter(Boolean).map((row, i) => { let newRow: {[key: string]: any} = {}; for (let key of Object.keys(row)) { newRow[key] = row[key]; @@ -782,7 +784,7 @@ export const dataFormulatorSlice = createSlice({ if (fieldIndex != -1) { table.names = table.names.slice(0, fieldIndex).concat(table.names.slice(fieldIndex + 1)); delete table.metadata[fieldName]; - table.rows = table.rows.map(r => { + table.rows = table.rows.filter(Boolean).map(r => { delete r[fieldName]; return r; }); diff --git a/src/app/tableThunks.ts b/src/app/tableThunks.ts index 8a9ea7a6..1fbf279d 100644 --- a/src/app/tableThunks.ts +++ b/src/app/tableThunks.ts @@ -229,7 +229,7 @@ export const loadTable = createAsyncThunk< }); const data = await response.json(); if (data.status === 'success') { - const rows = data.rows; + const rows = (data.rows || []).filter(Boolean); const names = rows.length > 0 ? Object.keys(rows[0]) : []; const totalCount: number = data.total_row_count ?? rows.length; originalRowCount = totalCount; diff --git a/src/components/ComponentType.tsx b/src/components/ComponentType.tsx index 20f55b75..26f14173 100644 --- a/src/components/ComponentType.tsx +++ b/src/components/ComponentType.tsx @@ -158,17 +158,18 @@ export function createDictTable( source: DataSourceConfig | undefined = undefined, ) : DictTable { - let names = Object.keys(rows[0]) + const safeRows = rows.filter(Boolean); + let names = safeRows.length > 0 ? Object.keys(safeRows[0]) : []; return { id, displayId: `${id}`, names, - rows, + rows: safeRows, metadata: names.reduce((acc, name) => ({ ...acc, [name]: { - type: inferTypeFromValueArray(rows.map(r => r[name])), + type: inferTypeFromValueArray(safeRows.map(r => r[name])), semanticType: "", levels: [] } diff --git a/src/views/DBTableManager.tsx b/src/views/DBTableManager.tsx index 264c30a7..3b1f1cec 100644 --- a/src/views/DBTableManager.tsx +++ b/src/views/DBTableManager.tsx @@ -414,7 +414,7 @@ export const DataLoaderForm: React.FC<{ const previewTable: DictTable | null = useMemo(() => { if (!selectedPreviewTable || !tableMetadata[selectedPreviewTable]) return null; const metadata = tableMetadata[selectedPreviewTable]; - const sampleRows = metadata.sample_rows || []; + const sampleRows = (metadata.sample_rows || []).filter(Boolean); const columns = metadata.columns || []; const names = columns.map((c: any) => c.name); return { @@ -644,7 +644,7 @@ export const DataLoaderForm: React.FC<{ } } - const sampleRows = metadata.sample_rows || []; + const sampleRows = (metadata.sample_rows || []).filter(Boolean); const columns = metadata.columns || []; const tableObj: DictTable = { id: tableName.split('.').pop() || tableName, diff --git a/src/views/DataView.tsx b/src/views/DataView.tsx index 8a4d6c06..01be30b8 100644 --- a/src/views/DataView.tsx +++ b/src/views/DataView.tsx @@ -50,9 +50,9 @@ export const FreeDataViewFC: FC = function DataView() { let rowData = []; if (targetTable) { if (targetTable.virtual) { - rowData = targetTable.rows; + rowData = targetTable.rows.filter(Boolean); } else { - rowData = targetTable.rows; + rowData = targetTable.rows.filter(Boolean); rowData = rowData.map((r: any, i: number) => ({ ...r, "#rowId": i })); } } @@ -66,7 +66,7 @@ export const FreeDataViewFC: FC = function DataView() { if (name === "#rowId") return { minWidth: 10, width: 40 }; // Default for row ID column // Get all values for this column from sampled rows - const values = sampledRows.map(row => String(row[name] || '')); + const values = sampledRows.filter(Boolean).map(row => String(row[name] || '')); // Estimate width based on content length (simple approach) const avgLength = values.length > 0 diff --git a/src/views/EncodingBox.tsx b/src/views/EncodingBox.tsx index aa56a34c..7dabe39c 100644 --- a/src/views/EncodingBox.tsx +++ b/src/views/EncodingBox.tsx @@ -279,7 +279,7 @@ export const EncodingBox: FC = function EncodingBox({ channel, let stackOpt: any[] = []; - let domainItems = (field && activeTable) ? activeTable.rows.map(row => row[field!.name]) : []; + let domainItems = (field && activeTable) ? activeTable.rows.filter(Boolean).map(row => row[field!.name]) : []; domainItems = [...new Set(domainItems)]; let autoSortEnabled = field && fieldMetadata?.type == Type.String && domainItems.length < 200; diff --git a/src/views/ReactTable.tsx b/src/views/ReactTable.tsx index a885811f..39e354c8 100644 --- a/src/views/ReactTable.tsx +++ b/src/views/ReactTable.tsx @@ -86,6 +86,7 @@ export const CustomReactTable: React.FC = ({ {rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .filter(Boolean) .map((row, i) => { return ( diff --git a/src/views/RefreshDataDialog.tsx b/src/views/RefreshDataDialog.tsx index 063f7323..6c33595c 100644 --- a/src/views/RefreshDataDialog.tsx +++ b/src/views/RefreshDataDialog.tsx @@ -86,7 +86,11 @@ export const RefreshDataDialog: React.FC = ({ return { valid: false, message: 'No data found in the uploaded content.' }; } - const newColumns = Object.keys(newRows[0]).sort(); + const firstRow = newRows.find(Boolean); + if (!firstRow) { + return { valid: false, message: 'No valid data rows found in the uploaded content.' }; + } + const newColumns = Object.keys(firstRow).sort(); const existingColumns = [...table.names].sort(); if (newColumns.length !== existingColumns.length) { diff --git a/src/views/SelectableDataGrid.tsx b/src/views/SelectableDataGrid.tsx index 252e11dd..e3015e8b 100644 --- a/src/views/SelectableDataGrid.tsx +++ b/src/views/SelectableDataGrid.tsx @@ -262,7 +262,7 @@ export const SelectableDataGrid: React.FC = ({ let theme = useTheme(); - const [rowsToDisplay, setRowsToDisplay] = React.useState(rows); + const [rowsToDisplay, setRowsToDisplay] = React.useState((rows || []).filter(Boolean)); // Initialize as true to cover the initial mount delay const [isLoading, setIsLoading] = React.useState(true); @@ -273,10 +273,11 @@ export const SelectableDataGrid: React.FC = ({ }, []); React.useEffect(() => { + const safeRows = (rows || []).filter(Boolean); if (orderBy && !isLoading) { - setRowsToDisplay(rows.slice().sort(getComparator(order, orderBy))); + setRowsToDisplay(safeRows.slice().sort(getComparator(order, orderBy))); } else { - setRowsToDisplay(rows); + setRowsToDisplay(safeRows); } }, [rows, order, orderBy]) @@ -358,7 +359,7 @@ export const SelectableDataGrid: React.FC = ({ .then(response => response.json()) .then(data => { if (data.status === 'success') { - setRowsToDisplay(data.rows); + setRowsToDisplay((data.rows || []).filter(Boolean)); } // Set loading to false when done setIsLoading(false); @@ -451,6 +452,7 @@ export const SelectableDataGrid: React.FC = ({ ) }} itemContent={(rowIndex, data) => { + if (!data) return null; return ( <> {columnDefs.map((column, colIndex) => { diff --git a/src/views/VisualizationView.tsx b/src/views/VisualizationView.tsx index 6522259b..f377497f 100644 --- a/src/views/VisualizationView.tsx +++ b/src/views/VisualizationView.tsx @@ -119,11 +119,12 @@ export let renderTableChart = ( return encoding.fieldID != undefined; }).map(([channel, encoding]) => conceptShelfItems.find(f => f.id == encoding.fieldID) as FieldItem); - if (fields.length == 0) { - fields = conceptShelfItems.filter(f => Object.keys(extTable[0]).includes(f.name)); + const safeExtTable = extTable.filter(Boolean); + if (fields.length == 0 && safeExtTable.length > 0) { + fields = conceptShelfItems.filter(f => Object.keys(safeExtTable[0]).includes(f.name)); } - let rows = extTable.map(row => Object.fromEntries(fields.filter(f => Object.keys(row).includes(f.name)).map(f => [f.name, row[f.name]]))) + let rows = safeExtTable.map(row => Object.fromEntries(fields.filter(f => Object.keys(row).includes(f.name)).map(f => [f.name, row[f.name]]))) let colDefs = fields.map(field => { let name = field.name; @@ -193,7 +194,8 @@ export let checkChartAvailabilityOnPreparedData = (chart: Chart, conceptShelfIte } return undefined; }).filter((f): f is string => f != undefined); - return visFieldsFinalNames.length > 0 && visTableRows.length > 0 && visFieldsFinalNames.every(name => Object.keys(visTableRows[0]).includes(name)); + const firstRow = visTableRows.find(Boolean); + return visFieldsFinalNames.length > 0 && firstRow != null && visFieldsFinalNames.every(name => Object.keys(firstRow).includes(name)); } export let checkChartAvailability = (chart: Chart, conceptShelfItems: FieldItem[], visTableRows: any[]) => { @@ -201,7 +203,8 @@ export let checkChartAvailability = (chart: Chart, conceptShelfItems: FieldItem[ .filter(key => chart.encodingMap[key as keyof EncodingMap].fieldID != undefined) .map(key => chart.encodingMap[key as keyof EncodingMap].fieldID); let visFields = conceptShelfItems.filter(f => visFieldIds.includes(f.id)); - return visFields.length > 0 && visTableRows.length > 0 && visFields.every(f => Object.keys(visTableRows[0]).includes(f.name)); + const firstRow = visTableRows.find(Boolean); + return visFields.length > 0 && firstRow != null && visFields.every(f => Object.keys(firstRow).includes(f.name)); } export let SampleSizeEditor: FC<{ @@ -592,10 +595,10 @@ export const ChartEditorFC: FC<{}> = function ChartEditorFC({}) { let createVisTableRowsLocal = (rows: any[]) => { if (visFields.length == 0) { - return rows; + return rows.filter(Boolean); } - let filteredRows = rows.map(row => Object.fromEntries(visFields.filter(f => table.names.includes(f.name)).map(f => [f.name, row[f.name]]))); + let filteredRows = rows.filter(Boolean).map(row => Object.fromEntries(visFields.filter(f => table.names.includes(f.name)).map(f => [f.name, row[f.name]]))); let visTable = prepVisTable(filteredRows, conceptShelfItems, focusedChart.encodingMap); if (visTable.length > serverConfig.MAX_DISPLAY_ROWS) { @@ -664,7 +667,7 @@ export const ChartEditorFC: FC<{}> = function ChartEditorFC({}) { if (currentRequestRef.current === requestId) { const versionId = computeVersionId(); if (data.status == "success") { - setVisTableRows(data.rows); + setVisTableRows((data.rows || []).filter(Boolean)); setVisTableTotalRowCount(data.total_row_count); setDataVersion(versionId); // Cache for instant reuse on chart revisit