From 00769cf12ef966df48d61407cf60199d329203d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 06:53:23 +0000 Subject: [PATCH 1/3] Initial plan From af970a4459a68dfe660610e902b93dd2fe7ef256 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:05:56 +0000 Subject: [PATCH 2/3] feat: implement P0 v1.0 UI Essentials spec compliance - Export TimelineConfig and TimelineConfigSchema from @object-ui/types (#64) - Export NavigationConfig and NavigationConfigSchema from @object-ui/types - Update ObjectTimeline to use spec-compliant startDateField with backward compat (#72) - Add endDateField, groupByField, colorField, scale to ObjectTimeline - Add navigation support to ObjectGallery via useNavigationOverlay (#66) - Implement navigation.view property in useNavigationOverlay hook (#68) - Implement emptyState spec property in ListView (#71) - Add emptyState to ListViewSchema type - Add tests for all new functionality Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../src/custom/navigation-overlay.tsx | 2 + packages/plugin-list/src/ListView.tsx | 30 ++-- packages/plugin-list/src/ObjectGallery.tsx | 149 +++++++++++------- .../src/__tests__/ListView.test.tsx | 40 +++++ .../src/__tests__/ObjectGallery.test.tsx | 96 +++++++++++ .../src/ObjectTimeline.test.tsx | 58 ++++++- .../plugin-timeline/src/ObjectTimeline.tsx | 32 +++- .../__tests__/useNavigationOverlay.test.ts | 60 +++++++ .../react/src/hooks/useNavigationOverlay.ts | 13 +- packages/types/src/index.ts | 4 + packages/types/src/objectql.ts | 13 ++ 11 files changed, 419 insertions(+), 78 deletions(-) create mode 100644 packages/plugin-list/src/__tests__/ObjectGallery.test.tsx diff --git a/packages/components/src/custom/navigation-overlay.tsx b/packages/components/src/custom/navigation-overlay.tsx index 39886a3fd..20e175eb9 100644 --- a/packages/components/src/custom/navigation-overlay.tsx +++ b/packages/components/src/custom/navigation-overlay.tsx @@ -89,6 +89,8 @@ export interface NavigationOverlayProps { width?: string | number; /** Whether navigation is an overlay mode */ isOverlay: boolean; + /** Target view/form name from NavigationConfig */ + view?: string; /** Title for the overlay header */ title?: string; /** Description for the overlay header */ diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index bad8b1784..559867fc9 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -9,7 +9,7 @@ import * as React from 'react'; import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder, SortBuilder, NavigationOverlay } from '@object-ui/components'; import type { SortItem } from '@object-ui/components'; -import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Group, Paintbrush, Ruler } from 'lucide-react'; +import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Group, Paintbrush, Ruler, Inbox } from 'lucide-react'; import type { FilterGroup } from '@object-ui/components'; import { ViewSwitcher, ViewType } from './ViewSwitcher'; import { SchemaRenderer, useNavigationOverlay } from '@object-ui/react'; @@ -229,7 +229,7 @@ export const ListView: React.FC = ({ } // Check for Timeline capabilities - if (schema.options?.timeline?.dateField || schema.options?.calendar?.startDateField) { + if (schema.options?.timeline?.startDateField || schema.options?.timeline?.dateField || schema.options?.calendar?.startDateField) { views.push('timeline'); } @@ -351,7 +351,7 @@ export const ListView: React.FC = ({ return { type: 'object-timeline', ...baseProps, - dateField: schema.options?.timeline?.dateField || 'created_at', + startDateField: schema.options?.timeline?.startDateField || schema.options?.timeline?.dateField || 'created_at', titleField: schema.options?.timeline?.titleField || 'name', ...(schema.options?.timeline || {}), }; @@ -592,12 +592,24 @@ export const ListView: React.FC = ({ {/* View Content */}
- + {!loading && data.length === 0 ? ( +
+ +

+ {(typeof schema.emptyState?.title === 'string' ? schema.emptyState.title : undefined) ?? 'No items found'} +

+

+ {(typeof schema.emptyState?.message === 'string' ? schema.emptyState.message : undefined) ?? 'There are no records to display. Try adjusting your filters or adding new data.'} +

+
+ ) : ( + + )}
{/* Navigation Overlay (drawer/modal/popover) */} diff --git a/packages/plugin-list/src/ObjectGallery.tsx b/packages/plugin-list/src/ObjectGallery.tsx index de3cf4792..79970ccec 100644 --- a/packages/plugin-list/src/ObjectGallery.tsx +++ b/packages/plugin-list/src/ObjectGallery.tsx @@ -7,10 +7,10 @@ */ import React, { useState, useEffect } from 'react'; -import { useDataScope, useSchemaContext } from '@object-ui/react'; +import { useDataScope, useSchemaContext, useNavigationOverlay } from '@object-ui/react'; import { ComponentRegistry } from '@object-ui/core'; -import { cn, Card, CardContent } from '@object-ui/components'; -import type { GalleryConfig } from '@object-ui/types'; +import { cn, Card, CardContent, NavigationOverlay } from '@object-ui/components'; +import type { GalleryConfig, ViewNavigationConfig } from '@object-ui/types'; export interface ObjectGalleryProps { schema: { @@ -20,6 +20,8 @@ export interface ObjectGalleryProps { data?: Record[]; className?: string; gallery?: GalleryConfig; + /** Navigation config for item click behavior */ + navigation?: ViewNavigationConfig; /** @deprecated Use gallery.coverField instead */ imageField?: string; /** @deprecated Use gallery.titleField instead */ @@ -29,6 +31,8 @@ export interface ObjectGalleryProps { data?: Record[]; dataSource?: { find: (name: string, query: unknown) => Promise }; onCardClick?: (record: Record) => void; + /** Callback when a row/item is clicked (overrides NavigationConfig) */ + onRowClick?: (record: Record) => void; } const GRID_CLASSES: Record, string> = { @@ -52,6 +56,13 @@ export const ObjectGallery: React.FC = (props) => { const [fetchedData, setFetchedData] = useState[]>([]); const [loading, setLoading] = useState(false); + // --- NavigationConfig support --- + const navigation = useNavigationOverlay({ + navigation: schema.navigation, + objectName: schema.objectName, + onRowClick: props.onRowClick ?? props.onCardClick, + }); + // Resolve GalleryConfig with backwards-compatible fallbacks const gallery = schema.gallery; const coverField = gallery?.coverField ?? schema.imageField ?? 'image'; @@ -110,66 +121,84 @@ export const ObjectGallery: React.FC = (props) => { if (!items.length) return
No items to display
; return ( -
- {items.map((item, i) => { - const id = (item._id ?? item.id ?? i) as string | number; - const title = String(item[titleField] ?? 'Untitled'); - const imageUrl = item[coverField] as string | undefined; - - return ( - props.onCardClick!(item) : undefined} - > -
- {imageUrl ? ( - {title} - ) : ( -
- - {title[0]?.toUpperCase()} + <> +
+ {items.map((item, i) => { + const id = (item._id ?? item.id ?? i) as string | number; + const title = String(item[titleField] ?? 'Untitled'); + const imageUrl = item[coverField] as string | undefined; + + return ( + navigation.handleClick(item)} + > +
+ {imageUrl ? ( + {title} + ) : ( +
+ + {title[0]?.toUpperCase()} + +
+ )} +
+ +

+ {title} +

+ {visibleFields && visibleFields.length > 0 && ( +
+ {visibleFields.map((field) => { + const value = item[field]; + if (value == null) return null; + return ( +

+ {String(value)} +

+ ); + })} +
+ )} +
+
+ ); + })} +
+ {navigation.isOverlay && ( + + {(record) => ( +
+ {Object.entries(record).map(([key, value]) => ( +
+ + {key.replace(/_/g, ' ')} + {String(value ?? '—')}
- )} + ))}
- -

- {title} -

- {visibleFields && visibleFields.length > 0 && ( -
- {visibleFields.map((field) => { - const value = item[field]; - if (value == null) return null; - return ( -

- {String(value)} -

- ); - })} -
- )} -
- - ); - })} -
+ )} + + )} + ); }; diff --git a/packages/plugin-list/src/__tests__/ListView.test.tsx b/packages/plugin-list/src/__tests__/ListView.test.tsx index 42aaa3ee0..fa3756ea0 100644 --- a/packages/plugin-list/src/__tests__/ListView.test.tsx +++ b/packages/plugin-list/src/__tests__/ListView.test.tsx @@ -221,4 +221,44 @@ describe('ListView', () => { fireEvent.click(clearButton); } }); + + it('should show default empty state when no data', async () => { + mockDataSource.find.mockResolvedValue([]); + const schema: ListViewSchema = { + type: 'list-view', + objectName: 'contacts', + viewType: 'grid', + fields: ['name', 'email'], + }; + + renderWithProvider(); + + // Wait for data fetch to complete + await vi.waitFor(() => { + expect(screen.getByTestId('empty-state')).toBeInTheDocument(); + }); + expect(screen.getByText('No items found')).toBeInTheDocument(); + }); + + it('should show custom empty state when configured', async () => { + mockDataSource.find.mockResolvedValue([]); + const schema: ListViewSchema = { + type: 'list-view', + objectName: 'contacts', + viewType: 'grid', + fields: ['name', 'email'], + emptyState: { + title: 'No contacts yet', + message: 'Add your first contact to get started.', + }, + }; + + renderWithProvider(); + + await vi.waitFor(() => { + expect(screen.getByTestId('empty-state')).toBeInTheDocument(); + }); + expect(screen.getByText('No contacts yet')).toBeInTheDocument(); + expect(screen.getByText('Add your first contact to get started.')).toBeInTheDocument(); + }); }); diff --git a/packages/plugin-list/src/__tests__/ObjectGallery.test.tsx b/packages/plugin-list/src/__tests__/ObjectGallery.test.tsx new file mode 100644 index 000000000..5e57b184c --- /dev/null +++ b/packages/plugin-list/src/__tests__/ObjectGallery.test.tsx @@ -0,0 +1,96 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { ObjectGallery } from '../ObjectGallery'; + +// Mock useDataScope, useSchemaContext, and useNavigationOverlay +const mockHandleClick = vi.fn(); +const mockNavigationOverlay = { + isOverlay: false, + handleClick: mockHandleClick, + selectedRecord: null, + isOpen: false, + close: vi.fn(), + setIsOpen: vi.fn(), + mode: 'page' as const, + width: undefined, + view: undefined, + open: vi.fn(), +}; + +vi.mock('@object-ui/react', () => ({ + useDataScope: () => undefined, + useSchemaContext: () => ({ dataSource: undefined }), + useNavigationOverlay: () => mockNavigationOverlay, +})); + +vi.mock('@object-ui/components', () => ({ + cn: (...args: any[]) => args.filter(Boolean).join(' '), + Card: ({ children, onClick, ...props }: any) => ( +
{children}
+ ), + CardContent: ({ children, ...props }: any) =>
{children}
, + NavigationOverlay: ({ children, selectedRecord }: any) => ( + selectedRecord ?
{children(selectedRecord)}
: null + ), +})); + +vi.mock('@object-ui/core', () => ({ + ComponentRegistry: { register: vi.fn() }, +})); + +const mockItems = [ + { id: '1', name: 'Item 1', image: 'https://example.com/1.jpg' }, + { id: '2', name: 'Item 2', image: 'https://example.com/2.jpg' }, +]; + +describe('ObjectGallery', () => { + it('renders gallery items', () => { + const schema = { objectName: 'products' }; + render(); + expect(screen.getByText('Item 1')).toBeInTheDocument(); + expect(screen.getByText('Item 2')).toBeInTheDocument(); + }); + + it('calls navigation.handleClick on card click', () => { + const schema = { + objectName: 'products', + navigation: { mode: 'drawer' as const }, + }; + render(); + + const cards = screen.getAllByTestId('gallery-card'); + fireEvent.click(cards[0]); + + expect(mockHandleClick).toHaveBeenCalledWith(mockItems[0]); + }); + + it('renders with cursor-pointer when navigation is configured', () => { + const schema = { + objectName: 'products', + navigation: { mode: 'drawer' as const }, + }; + render(); + + const cards = screen.getAllByTestId('gallery-card'); + expect(cards.length).toBe(2); + }); + + it('renders with cursor-pointer when onCardClick is provided', () => { + const onCardClick = vi.fn(); + const schema = { objectName: 'products' }; + render(); + + const cards = screen.getAllByTestId('gallery-card'); + fireEvent.click(cards[0]); + + expect(mockHandleClick).toHaveBeenCalled(); + }); +}); diff --git a/packages/plugin-timeline/src/ObjectTimeline.test.tsx b/packages/plugin-timeline/src/ObjectTimeline.test.tsx index 59b1a306a..c5c9dcd3d 100644 --- a/packages/plugin-timeline/src/ObjectTimeline.test.tsx +++ b/packages/plugin-timeline/src/ObjectTimeline.test.tsx @@ -15,6 +15,7 @@ vi.mock('@object-ui/react', () => ({ close: vi.fn(), setIsOpen: vi.fn(), mode: 'overlay', + view: undefined, }), })); @@ -66,7 +67,7 @@ describe('ObjectTimeline', () => { type: 'timeline', objectName: 'events', titleField: 'name', - dateField: 'date' // Mapping needs to be correct in logic + dateField: 'date' // Backward-compat: legacy dateField still works }; render(); @@ -79,4 +80,59 @@ describe('ObjectTimeline', () => { expect(screen.getByText('Event 1')).toBeDefined(); }); }); + + it('uses spec-compliant startDateField property', async () => { + const dataWithDates = [ + { id: '1', name: 'Sprint 1', start_date: '2024-01-01', end_date: '2024-01-14', team: 'Alpha' }, + ]; + (mockDataSource.find as any).mockResolvedValue(dataWithDates); + + const schema: any = { + type: 'timeline', + objectName: 'sprints', + titleField: 'name', + startDateField: 'start_date', + endDateField: 'end_date', + }; + + render(); + + await waitFor(() => { + expect(screen.getByText('Sprint 1')).toBeDefined(); + }); + }); + + it('supports groupByField and colorField properties', async () => { + const dataWithGroups = [ + { id: '1', name: 'Task A', start_date: '2024-01-01', team: 'Alpha', priority: 'high' }, + ]; + (mockDataSource.find as any).mockResolvedValue(dataWithGroups); + + const schema: any = { + type: 'timeline', + objectName: 'tasks', + titleField: 'name', + startDateField: 'start_date', + groupByField: 'team', + colorField: 'priority', + }; + + render(); + + await waitFor(() => { + expect(screen.getByText('Task A')).toBeDefined(); + }); + }); + + it('supports scale property', () => { + const schema: any = { + type: 'timeline', + items: [ + { title: 'Weekly Event', date: '2024-01-01' } + ], + scale: 'week', + }; + render(); + expect(screen.getByText('Weekly Event')).toBeDefined(); + }); }); diff --git a/packages/plugin-timeline/src/ObjectTimeline.tsx b/packages/plugin-timeline/src/ObjectTimeline.tsx index 3fee8963a..2511316d5 100644 --- a/packages/plugin-timeline/src/ObjectTimeline.tsx +++ b/packages/plugin-timeline/src/ObjectTimeline.tsx @@ -25,16 +25,33 @@ const TimelineExtensionSchema = z.object({ mapping: TimelineMappingSchema.optional(), objectName: z.string().optional(), titleField: z.string().optional(), + /** @deprecated Use startDateField instead */ dateField: z.string().optional(), + startDateField: z.string().optional(), + endDateField: z.string().optional(), descriptionField: z.string().optional(), + groupByField: z.string().optional(), + colorField: z.string().optional(), + scale: z.enum(['hour', 'day', 'week', 'month', 'quarter', 'year']).default('day').optional(), }); export interface ObjectTimelineProps { schema: TimelineSchema & { objectName?: string; titleField?: string; + /** @deprecated Use startDateField instead */ dateField?: string; + /** Spec-compliant: field name for the start date */ + startDateField?: string; + /** Spec-compliant: field name for the end date */ + endDateField?: string; descriptionField?: string; + /** Spec-compliant: field name for grouping timeline items */ + groupByField?: string; + /** Spec-compliant: field name for timeline item color */ + colorField?: string; + /** Spec-compliant: time scale for the timeline display */ + scale?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; // Map data fields to timeline item properties mapping?: { title?: string; @@ -109,18 +126,25 @@ export const ObjectTimeline: React.FC = ({ if (!effectiveItems && rawData && Array.isArray(rawData)) { const titleField = schema.mapping?.title || schema.titleField || 'name'; - const dateField = schema.mapping?.date || schema.dateField || 'date'; + // Spec-compliant: prefer startDateField, fallback to dateField for backward compat + const startDateField = schema.mapping?.date || schema.startDateField || schema.dateField || 'date'; + const endDateField = schema.endDateField || startDateField; const descField = schema.mapping?.description || schema.descriptionField || 'description'; const variantField = schema.mapping?.variant || 'variant'; + const groupByField = schema.groupByField; + const colorField = schema.colorField; effectiveItems = rawData.map(item => ({ title: item[titleField], // Support both 'time' (vertical) and 'startDate' (gantt) - time: item[dateField], - startDate: item[dateField], - endDate: item[dateField], // Default single point + time: item[startDateField], + startDate: item[startDateField], + endDate: item[endDateField], description: item[descField], variant: item[variantField] || 'default', + // Spec-compliant: group and color support + ...(groupByField ? { group: item[groupByField] } : {}), + ...(colorField ? { color: item[colorField] } : {}), // Pass original item for click handlers _data: item })); diff --git a/packages/react/src/__tests__/useNavigationOverlay.test.ts b/packages/react/src/__tests__/useNavigationOverlay.test.ts index cae635b82..f124b1df5 100644 --- a/packages/react/src/__tests__/useNavigationOverlay.test.ts +++ b/packages/react/src/__tests__/useNavigationOverlay.test.ts @@ -413,4 +413,64 @@ describe('useNavigationOverlay', () => { }); }); }); + + // ============================================================ + // navigation.view property + // ============================================================ + + describe('navigation.view property', () => { + it('should expose the view property from config', () => { + const { result } = renderHook(() => + useNavigationOverlay({ + navigation: { mode: 'drawer', view: 'edit_form' }, + objectName: 'contacts', + }) + ); + + expect(result.current.view).toBe('edit_form'); + }); + + it('should pass view to onNavigate for page mode', () => { + const onNavigate = vi.fn(); + const { result } = renderHook(() => + useNavigationOverlay({ + navigation: { mode: 'page', view: 'detail_view' }, + objectName: 'contacts', + onNavigate, + }) + ); + + act(() => { + result.current.handleClick({ _id: '123' }); + }); + + expect(onNavigate).toHaveBeenCalledWith('123', 'detail_view'); + }); + + it('should include view in new_window URL', () => { + const { result } = renderHook(() => + useNavigationOverlay({ + navigation: { mode: 'new_window', view: 'edit_form' }, + objectName: 'contacts', + }) + ); + + act(() => { + result.current.handleClick({ _id: '123' }); + }); + + expect(mockWindowOpen).toHaveBeenCalledWith('/contacts/123/edit_form', '_blank'); + }); + + it('should return undefined view when not configured', () => { + const { result } = renderHook(() => + useNavigationOverlay({ + navigation: { mode: 'drawer' }, + objectName: 'contacts', + }) + ); + + expect(result.current.view).toBeUndefined(); + }); + }); }); diff --git a/packages/react/src/hooks/useNavigationOverlay.ts b/packages/react/src/hooks/useNavigationOverlay.ts index 85561be5e..f5ae9a085 100644 --- a/packages/react/src/hooks/useNavigationOverlay.ts +++ b/packages/react/src/hooks/useNavigationOverlay.ts @@ -64,6 +64,8 @@ export interface NavigationOverlayState { width: string | number | undefined; /** Whether navigation is an overlay mode (drawer/modal/split/popover) */ isOverlay: boolean; + /** The target view/form name from NavigationConfig */ + view: string | undefined; } /** @@ -102,6 +104,7 @@ export function useNavigationOverlay( const mode: NavigationMode = navigation?.mode ?? 'page'; const width = navigation?.width; + const view = navigation?.view; const isOverlay = mode === 'drawer' || mode === 'modal' || mode === 'split' || mode === 'popover'; const close = useCallback(() => { @@ -126,7 +129,7 @@ export function useNavigationOverlay( if (!navigation) { const recordId = record._id || record.id; if (onNavigate && recordId != null) { - onNavigate(recordId as string | number, 'view'); + onNavigate(recordId as string | number, view ?? 'view'); } return; } @@ -139,7 +142,8 @@ export function useNavigationOverlay( // new_window / openNewTab — open in new browser tab if (mode === 'new_window' || navigation.openNewTab) { const recordId = record._id || record.id; - const url = objectName ? `/${objectName}/${recordId}` : `/${recordId}`; + const viewPath = view ? `/${view}` : ''; + const url = objectName ? `/${objectName}/${recordId}${viewPath}` : `/${recordId}${viewPath}`; window.open(url, '_blank'); return; } @@ -148,7 +152,7 @@ export function useNavigationOverlay( if (mode === 'page') { const recordId = record._id || record.id; if (onNavigate && recordId != null) { - onNavigate(recordId as string | number, 'view'); + onNavigate(recordId as string | number, view ?? 'view'); } return; } @@ -173,8 +177,9 @@ export function useNavigationOverlay( setIsOpen, handleClick, width, + view, isOverlay, }), - [isOpen, selectedRecord, mode, close, open, handleClick, width, isOverlay] + [isOpen, selectedRecord, mode, close, open, handleClick, width, view, isOverlay] ); } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 9178e0022..e29ab26ee 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -962,6 +962,10 @@ export type { RowHeight, RowHeightSchema, DensityMode, + TimelineConfig, + TimelineConfigSchema, + NavigationConfig, + NavigationConfigSchema, ViewSharing, ViewSharingSchema, } from '@objectstack/spec/ui'; diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 64949cfce..787ba65ae 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -1033,6 +1033,19 @@ export interface ListViewSchema extends BaseSchema { /** Visual Component overrides (legacy, prefer typed configs above) */ options?: Record; + + /** + * Empty state configuration shown when no data is available. + * Aligned with @objectstack/spec ListViewSchema.emptyState. + */ + emptyState?: { + /** Title text for the empty state */ + title?: string; + /** Message/description for the empty state */ + message?: string; + /** Icon name (Lucide icon identifier) for the empty state */ + icon?: string; + }; } /** From 7f368cfcd2bc6c3935803751140cc3322844bae6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:10:30 +0000 Subject: [PATCH 3/3] fix: address code review feedback - emptyState icon support and Zod schema fix Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/plugin-list/src/ListView.tsx | 30 ++++++++++++------- .../plugin-timeline/src/ObjectTimeline.tsx | 2 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index 559867fc9..40d89250c 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -9,7 +9,7 @@ import * as React from 'react'; import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder, SortBuilder, NavigationOverlay } from '@object-ui/components'; import type { SortItem } from '@object-ui/components'; -import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Group, Paintbrush, Ruler, Inbox } from 'lucide-react'; +import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Group, Paintbrush, Ruler, Inbox, icons, type LucideIcon } from 'lucide-react'; import type { FilterGroup } from '@object-ui/components'; import { ViewSwitcher, ViewType } from './ViewSwitcher'; import { SchemaRenderer, useNavigationOverlay } from '@object-ui/react'; @@ -593,15 +593,25 @@ export const ListView: React.FC = ({ {/* View Content */}
{!loading && data.length === 0 ? ( -
- -

- {(typeof schema.emptyState?.title === 'string' ? schema.emptyState.title : undefined) ?? 'No items found'} -

-

- {(typeof schema.emptyState?.message === 'string' ? schema.emptyState.message : undefined) ?? 'There are no records to display. Try adjusting your filters or adding new data.'} -

-
+ (() => { + const iconName = schema.emptyState?.icon; + const ResolvedIcon: LucideIcon = iconName + ? ((icons as Record)[ + iconName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('') + ] ?? Inbox) + : Inbox; + return ( +
+ +

+ {(typeof schema.emptyState?.title === 'string' ? schema.emptyState.title : undefined) ?? 'No items found'} +

+

+ {(typeof schema.emptyState?.message === 'string' ? schema.emptyState.message : undefined) ?? 'There are no records to display. Try adjusting your filters or adding new data.'} +

+
+ ); + })() ) : (