mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 16:01:42 +00:00
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:
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user