Skip to content

Commit d7253fc

Browse files
authored
Merge pull request #1103 from objectstack-ai/copilot/fix-columns-normalization
2 parents 5f5dbbe + 538ee63 commit d7253fc

File tree

3 files changed

+130
-2
lines changed

3 files changed

+130
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **ObjectDataTable: columns now support `string[]` shorthand** (`@object-ui/plugin-dashboard`): `ObjectDataTable` now normalizes `columns` entries so that both `string[]` (e.g. `['name', 'close_date']`) and `object[]` formats are accepted. String entries are automatically converted to `{ header, accessorKey }` objects with title-cased headers derived from snake_case and camelCase field names. Previously, passing a `string[]` caused the downstream `data-table` renderer to crash when accessing `col.accessorKey` on a plain string. Mixed arrays (some strings, some objects) are also handled correctly. Includes 8 new unit tests.
13+
1014
### Fixed
1115

1216
- **i18n loadLanguage Not Compatible with Spec REST API Response Format** (`apps/console`): Fixed `loadLanguage` in `apps/console/src/main.tsx` to correctly unwrap the `@objectstack/spec` REST API envelope `{ data: { locale, translations } }`. Previously, the function returned the raw JSON response, which meant `useObjectLabel` and `useObjectTranslation` hooks received a wrapped object instead of the flat translation map, causing business object and field labels (e.g., CRM contact/account) to fall back to English. The fix extracts `data.translations` when the spec envelope is detected, while preserving backward compatibility with mock/dev environments that return flat translation objects. Includes 6 unit tests covering spec envelope unwrapping, flat fallback, HTTP errors, network failures, and edge cases.

packages/plugin-dashboard/src/ObjectDataTable.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,39 @@ export interface ObjectDataTableProps {
2929
className?: string;
3030
}
3131

32+
/** A column definition after normalization, with header and accessor key. */
33+
interface NormalizedColumn {
34+
header: string;
35+
accessorKey: string;
36+
[key: string]: any;
37+
}
38+
39+
/**
40+
* Normalize columns to support both string[] shorthand and object[] formats.
41+
*
42+
* - `string[]` entries are converted to `{ header, accessorKey }` objects,
43+
* handling both snake_case and camelCase for header generation.
44+
* - Object entries are returned as-is.
45+
*/
46+
export function normalizeColumns(columns: (string | Record<string, any>)[]): NormalizedColumn[] {
47+
return columns.map((col) => {
48+
if (typeof col === 'string') {
49+
return {
50+
header: col
51+
// snake_case → spaces
52+
.replace(/_/g, ' ')
53+
// camelCase → spaces before uppercase letters
54+
.replace(/([A-Z])/g, ' $1')
55+
.trim()
56+
// Title Case each word
57+
.replace(/\b\w/g, (c: string) => c.toUpperCase()),
58+
accessorKey: col,
59+
};
60+
}
61+
return col;
62+
});
63+
}
64+
3265
/**
3366
* ObjectDataTable — Async-aware wrapper for data-table.
3467
*
@@ -101,7 +134,9 @@ export const ObjectDataTable: React.FC<ObjectDataTableProps> = ({ schema, dataSo
101134

102135
// Auto-derive columns from data keys when none are provided
103136
const derivedColumns = useMemo(() => {
104-
if (schema.columns && schema.columns.length > 0) return schema.columns;
137+
if (schema.columns && schema.columns.length > 0) {
138+
return normalizeColumns(schema.columns);
139+
}
105140
if (finalData.length === 0) return [];
106141
// Exclude internal/private fields (prefixed with '_') from auto-derived columns
107142
const keys = Object.keys(finalData[0]).filter(k => !k.startsWith('_'));

packages/plugin-dashboard/src/__tests__/ObjectDataTable.test.tsx

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { describe, it, expect, vi } from 'vitest';
1010
import { render, screen, waitFor } from '@testing-library/react';
1111
import React from 'react';
12-
import { ObjectDataTable } from '../ObjectDataTable';
12+
import { ObjectDataTable, normalizeColumns } from '../ObjectDataTable';
1313
import { SchemaRendererProvider } from '@object-ui/react';
1414

1515
describe('ObjectDataTable', () => {
@@ -119,4 +119,93 @@ describe('ObjectDataTable', () => {
119119

120120
expect(dataSource.find).not.toHaveBeenCalled();
121121
});
122+
123+
it('should normalize string[] columns without crashing', () => {
124+
const schema = {
125+
...baseSchema,
126+
columns: ['name', 'amount', 'close_date'],
127+
data: [
128+
{ name: 'Deal A', amount: 1000, close_date: '2025-01-01' },
129+
{ name: 'Deal B', amount: 2000, close_date: '2025-06-01' },
130+
],
131+
};
132+
133+
// Should not crash when columns are strings
134+
const { container } = render(
135+
<SchemaRendererProvider>
136+
<ObjectDataTable schema={schema} />
137+
</SchemaRendererProvider>,
138+
);
139+
140+
expect(container).toBeDefined();
141+
});
142+
143+
it('should pass through object[] columns unchanged', () => {
144+
const schema = {
145+
...baseSchema,
146+
columns: [
147+
{ header: 'Name', accessorKey: 'name' },
148+
{ header: 'Amount', accessorKey: 'amount' },
149+
],
150+
data: [
151+
{ name: 'Deal A', amount: 1000 },
152+
],
153+
};
154+
155+
const { container } = render(
156+
<SchemaRendererProvider>
157+
<ObjectDataTable schema={schema} />
158+
</SchemaRendererProvider>,
159+
);
160+
161+
expect(container).toBeDefined();
162+
});
163+
});
164+
165+
describe('normalizeColumns', () => {
166+
it('should convert snake_case string to title-cased header', () => {
167+
const result = normalizeColumns(['close_date']);
168+
expect(result).toEqual([{ header: 'Close Date', accessorKey: 'close_date' }]);
169+
});
170+
171+
it('should convert camelCase string to title-cased header', () => {
172+
const result = normalizeColumns(['firstName']);
173+
expect(result).toEqual([{ header: 'First Name', accessorKey: 'firstName' }]);
174+
});
175+
176+
it('should convert simple string to capitalized header', () => {
177+
const result = normalizeColumns(['name']);
178+
expect(result).toEqual([{ header: 'Name', accessorKey: 'name' }]);
179+
});
180+
181+
it('should handle multiple string columns', () => {
182+
const result = normalizeColumns(['name', 'total_amount', 'createdAt']);
183+
expect(result).toEqual([
184+
{ header: 'Name', accessorKey: 'name' },
185+
{ header: 'Total Amount', accessorKey: 'total_amount' },
186+
{ header: 'Created At', accessorKey: 'createdAt' },
187+
]);
188+
});
189+
190+
it('should pass through object columns unchanged', () => {
191+
const cols = [
192+
{ header: 'Custom Name', accessorKey: 'name' },
193+
{ header: 'Amount ($)', accessorKey: 'amount' },
194+
];
195+
const result = normalizeColumns(cols);
196+
expect(result).toEqual(cols);
197+
});
198+
199+
it('should handle mixed string and object columns', () => {
200+
const result = normalizeColumns([
201+
'name',
202+
{ header: 'Custom Amount', accessorKey: 'amount' },
203+
'close_date',
204+
]);
205+
expect(result).toEqual([
206+
{ header: 'Name', accessorKey: 'name' },
207+
{ header: 'Custom Amount', accessorKey: 'amount' },
208+
{ header: 'Close Date', accessorKey: 'close_date' },
209+
]);
210+
});
122211
});

0 commit comments

Comments
 (0)