fix: respect explicit table column order for integer-like column names

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-05-18 11:04:20 +02:00
parent 69b3141e03
commit 70e6b2b80c
4 changed files with 82 additions and 24 deletions
@@ -378,7 +378,10 @@
}
}
function handleArrayOfObjectsHeaders(json: any) {
function handleArrayOfObjectsHeaders(json: any): {
objects: any
headersOverride: string[] | undefined
} {
// handle possible a first row of headers
if (
Array.isArray(json) &&
@@ -401,10 +404,10 @@
rows[i - 1] = obj
}
return rows
return { objects: rows, headersOverride: headers }
}
return json
return { objects: json, headersOverride: undefined }
}
type InputObject = { [key: string]: number[] }
@@ -432,7 +435,10 @@
return result
}
function arrayOfRowsToObjects(input: any) {
function arrayOfRowsToObjects(input: any): {
objects: any
headersOverride: string[] | undefined
} {
if (Array.isArray(input) && input.length > 0) {
// handle possible first row of headers
if (
@@ -444,20 +450,23 @@
const headers = input[0]
const rows = input.slice(1)
return rows.map((row) => {
const obj: { [key: string]: string } = {}
return {
objects: rows.map((row) => {
const obj: { [key: string]: string } = {}
for (let i = 0; i < headers.length; i++) {
obj[headers[i]] = row[i]
}
for (let i = 0; i < headers.length; i++) {
obj[headers[i]] = row[i]
}
return obj
})
return obj
}),
headersOverride: headers
}
} else {
return input
return { objects: input, headersOverride: undefined }
}
}
return []
return { objects: [], headersOverride: undefined }
}
export function openDrawer() {
@@ -624,22 +633,26 @@
{:else if !forceJson && resultKind === 'table-row'}
{@const data =
typeof result === 'object' && 'table-row' in result ? result['table-row'] : result}
{@const tableData = arrayOfRowsToObjects(data)}
<AutoDataTable
class={fixTableSizingToParent
? 'absolute inset-0 [&>div]:h-full [&>div]:min-h-[10rem]'
: ''}
objects={arrayOfRowsToObjects(data)}
objects={tableData.objects}
headersOverride={tableData.headersOverride}
/>
{:else if !forceJson && resultKind === 'table-row-object'}
{@const data =
typeof result === 'object' && 'table-row-object' in result
? result['table-row-object']
: result}
{@const tableData = handleArrayOfObjectsHeaders(data)}
<AutoDataTable
class={fixTableSizingToParent
? 'absolute inset-0 [&>div]:h-full [&>div]:min-h-[10rem]'
: ''}
objects={handleArrayOfObjectsHeaders(data)}
objects={tableData.objects}
headersOverride={tableData.headersOverride}
/>
{:else if !forceJson && resultKind === 'html'}
<div class="h-full">
@@ -1016,9 +1029,7 @@
{#if largeObject}
<div class="text-xs text-emphasis"
>{#if resultApiPath && shouldDownloadViaClient()}
<button
onclick={() => downloadViaClient(resultApiPath!, resultDownloadName)}
>
<button onclick={() => downloadViaClient(resultApiPath!, resultDownloadName)}>
Download {filename ? '' : 'as JSON'}
</button>
{:else}
@@ -21,10 +21,11 @@
import DownloadCsv from './DownloadCsv.svelte'
interface Props {
objects?: Array<Record<string, any>>
headersOverride?: string[]
class?: string
}
let { objects = [], class: className }: Props = $props()
let { objects = [], headersOverride, class: className }: Props = $props()
let currentPage = $state(1)
let perPage = $state(25)
@@ -36,8 +37,11 @@
}[] = $state([])
let headers: string[] = $state([])
function recomputeObjectsAndHeaders(objects: Array<Record<string, any>>) {
;[headers, structuredObjects] = computeStructuredObjectsAndHeaders(objects)
function recomputeObjectsAndHeaders(
objects: Array<Record<string, any>>,
headersOverride: string[] | undefined
) {
;[headers, structuredObjects] = computeStructuredObjectsAndHeaders(objects, headersOverride)
}
function adjustCurrentPage() {
@@ -160,7 +164,7 @@
}
}
run(() => {
recomputeObjectsAndHeaders(objects)
recomputeObjectsAndHeaders(objects, headersOverride)
})
run(() => {
perPage && adjustCurrentPage()
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import { computeStructuredObjectsAndHeaders } from './tableUtils'
describe('computeStructuredObjectsAndHeaders', () => {
const rows = [
{ 'Pokemon name': 'Pikachu', Type: 'Electric', '1234': 'nil', 'Main strength': 'Speed' },
{ 'Pokemon name': 'Charizard', Type: 'Fire/Flying', '1234': 'nil', 'Main strength': 'Attack' }
]
it('respects the explicit header order even with integer-like column names', () => {
const [headers] = computeStructuredObjectsAndHeaders(rows, [
'Pokemon name',
'Type',
'1234',
'Main strength'
])
expect(headers).toEqual(['Pokemon name', 'Type', '1234', 'Main strength'])
})
it('keeps extra keys not present in the override (appended at the end)', () => {
const [headers] = computeStructuredObjectsAndHeaders(
[{ 'Pokemon name': 'Pikachu', Type: 'Electric', extra: 'x' }],
['Pokemon name', 'Type']
)
expect(headers).toEqual(['Pokemon name', 'Type', 'extra'])
})
it('without an override, integer-like keys are reordered by Object.keys (documented baseline)', () => {
const [headers] = computeStructuredObjectsAndHeaders(rows)
expect(headers).toEqual(['1234', 'Pokemon name', 'Type', 'Main strength'])
})
it('returns empty arrays for non-array input', () => {
expect(computeStructuredObjectsAndHeaders({} as any)).toEqual([[], []])
})
})
@@ -8,7 +8,10 @@ export function isEmail(value: string) {
return value?.includes('@')
}
export function computeStructuredObjectsAndHeaders(objects: Array<Record<string, any>>): [
export function computeStructuredObjectsAndHeaders(
objects: Array<Record<string, any>>,
headersOverride?: string[]
): [
string[],
{
_id: number
@@ -18,7 +21,10 @@ export function computeStructuredObjectsAndHeaders(objects: Array<Record<string,
if (Array.isArray(objects)) {
let nextId = 1
let hds: string[] = []
// `Object.keys` reorders integer-like keys (e.g. "1234") ahead of
// insertion-ordered keys, so an explicit column order must be passed in
// rather than re-derived from the row objects.
let hds: string[] = headersOverride ? [...headersOverride] : []
let objs = objects.map((obj) => {
let rowData = obj && typeof obj == 'object' ? obj : {}
if (Array.isArray(rowData)) {