Skip to content

Commit bd65d52

Browse files
Copilothotlong
andcommitted
fix: centralize extractRecords in @object-ui/core and unify data extraction across all 6 components
- Create shared extractRecords() utility in packages/core/src/utils/extract-records.ts - ObjectMap: was missing results.value extraction - ObjectCalendar: was missing results.records extraction - ObjectGantt: was missing results.value extraction - ObjectTimeline: was missing results.value, had inconsistent pattern - ObjectKanban: was missing results.records extraction - ObjectChart: re-exports from @object-ui/core for backward compatibility - Add 8 unit tests for extractRecords in @object-ui/core - Update ROADMAP.md Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 2fc9fba commit bd65d52

File tree

10 files changed

+99
-74
lines changed

10 files changed

+99
-74
lines changed

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,7 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
854854
- [x] **P1: Opportunity ↔ Contact Junction**`opportunity_contacts` object with role-based relationships + 7 seed records
855855
- [x] **P1: Contact ↔ Event Attendees**`participants` field populated on all event seed data
856856
- [x] **P2: Dashboard Dynamic Data** — "Revenue by Account" widget using `provider: 'object'` aggregation. DashboardRenderer now delegates `provider: 'object'` widgets to ObjectChart (`type: 'object-chart'`) for async data loading + client-side aggregation (sum/count/avg/min/max)
857-
- [x] **P2: Fix Revenue by Account Chart** — Fixed 3 bugs preventing "Revenue by Account" chart from displaying data: (1) ObjectChart `extractRecords()` now handles `results.data` and `results.value` response formats in addition to `results.records`, (2) DashboardRenderer auto-adapts series `dataKey` from `aggregate.field` when aggregate config is present, (3) CRM dashboard `yField` aligned to aggregate field `amount` (was `total`). Added 10 new tests.
857+
- [x] **P2: Fix Revenue by Account Chart** — Fixed 3 bugs preventing "Revenue by Account" chart from displaying data: (1) ObjectChart `extractRecords()` now handles `results.data` and `results.value` response formats in addition to `results.records`, (2) DashboardRenderer auto-adapts series `dataKey` from `aggregate.field` when aggregate config is present, (3) CRM dashboard `yField` aligned to aggregate field `amount` (was `total`). Centralized `extractRecords()` utility in `@object-ui/core` and unified data extraction across all 6 data components (ObjectChart, ObjectMap, ObjectCalendar, ObjectGantt, ObjectTimeline, ObjectKanban). Added 16 new tests.
858858
- [x] **P2: App Branding**`logo`, `favicon`, `backgroundColor` fields on CRM app
859859
- [x] **P3: Pages** — Settings page (utility) and Getting Started page (onboarding)
860860
- [x] **P2: Spec Compliance Audit** — Fixed `variant: 'danger'``'destructive'` (4 actions), `columns: string``number` (33 form sections), added `type: 'dashboard'` to dashboard

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './validation/index.js';
1515
export * from './builder/schema-builder.js';
1616
export * from './utils/filter-converter.js';
1717
export * from './utils/normalize-quick-filter.js';
18+
export * from './utils/extract-records.js';
1819
export * from './evaluator/index.js';
1920
export * from './actions/index.js';
2021
export * from './query/index.js';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect } from 'vitest';
10+
import { extractRecords } from '../extract-records';
11+
12+
describe('extractRecords', () => {
13+
const sampleData = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
14+
15+
it('should return the array directly when results is an array', () => {
16+
expect(extractRecords(sampleData)).toEqual(sampleData);
17+
});
18+
19+
it('should extract from results.records', () => {
20+
expect(extractRecords({ records: sampleData })).toEqual(sampleData);
21+
});
22+
23+
it('should extract from results.data', () => {
24+
expect(extractRecords({ data: sampleData })).toEqual(sampleData);
25+
});
26+
27+
it('should extract from results.value', () => {
28+
expect(extractRecords({ value: sampleData })).toEqual(sampleData);
29+
});
30+
31+
it('should return empty array for null/undefined', () => {
32+
expect(extractRecords(null)).toEqual([]);
33+
expect(extractRecords(undefined)).toEqual([]);
34+
});
35+
36+
it('should return empty array for non-array/non-object', () => {
37+
expect(extractRecords('string')).toEqual([]);
38+
expect(extractRecords(42)).toEqual([]);
39+
});
40+
41+
it('should return empty array for object without recognized keys', () => {
42+
expect(extractRecords({ total: 100 })).toEqual([]);
43+
});
44+
45+
it('should prefer records over data and value', () => {
46+
const records = [{ id: 1 }];
47+
const data = [{ id: 2 }];
48+
expect(extractRecords({ records, data })).toEqual(records);
49+
});
50+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* Extract an array of records from various API response formats.
11+
* Supports: raw array, { records: [] }, { data: [] }, { value: [] }.
12+
*
13+
* This utility normalises the different shapes returned by ObjectStack,
14+
* OData, and MSW mock endpoints so that every data-fetching component
15+
* can rely on a single extraction path.
16+
*/
17+
export function extractRecords(results: unknown): any[] {
18+
if (Array.isArray(results)) {
19+
return results;
20+
}
21+
if (results && typeof results === 'object') {
22+
if (Array.isArray((results as any).records)) {
23+
return (results as any).records;
24+
}
25+
if (Array.isArray((results as any).data)) {
26+
return (results as any).data;
27+
}
28+
if (Array.isArray((results as any).value)) {
29+
return (results as any).value;
30+
}
31+
}
32+
return [];
33+
}

packages/plugin-calendar/src/ObjectCalendar.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { CalendarView, type CalendarEvent } from './CalendarView';
2828
import { usePullToRefresh } from '@object-ui/mobile';
2929
import { useNavigationOverlay } from '@object-ui/react';
3030
import { NavigationOverlay } from '@object-ui/components';
31+
import { extractRecords } from '@object-ui/core';
3132

3233
export interface CalendarSchema {
3334
type: 'calendar';
@@ -219,17 +220,7 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
219220
$orderby: convertSortToQueryParams(schema.sort),
220221
});
221222

222-
let items: any[] = [];
223-
224-
if (Array.isArray(result)) {
225-
items = result;
226-
} else if (result && typeof result === 'object') {
227-
if (Array.isArray((result as any).data)) {
228-
items = (result as any).data;
229-
} else if (Array.isArray((result as any).value)) {
230-
items = (result as any).value;
231-
}
232-
}
223+
let items: any[] = extractRecords(result);
233224

234225
if (isMounted) {
235226
setData(items);

packages/plugin-charts/src/ObjectChart.tsx

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import React, { useState, useEffect } from 'react';
33
import { useDataScope, useSchemaContext } from '@object-ui/react';
44
import { ChartRenderer } from './ChartRenderer';
5-
import { ComponentRegistry } from '@object-ui/core';
5+
import { ComponentRegistry, extractRecords } from '@object-ui/core';
66

77
/**
88
* Client-side aggregation for fetched records.
@@ -49,27 +49,8 @@ export function aggregateRecords(
4949
});
5050
}
5151

52-
/**
53-
* Extract an array of records from various API response formats.
54-
* Supports: raw array, { records: [] }, { data: [] }, { value: [] }.
55-
*/
56-
export function extractRecords(results: unknown): any[] {
57-
if (Array.isArray(results)) {
58-
return results;
59-
}
60-
if (results && typeof results === 'object') {
61-
if (Array.isArray((results as any).records)) {
62-
return (results as any).records;
63-
}
64-
if (Array.isArray((results as any).data)) {
65-
return (results as any).data;
66-
}
67-
if (Array.isArray((results as any).value)) {
68-
return (results as any).value;
69-
}
70-
}
71-
return [];
72-
}
52+
// Re-export extractRecords from @object-ui/core for backward compatibility
53+
export { extractRecords } from '@object-ui/core';
7354

7455
export const ObjectChart = (props: any) => {
7556
const { schema } = props;

packages/plugin-gantt/src/ObjectGantt.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type { ObjectGridSchema, DataSource, ViewData, GanttConfig } from '@objec
2727
import { GanttConfigSchema } from '@objectstack/spec/ui';
2828
import { useNavigationOverlay } from '@object-ui/react';
2929
import { NavigationOverlay } from '@object-ui/components';
30+
import { extractRecords } from '@object-ui/core';
3031
import { GanttView, type GanttTask } from './GanttView';
3132

3233
export interface ObjectGanttProps {
@@ -178,16 +179,7 @@ export const ObjectGantt: React.FC<ObjectGanttProps> = ({
178179
$orderby: convertSortToQueryParams(schema.sort),
179180
});
180181

181-
let items: any[] = [];
182-
if (Array.isArray(result)) {
183-
items = result;
184-
} else if (result && typeof result === 'object') {
185-
if (Array.isArray((result as any).data)) {
186-
items = (result as any).data;
187-
} else if (Array.isArray((result as any).records)) {
188-
items = (result as any).records;
189-
}
190-
}
182+
let items: any[] = extractRecords(result);
191183
setData(items);
192184
} else if (dataConfig?.provider === 'api') {
193185
console.warn('API provider not yet implemented for ObjectGantt');

packages/plugin-kanban/src/ObjectKanban.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React, { useEffect, useState, useMemo } from 'react';
1010
import type { DataSource } from '@object-ui/types';
1111
import { useDataScope, useNavigationOverlay } from '@object-ui/react';
1212
import { NavigationOverlay } from '@object-ui/components';
13+
import { extractRecords } from '@object-ui/core';
1314
import { KanbanRenderer } from './index';
1415
import { KanbanSchema } from './types';
1516

@@ -66,16 +67,7 @@ export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
6667
});
6768

6869
// Handle { value: [] } OData shape or { data: [] } shape or direct array
69-
let data: any[] = [];
70-
if (Array.isArray(results)) {
71-
data = results;
72-
} else if (results && typeof results === 'object') {
73-
if (Array.isArray((results as any).value)) {
74-
data = (results as any).value;
75-
} else if (Array.isArray((results as any).data)) {
76-
data = (results as any).data;
77-
}
78-
}
70+
const data = extractRecords(results);
7971

8072
console.log(`[ObjectKanban] Extracted data (length: ${data.length})`);
8173

packages/plugin-map/src/ObjectMap.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import React, { useEffect, useState, useMemo } from 'react';
2424
import type { ObjectGridSchema, DataSource, ViewData } from '@object-ui/types';
2525
import { useNavigationOverlay } from '@object-ui/react';
2626
import { NavigationOverlay, cn } from '@object-ui/components';
27+
import { extractRecords } from '@object-ui/core';
2728
import { z } from 'zod';
2829
import MapGL, { NavigationControl, Marker, Popup } from 'react-map-gl/maplibre';
2930
import maplibregl from 'maplibre-gl';
@@ -354,16 +355,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
354355
$orderby: convertSortToQueryParams(schema.sort),
355356
});
356357

357-
let items: any[] = [];
358-
if (Array.isArray(result)) {
359-
items = result;
360-
} else if (result && typeof result === 'object') {
361-
if (Array.isArray((result as any).data)) {
362-
items = (result as any).data;
363-
} else if (Array.isArray((result as any).records)) {
364-
items = (result as any).records;
365-
}
366-
}
358+
let items: any[] = extractRecords(result);
367359
setData(items);
368360
} else if (dataConfig?.provider === 'api') {
369361
console.warn('API provider not yet implemented for ObjectMap');

packages/plugin-timeline/src/ObjectTimeline.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React, { useEffect, useState, useCallback } from 'react';
1010
import type { DataSource, TimelineSchema, TimelineConfig } from '@object-ui/types';
1111
import { useDataScope, useNavigationOverlay } from '@object-ui/react';
1212
import { NavigationOverlay } from '@object-ui/components';
13+
import { extractRecords } from '@object-ui/core';
1314
import { usePullToRefresh } from '@object-ui/mobile';
1415
import { z } from 'zod';
1516
import { TimelineRenderer } from './renderer';
@@ -102,16 +103,8 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
102103
const results = await dataSource.find(schema.objectName, {
103104
options: { $top: 100 }
104105
});
105-
let data = results;
106-
if ((results as any).records) {
107-
data = (results as any).records;
108-
} else if ((results as any).data) {
109-
data = (results as any).data;
110-
}
111-
112-
if (Array.isArray(data)) {
113-
setFetchedData(data);
114-
}
106+
const data = extractRecords(results);
107+
setFetchedData(data);
115108
} catch (e) {
116109
console.error(e);
117110
setError(e as Error);

0 commit comments

Comments
 (0)