Skip to content

Commit 03b883d

Browse files
Copilothotlong
andcommitted
feat(plugin-dashboard): normalize string[] columns in ObjectDataTable
ObjectDataTable now supports both string[] shorthand and object[] formats for column configuration. String entries like 'close_date' are auto-converted to { header: 'Close Date', accessorKey: 'close_date' }. Handles snake_case, camelCase, and mixed arrays. Fixes #303. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/de8fa9da-2f3d-4641-b43f-330b65eee7db
1 parent b16b20b commit 03b883d

3 files changed

Lines changed: 121 additions & 2 deletions

File tree

CHANGELOG.md

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

1010
### Fixed
1111

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+
1214
- **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.
1315

1416
- **List Toolbar Action Buttons Not Responding** (`apps/console`): Fixed a bug where schema-driven action buttons in the upper-right corner of the list view (rendered via `action:bar` with `locations: ['list_toolbar']`) did not respond to clicks. The root cause: the console `ObjectView` component rendered these action buttons via `SchemaRenderer` without wrapping them in an `ActionProvider`. This caused `useAction()` to fall back to a local `ActionRunner` with no handlers, toast, confirmation, or navigation capabilities — so clicks executed silently with no visible effect. Added `ActionProvider` with proper handlers (toast via Sonner, confirmation dialog, navigation via React Router, param collection dialog, and a generic API handler) to the console `ObjectView`, following the same pattern already used in `RecordDetailView`. Includes 4 new integration tests validating the full action chain (render, confirm, execute, cancel).

packages/plugin-dashboard/src/ObjectDataTable.tsx

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

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

102128
// Auto-derive columns from data keys when none are provided
103129
const derivedColumns = useMemo(() => {
104-
if (schema.columns && schema.columns.length > 0) return schema.columns;
130+
if (schema.columns && schema.columns.length > 0) {
131+
return normalizeColumns(schema.columns);
132+
}
105133
if (finalData.length === 0) return [];
106134
// Exclude internal/private fields (prefixed with '_') from auto-derived columns
107135
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)