@object-ui/plugin-list 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ListView.tsx CHANGED
@@ -557,7 +557,16 @@ export const ListView: React.FC<ListViewProps> = ({
557
557
  if (schema.data && typeof schema.data === 'object' && !Array.isArray(schema.data)) {
558
558
  const dataConfig = schema.data as any;
559
559
  if (dataConfig.provider === 'value' && Array.isArray(dataConfig.items)) {
560
- setData(dataConfig.items);
560
+ let items = dataConfig.items;
561
+ if (searchTerm) {
562
+ const q = searchTerm.toLowerCase();
563
+ items = items.filter((row: any) =>
564
+ Object.values(row).some(
565
+ (v) => v != null && String(v).toLowerCase().includes(q),
566
+ ),
567
+ );
568
+ }
569
+ setData(items);
561
570
  setLoading(false);
562
571
  setDataLimitReached(false);
563
572
  return;
@@ -565,7 +574,16 @@ export const ListView: React.FC<ListViewProps> = ({
565
574
  }
566
575
  // Also support schema.data as a plain array (shorthand for value provider)
567
576
  if (Array.isArray(schema.data)) {
568
- setData(schema.data as any[]);
577
+ let items = schema.data as any[];
578
+ if (searchTerm) {
579
+ const q = searchTerm.toLowerCase();
580
+ items = items.filter((row: any) =>
581
+ Object.values(row).some(
582
+ (v) => v != null && String(v).toLowerCase().includes(q),
583
+ ),
584
+ );
585
+ }
586
+ setData(items);
569
587
  setLoading(false);
570
588
  setDataLimitReached(false);
571
589
  return;
@@ -1071,12 +1089,11 @@ export const ListView: React.FC<ListViewProps> = ({
1071
1089
  </div>
1072
1090
  )}
1073
1091
 
1074
- {/* Airtable-style Toolbar — Merged: UserFilter badges (left) + Tool buttons (right) */}
1092
+ {/* Airtable-style Toolbar — UserFilter badges (left) + Tool buttons (right) */}
1075
1093
  <div className="border-b px-2 sm:px-4 py-1 flex items-center justify-between gap-1 sm:gap-2 bg-background">
1076
- <div className="flex items-center gap-0.5 overflow-x-auto flex-1 min-w-0">
1094
+ <div className="flex items-center gap-0.5 overflow-x-auto min-w-0">
1077
1095
  {/* User Filters — inline in toolbar (Airtable Interfaces-style) */}
1078
1096
  {resolvedUserFilters && (
1079
- <>
1080
1097
  <div className="shrink-0 min-w-0" data-testid="user-filters">
1081
1098
  <UserFilters
1082
1099
  config={resolvedUserFilters}
@@ -1086,10 +1103,10 @@ export const ListView: React.FC<ListViewProps> = ({
1086
1103
  maxVisible={3}
1087
1104
  />
1088
1105
  </div>
1089
- <div className="h-4 w-px bg-border/60 mx-0.5 shrink-0" />
1090
- </>
1091
1106
  )}
1107
+ </div>
1092
1108
 
1109
+ <div className="flex items-center gap-0.5 shrink-0">
1093
1110
  {/* Hide Fields */}
1094
1111
  {toolbarFlags.showHideFields && (
1095
1112
  <Popover open={showHideFields} onOpenChange={setShowHideFields}>
@@ -1456,7 +1473,7 @@ export const ListView: React.FC<ListViewProps> = ({
1456
1473
  <Search className="h-3.5 w-3.5" />
1457
1474
  </Button>
1458
1475
  </PopoverTrigger>
1459
- <PopoverContent align="end" className="w-64 p-2" data-testid="search-popover">
1476
+ <PopoverContent align="end" className="w-[calc(100vw-2rem)] sm:w-64 p-2" data-testid="search-popover">
1460
1477
  <div className="relative">
1461
1478
  <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
1462
1479
  <Input
@@ -1480,10 +1497,7 @@ export const ListView: React.FC<ListViewProps> = ({
1480
1497
  </PopoverContent>
1481
1498
  </Popover>
1482
1499
  )}
1483
- </div>
1484
1500
 
1485
- {/* Right: Add Record */}
1486
- <div className="flex items-center gap-1">
1487
1501
  {/* Add Record (top position) */}
1488
1502
  {toolbarFlags.showAddRecord && toolbarFlags.addRecordPosition === 'top' && (
1489
1503
  <Button
@@ -303,14 +303,6 @@ function DropdownFilters({ fields, objectDef, data, onFilterChange, maxVisible,
303
303
  )}
304
304
  </>
305
305
  )}
306
- <button
307
- className="inline-flex items-center gap-1 h-7 px-2 text-xs text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors shrink-0"
308
- data-testid="user-filters-add"
309
- title="Add filter"
310
- >
311
- <Plus className="h-3.5 w-3.5" />
312
- <span className="hidden sm:inline">Add filter</span>
313
- </button>
314
306
  </div>
315
307
  );
316
308
  }
@@ -540,7 +540,7 @@ describe('ListView', () => {
540
540
  expect(screen.getByTestId('filter-badge-is_active')).toBeInTheDocument();
541
541
  });
542
542
 
543
- it('should show Add filter button in userFilters', () => {
543
+ it('should not show Add filter button in userFilters (removed from UI)', () => {
544
544
  const schema: ListViewSchema = {
545
545
  type: 'list-view',
546
546
  objectName: 'contacts',
@@ -555,7 +555,7 @@ describe('ListView', () => {
555
555
  };
556
556
 
557
557
  renderWithProvider(<ListView schema={schema} />);
558
- expect(screen.getByTestId('user-filters-add')).toBeInTheDocument();
558
+ expect(screen.queryByTestId('user-filters-add')).not.toBeInTheDocument();
559
559
  });
560
560
 
561
561
  it('should not render userFilters when objectDef has no filterable fields', async () => {
@@ -1577,6 +1577,68 @@ describe('ListView', () => {
1577
1577
  expect(mockDataSource.find).not.toHaveBeenCalled();
1578
1578
  });
1579
1579
 
1580
+ it('should filter inline array data by searchTerm', async () => {
1581
+ const schema: ListViewSchema = {
1582
+ type: 'list-view',
1583
+ objectName: 'contacts',
1584
+ viewType: 'grid',
1585
+ fields: ['name', 'email'],
1586
+ data: [
1587
+ { _id: '1', name: 'Alice', email: 'alice@test.com' },
1588
+ { _id: '2', name: 'Bob', email: 'bob@test.com' },
1589
+ { _id: '3', name: 'Charlie', email: 'charlie@test.com' },
1590
+ ] as any,
1591
+ };
1592
+
1593
+ mockDataSource.find.mockClear();
1594
+ renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
1595
+
1596
+ await vi.waitFor(() => {
1597
+ expect(screen.getByText('3 records')).toBeInTheDocument();
1598
+ });
1599
+
1600
+ // Open search popover and type search query
1601
+ fireEvent.click(screen.getByTestId('search-icon-button'));
1602
+ fireEvent.change(screen.getByPlaceholderText(/search/i), { target: { value: 'alice' } });
1603
+
1604
+ await vi.waitFor(() => {
1605
+ expect(screen.getByText('1 record')).toBeInTheDocument();
1606
+ });
1607
+ expect(mockDataSource.find).not.toHaveBeenCalled();
1608
+ });
1609
+
1610
+ it('should filter value provider data by searchTerm', async () => {
1611
+ const schema: ListViewSchema = {
1612
+ type: 'list-view',
1613
+ objectName: 'contacts',
1614
+ viewType: 'grid',
1615
+ fields: ['name', 'email'],
1616
+ data: {
1617
+ provider: 'value',
1618
+ items: [
1619
+ { _id: '1', name: 'Alice', email: 'alice@test.com' },
1620
+ { _id: '2', name: 'Bob', email: 'bob@test.com' },
1621
+ ],
1622
+ } as any,
1623
+ };
1624
+
1625
+ mockDataSource.find.mockClear();
1626
+ renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
1627
+
1628
+ await vi.waitFor(() => {
1629
+ expect(screen.getByText('2 records')).toBeInTheDocument();
1630
+ });
1631
+
1632
+ // Open search popover and type search query
1633
+ fireEvent.click(screen.getByTestId('search-icon-button'));
1634
+ fireEvent.change(screen.getByPlaceholderText(/search/i), { target: { value: 'bob' } });
1635
+
1636
+ await vi.waitFor(() => {
1637
+ expect(screen.getByText('1 record')).toBeInTheDocument();
1638
+ });
1639
+ expect(mockDataSource.find).not.toHaveBeenCalled();
1640
+ });
1641
+
1580
1642
  it('should fall back to dataSource.find when schema.data is not set', async () => {
1581
1643
  const mockItems = [
1582
1644
  { _id: '1', name: 'Alice', email: 'alice@test.com' },
@@ -369,10 +369,10 @@ describe('UserFilters', () => {
369
369
  });
370
370
 
371
371
  // ============================================
372
- // Add Filter Entry Point
372
+ // Add Filter Entry Point (removed)
373
373
  // ============================================
374
374
  describe('Add filter entry', () => {
375
- it('renders "Add filter" button in dropdown mode', () => {
375
+ it('does not render "Add filter" button (removed from UI)', () => {
376
376
  const config = {
377
377
  element: 'dropdown' as const,
378
378
  fields: [
@@ -387,15 +387,7 @@ describe('UserFilters', () => {
387
387
  };
388
388
  const onChange = vi.fn();
389
389
  render(<UserFilters config={config} onFilterChange={onChange} />);
390
- expect(screen.getByTestId('user-filters-add')).toBeInTheDocument();
391
- expect(screen.getByText('Add filter')).toBeInTheDocument();
392
- });
393
-
394
- it('renders "Add filter" button even when no fields are provided', () => {
395
- const config = { element: 'dropdown' as const };
396
- const onChange = vi.fn();
397
- render(<UserFilters config={config} onFilterChange={onChange} />);
398
- expect(screen.getByTestId('user-filters-add')).toBeInTheDocument();
390
+ expect(screen.queryByTestId('user-filters-add')).not.toBeInTheDocument();
399
391
  });
400
392
  });
401
393