Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,7 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
- [x] **P2: Full Examples Metadata Audit** — Systematic spec compliance audit across all 4 examples: added `type: 'dashboard'` + `description` to todo/kitchen-sink dashboards, refactored msw-todo to use `ObjectSchema.create` + `Field.*` with snake_case field names, added explicit views to kitchen-sink and msw-todo, added missing `successMessage` on CRM opportunity action, 21 automated compliance tests
- [x] **P2: CRM Dashboard Full provider:'object' Adaptation** — Converted all chart and table widgets in CRM dashboard from static `provider: 'value'` to dynamic `provider: 'object'` with aggregation configs. 12 widgets total: 4 KPI metrics (static), 7 charts (sum/count/avg/max aggregation across opportunity, product, order objects), 1 table (dynamic fetch). Cross-object coverage (order), diverse aggregate functions (sum, count, avg, max). Fixed table `close_date` field alignment. Added i18n for 2 new widgets (10 locales). 9 new CRM metadata tests, 6 new DashboardRenderer rendering tests (area/donut/line/cross-object + edge cases). All provider:'object' paths covered.
- [x] **P1: Dashboard provider:'object' Crash & Blank Rendering Fixes** — Fixed 3 critical bugs causing all charts to be blank and tables to crash on provider:'object' dashboards: (1) DashboardRenderer `...options` spread was leaking provider config objects as `data` in data-table and pivot schemas — fixed by destructuring `data` out before spread, (2) DataTableRenderer and PivotTable now guard with `Array.isArray()` for graceful degradation when non-array data arrives, (3) ObjectChart now shows visible loading/warning messages instead of silently rendering blank when `dataSource` is missing. Also added provider:'object' support to DashboardGridLayout (charts, tables, pivots). 2 new regression tests.
- [x] **P1: Dashboard Widget Data Blank — useDataScope/dataSource Injection Fix** — Fixed root cause of dashboard widgets showing blank data with no server requests: `useDataScope(undefined)` was returning the full context `dataSource` (service adapter) instead of `undefined` when no bind path was given, causing ObjectChart and all data components (ObjectKanban, ObjectGallery, ObjectTimeline, ObjectGrid) to treat the adapter as pre-bound data and skip async fetching. Fixed `useDataScope` to return `undefined` when no path is provided. Also improved ObjectChart fault tolerance: uses `useContext` directly instead of `useSchemaContext` (no throw without provider), validates `dataSource.find` is callable before invoking. 14 new tests (7 useDataScope + 7 ObjectChart data fetch/fault tolerance).
- [x] **P1: URL-Driven Debug/Developer Panel** — Universal debug mode activated via `?__debug` URL parameter (amis devtools-style). `@object-ui/core`: exported `DebugFlags`, `DebugCollector` (perf/expr/event data collection, tree-shakeable), `parseDebugFlags()`, enhanced `isDebugEnabled()` (URL → globalThis → env resolution, SSR-safe). `@object-ui/react`: `useDebugMode` hook with URL detection, Ctrl+Shift+D shortcut, manual toggle; `SchemaRendererContext` extended with `debugFlags`; `SchemaRenderer` injects `data-debug-type`/`data-debug-id` attrs + reports render perf to `DebugCollector` when debug enabled. `@object-ui/components`: floating `DebugPanel` with 7 built-in tabs (Schema, Data, Perf, Expr, Events, Registry, Flags), plugin-extensible via `extraTabs`. Console `MetadataInspector` auto-opens when `?__debug` detected. Fine-grained sub-flags: `?__debug_schema`, `?__debug_perf`, `?__debug_data`, `?__debug_expr`, `?__debug_events`, `?__debug_registry`. 48 new tests.

### Ecosystem & Marketplace
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-calendar/src/ObjectCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
}
}

if (!dataSource) {
if (!dataSource || typeof dataSource.find !== 'function') {
throw new Error('DataSource required for object/api providers');
}

Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-calendar/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import './calendar-view-renderer';

// Register object-calendar component
export const ObjectCalendarRenderer: React.FC<{ schema: any; [key: string]: any }> = ({ schema, data: _data, loading: _loading, ...props }) => {
const { dataSource } = useSchemaContext();
const { dataSource } = useSchemaContext() || {};
return <ObjectCalendar schema={schema} dataSource={dataSource} {...props} />;
Comment on lines 27 to 29
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useSchemaContext() throws when no SchemaRendererProvider is present, so useSchemaContext() || {} will still throw and the || {} fallback is never reached. If the goal is graceful degradation, use useContext(SchemaRendererContext) with optional chaining (like ObjectChart/ObjectGallery) or introduce a non-throwing optional hook.

Copilot uses AI. Check for mistakes.
};

Expand Down
10 changes: 5 additions & 5 deletions packages/plugin-charts/src/ObjectChart.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import React, { useState, useEffect } from 'react';
import { useDataScope, useSchemaContext } from '@object-ui/react';
import React, { useState, useEffect, useContext } from 'react';
import { useDataScope, SchemaRendererContext } from '@object-ui/react';
import { ChartRenderer } from './ChartRenderer';
import { ComponentRegistry, extractRecords } from '@object-ui/core';

Expand Down Expand Up @@ -54,8 +54,8 @@ export { extractRecords } from '@object-ui/core';

export const ObjectChart = (props: any) => {
const { schema } = props;
const context = useSchemaContext();
const dataSource = props.dataSource || context.dataSource;
const context = useContext(SchemaRendererContext);
const dataSource = props.dataSource || context?.dataSource;
const boundData = useDataScope(schema.bind);

const [fetchedData, setFetchedData] = useState<any[]>([]);
Expand All @@ -64,7 +64,7 @@ export const ObjectChart = (props: any) => {
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
if (!dataSource || !schema.objectName) return;
if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
if (isMounted) setLoading(true);
Comment on lines 66 to 68
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With SchemaRendererProvider sometimes receiving {} as dataSource, this guard will skip fetching (good), but the component’s empty-state message later only checks !dataSource (so a truthy-but-invalid dataSource still renders a blank chart with no warning). Consider aligning the UI/empty-state logic to also treat typeof dataSource.find !== 'function' as “no data source available”.

Copilot uses AI. Check for mistakes.
try {
const results = await dataSource.find(schema.objectName, {
Expand Down
179 changes: 179 additions & 0 deletions packages/plugin-charts/src/__tests__/ObjectChart.dataFetch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* Tests for ObjectChart data fetching & fault tolerance.
*
* Verifies that ObjectChart:
* - Calls dataSource.find() when objectName is set and no bound data
* - Handles missing/invalid dataSource gracefully
* - Works without a SchemaRendererProvider
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import React from 'react';
import { SchemaRendererProvider } from '@object-ui/react';
import { ObjectChart } from '../ObjectChart';

// Suppress console.error from React error boundary / fetch errors
const originalConsoleError = console.error;
beforeEach(() => {
console.error = vi.fn();
});
afterEach(() => {
console.error = originalConsoleError;
});

describe('ObjectChart data fetching', () => {
it('should call dataSource.find when objectName is set and no bind path', async () => {
const mockFind = vi.fn().mockResolvedValue([
{ stage: 'Prospect', amount: 100 },
{ stage: 'Proposal', amount: 200 },
]);
const dataSource = { find: mockFind };

render(
<SchemaRendererProvider dataSource={dataSource}>
<ObjectChart
schema={{
type: 'object-chart',
objectName: 'opportunity',
chartType: 'bar',
xAxisKey: 'stage',
series: [{ dataKey: 'amount' }],
}}
/>
</SchemaRendererProvider>
);

await waitFor(() => {
expect(mockFind).toHaveBeenCalledWith('opportunity', { $filter: undefined });
});
});

it('should NOT call dataSource.find when schema.data is provided', () => {
const mockFind = vi.fn();
const dataSource = { find: mockFind };

render(
<SchemaRendererProvider dataSource={dataSource}>
<ObjectChart
schema={{
type: 'object-chart',
objectName: 'opportunity',
chartType: 'bar',
data: [{ stage: 'A', amount: 100 }],
xAxisKey: 'stage',
series: [{ dataKey: 'amount' }],
}}
/>
</SchemaRendererProvider>
);

expect(mockFind).not.toHaveBeenCalled();
});

it('should apply aggregation to fetched data', async () => {
const mockFind = vi.fn().mockResolvedValue([
{ stage: 'Prospect', amount: 100 },
{ stage: 'Prospect', amount: 200 },
{ stage: 'Proposal', amount: 300 },
]);
const dataSource = { find: mockFind };

const { container } = render(
<SchemaRendererProvider dataSource={dataSource}>
<ObjectChart
schema={{
type: 'object-chart',
objectName: 'opportunity',
chartType: 'bar',
xAxisKey: 'stage',
series: [{ dataKey: 'amount' }],
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
}}
/>
</SchemaRendererProvider>
);

await waitFor(() => {
expect(mockFind).toHaveBeenCalled();
});
});
});

describe('ObjectChart fault tolerance', () => {
it('should not crash when dataSource has no find method', () => {
const { container } = render(
<SchemaRendererProvider dataSource={{}}>
<ObjectChart
schema={{
type: 'object-chart',
objectName: 'opportunity',
chartType: 'bar',
xAxisKey: 'stage',
series: [{ dataKey: 'amount' }],
}}
/>
</SchemaRendererProvider>
);

// Should render without crashing
expect(container).toBeDefined();
});

it('should not crash when rendered outside SchemaRendererProvider', () => {
const { container } = render(
<ObjectChart
schema={{
type: 'object-chart',
chartType: 'bar',
xAxisKey: 'stage',
series: [{ dataKey: 'amount' }],
}}
/>
);

// Should render without crashing
expect(container).toBeDefined();
});

it('should show "No data source available" when no dataSource and objectName set', () => {
const { container } = render(
<ObjectChart
schema={{
type: 'object-chart',
objectName: 'opportunity',
chartType: 'bar',
xAxisKey: 'stage',
series: [{ dataKey: 'amount' }],
}}
/>
);

expect(container.textContent).toContain('No data source available');
});

it('should use dataSource prop over context when both are present', async () => {
const contextFind = vi.fn().mockResolvedValue([]);
const propFind = vi.fn().mockResolvedValue([{ stage: 'A', amount: 1 }]);

render(
<SchemaRendererProvider dataSource={{ find: contextFind }}>
<ObjectChart
dataSource={{ find: propFind }}
schema={{
type: 'object-chart',
objectName: 'opportunity',
chartType: 'bar',
xAxisKey: 'stage',
series: [{ dataKey: 'amount' }],
}}
/>
</SchemaRendererProvider>
);

await waitFor(() => {
expect(propFind).toHaveBeenCalled();
});
expect(contextFind).not.toHaveBeenCalled();
});
});
2 changes: 1 addition & 1 deletion packages/plugin-detail/src/RelatedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
React.useEffect(() => {
if (api && !data.length) {
setLoading(true);
if (dataSource) {
if (dataSource && typeof dataSource.find === 'function') {
dataSource.find(api).then((result) => {
const items = Array.isArray(result)
? result
Expand Down
3 changes: 1 addition & 2 deletions packages/plugin-gantt/src/ObjectGantt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export const ObjectGantt: React.FC<ObjectGanttProps> = ({
return;
}

if (!dataSource) {
if (!dataSource || typeof dataSource.find !== 'function') {
throw new Error('DataSource required for object/api providers');
}

Expand All @@ -178,7 +178,6 @@ export const ObjectGantt: React.FC<ObjectGanttProps> = ({
$filter: schema.filter,
$orderby: convertSortToQueryParams(schema.sort),
});

let items: any[] = extractRecords(result);
setData(items);
} else if (dataConfig?.provider === 'api') {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-gantt/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type { GanttViewProps, GanttTask, GanttViewMode } from './GanttView';

// Register component
export const ObjectGanttRenderer: React.FC<{ schema: any }> = ({ schema }) => {
const { dataSource } = useSchemaContext();
const { dataSource } = useSchemaContext() || {};
return <ObjectGantt schema={schema} dataSource={dataSource} />;
Comment on lines 22 to 24
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useSchemaContext() throws outside SchemaRendererProvider, so useSchemaContext() || {} does not provide a fallback and can’t prevent runtime errors in Storybook/standalone usage. Consider switching to useContext(SchemaRendererContext) (optional) or adding a dedicated non-throwing hook for bridge renderers.

Copilot uses AI. Check for mistakes.
};

Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-kanban/src/ObjectKanban.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
if (!dataSource || !schema.objectName) return;
if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
if (isMounted) setLoading(true);
try {
const results = await dataSource.find(schema.objectName, {
Expand Down
10 changes: 5 additions & 5 deletions packages/plugin-list/src/ObjectGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* LICENSE file in the root directory of this source tree.
*/

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useDataScope, useSchemaContext, useNavigationOverlay } from '@object-ui/react';
import React, { useState, useEffect, useCallback, useMemo, useContext } from 'react';
import { useDataScope, SchemaRendererContext, useNavigationOverlay } from '@object-ui/react';
import { ComponentRegistry } from '@object-ui/core';
import { cn, Card, CardContent, NavigationOverlay } from '@object-ui/components';
import type { GalleryConfig, ViewNavigationConfig, GroupingConfig } from '@object-ui/types';
Expand Down Expand Up @@ -52,8 +52,8 @@ const ASPECT_CLASSES: Record<NonNullable<GalleryConfig['cardSize']>, string> = {

export const ObjectGallery: React.FC<ObjectGalleryProps> = (props) => {
const { schema } = props;
const context = useSchemaContext();
const dataSource = props.dataSource || context.dataSource;
const context = useContext(SchemaRendererContext);
const dataSource = props.dataSource || context?.dataSource;
const boundData = useDataScope(schema.bind);

const [fetchedData, setFetchedData] = useState<Record<string, unknown>[]>([]);
Expand Down Expand Up @@ -83,7 +83,7 @@ export const ObjectGallery: React.FC<ObjectGalleryProps> = (props) => {
}

const fetchData = async () => {
if (!dataSource || !schema.objectName) return;
if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
if (isMounted) setLoading(true);
try {
const results = await dataSource.find(schema.objectName, {
Expand Down
13 changes: 8 additions & 5 deletions packages/plugin-list/src/__tests__/GalleryGrouping.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ const mockNavigationOverlay = {
open: vi.fn(),
};

vi.mock('@object-ui/react', () => ({
useDataScope: () => undefined,
useSchemaContext: () => ({ dataSource: undefined }),
useNavigationOverlay: () => mockNavigationOverlay,
}));
vi.mock('@object-ui/react', () => {
const React = require('react');
return {
useDataScope: () => undefined,
SchemaRendererContext: React.createContext(null),
useNavigationOverlay: () => mockNavigationOverlay,
};
});

vi.mock('@object-ui/components', () => ({
cn: (...args: any[]) => args.filter(Boolean).join(' '),
Expand Down
13 changes: 8 additions & 5 deletions packages/plugin-list/src/__tests__/ObjectGallery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ const mockNavigationOverlay = {
open: vi.fn(),
};

vi.mock('@object-ui/react', () => ({
useDataScope: () => undefined,
useSchemaContext: () => ({ dataSource: undefined }),
useNavigationOverlay: () => mockNavigationOverlay,
}));
vi.mock('@object-ui/react', () => {
const React = require('react');
return {
useDataScope: () => undefined,
SchemaRendererContext: React.createContext(null),
useNavigationOverlay: () => mockNavigationOverlay,
};
});

vi.mock('@object-ui/components', () => ({
cn: (...args: any[]) => args.filter(Boolean).join(' '),
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-map/src/ObjectMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
return;
}

if (!dataSource) {
if (!dataSource || typeof dataSource.find !== 'function') {
throw new Error('DataSource required for object/api providers');
}

Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-map/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type { ObjectMapProps };

// Register component
export const ObjectMapRenderer: React.FC<any> = ({ schema, ...props }) => {
const { dataSource } = useSchemaContext();
const { dataSource } = useSchemaContext() || {};
return <ObjectMap schema={schema} dataSource={dataSource} {...props} />;
Comment on lines 19 to 21
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useSchemaContext() throws when there is no SchemaRendererProvider, so useSchemaContext() || {} will still throw and never fall back to {}. If this renderer is meant to be safe without a provider, read from SchemaRendererContext via useContext with ?.dataSource (and let props.dataSource override).

Copilot uses AI. Check for mistakes.
};

Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-timeline/src/ObjectTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({

useEffect(() => {
const fetchData = async () => {
if (!dataSource || !schema.objectName) return;
if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
setLoading(true);
try {
const results = await dataSource.find(schema.objectName, {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-timeline/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ import { useSchemaContext } from '@object-ui/react';

// Register object-timeline component
export const ObjectTimelineRenderer: React.FC<any> = ({ schema, ...props }) => {
const { dataSource } = useSchemaContext();
const { dataSource } = useSchemaContext() || {};
return <ObjectTimeline schema={schema} dataSource={dataSource} {...props} />;
Comment on lines 311 to 313
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useSchemaContext() throws outside SchemaRendererProvider, so useSchemaContext() || {} is ineffective (the fallback is unreachable). To actually avoid crashes when the provider is missing, use useContext(SchemaRendererContext) with optional chaining or provide a non-throwing context hook for these bridge renderers.

Copilot uses AI. Check for mistakes.
};

Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/context/SchemaRendererContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export const useSchemaContext = () => {
export const useDataScope = (path?: string) => {
const context = useContext(SchemaRendererContext);
const dataSource = context?.dataSource;
if (!dataSource || !path) return dataSource;
if (!path) return undefined;
if (!dataSource) return undefined;
// Simple path resolution for now. In real app might be more complex
return path.split('.').reduce((acc, part) => acc && acc[part], dataSource);
}
Loading