@object-ui/plugin-list 0.5.0 → 2.0.0
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +21 -0
- package/dist/index.js +23312 -21572
- package/dist/index.umd.cjs +28 -28
- package/dist/plugin-list.css +1 -1
- package/dist/src/ListView.d.ts +4 -0
- package/dist/src/ListView.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/ListView.tsx +225 -116
- package/src/__tests__/ListView.test.tsx +16 -7
- package/src/__tests__/ListViewPersistence.test.tsx +8 -5
- package/src/index.tsx +22 -0
package/src/ListView.tsx
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import * as React from 'react';
|
|
10
|
-
import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder, SortBuilder } from '@object-ui/components';
|
|
10
|
+
import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder, SortBuilder, NavigationOverlay } from '@object-ui/components';
|
|
11
11
|
import type { SortItem } from '@object-ui/components';
|
|
12
|
-
import { Search, SlidersHorizontal, ArrowUpDown, X } from 'lucide-react';
|
|
12
|
+
import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Group, Paintbrush, Ruler } from 'lucide-react';
|
|
13
13
|
import type { FilterGroup } from '@object-ui/components';
|
|
14
14
|
import { ViewSwitcher, ViewType } from './ViewSwitcher';
|
|
15
|
-
import { SchemaRenderer } from '@object-ui/react';
|
|
15
|
+
import { SchemaRenderer, useNavigationOverlay } from '@object-ui/react';
|
|
16
16
|
import type { ListViewSchema } from '@object-ui/types';
|
|
17
17
|
|
|
18
18
|
export interface ListViewProps {
|
|
@@ -22,6 +22,10 @@ export interface ListViewProps {
|
|
|
22
22
|
onFilterChange?: (filters: any) => void;
|
|
23
23
|
onSortChange?: (sort: any) => void;
|
|
24
24
|
onSearchChange?: (search: string) => void;
|
|
25
|
+
/** Callback when a row/item is clicked (overrides NavigationConfig) */
|
|
26
|
+
onRowClick?: (record: Record<string, unknown>) => void;
|
|
27
|
+
/** Show view type switcher (Grid/Kanban/etc). Default: false (view type is fixed) */
|
|
28
|
+
showViewSwitcher?: boolean;
|
|
25
29
|
[key: string]: any;
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -65,6 +69,8 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
65
69
|
onFilterChange,
|
|
66
70
|
onSortChange,
|
|
67
71
|
onSearchChange,
|
|
72
|
+
onRowClick,
|
|
73
|
+
showViewSwitcher = false,
|
|
68
74
|
...props
|
|
69
75
|
}) => {
|
|
70
76
|
// Kernel level default: Ensure viewType is always defined (default to 'grid')
|
|
@@ -236,7 +242,15 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
236
242
|
return views;
|
|
237
243
|
}, [schema.options, schema.viewType]);
|
|
238
244
|
|
|
239
|
-
//
|
|
245
|
+
// Sync view from props
|
|
246
|
+
React.useEffect(() => {
|
|
247
|
+
if (schema.viewType) {
|
|
248
|
+
setCurrentView(schema.viewType as ViewType);
|
|
249
|
+
}
|
|
250
|
+
}, [schema.viewType]);
|
|
251
|
+
|
|
252
|
+
// Load saved view preference (DISABLED: interfering with schema-defined views)
|
|
253
|
+
/*
|
|
240
254
|
React.useEffect(() => {
|
|
241
255
|
try {
|
|
242
256
|
const savedView = localStorage.getItem(storageKey);
|
|
@@ -247,6 +261,7 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
247
261
|
console.warn('Failed to load view preference from localStorage:', error);
|
|
248
262
|
}
|
|
249
263
|
}, [storageKey, availableViews]);
|
|
264
|
+
*/
|
|
250
265
|
|
|
251
266
|
const handleViewChange = React.useCallback((view: ViewType) => {
|
|
252
267
|
setCurrentView(view);
|
|
@@ -263,6 +278,14 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
263
278
|
onSearchChange?.(value);
|
|
264
279
|
}, [onSearchChange]);
|
|
265
280
|
|
|
281
|
+
// --- NavigationConfig support ---
|
|
282
|
+
const navigation = useNavigationOverlay({
|
|
283
|
+
navigation: schema.navigation,
|
|
284
|
+
objectName: schema.objectName,
|
|
285
|
+
onNavigate: schema.onNavigate,
|
|
286
|
+
onRowClick,
|
|
287
|
+
});
|
|
288
|
+
|
|
266
289
|
// Generate the appropriate view component schema
|
|
267
290
|
const viewComponentSchema = React.useMemo(() => {
|
|
268
291
|
const baseProps = {
|
|
@@ -273,6 +296,8 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
273
296
|
className: "h-full w-full",
|
|
274
297
|
// Disable internal controls that clash with ListView toolbar
|
|
275
298
|
showSearch: false,
|
|
299
|
+
// Pass navigation click handler to child views
|
|
300
|
+
onRowClick: navigation.handleClick,
|
|
276
301
|
};
|
|
277
302
|
|
|
278
303
|
switch (currentView) {
|
|
@@ -365,124 +390,181 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
365
390
|
}));
|
|
366
391
|
}, [objectDef, schema.fields]);
|
|
367
392
|
|
|
393
|
+
const [searchExpanded, setSearchExpanded] = React.useState(false);
|
|
394
|
+
|
|
368
395
|
return (
|
|
369
396
|
<div className={cn('flex flex-col h-full bg-background', className)}>
|
|
370
|
-
{/* Airtable-style Toolbar */}
|
|
371
|
-
|
|
372
|
-
<div className="
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
onViewChange={handleViewChange}
|
|
379
|
-
/>
|
|
380
|
-
</div>
|
|
381
|
-
|
|
382
|
-
{/* Action Tools */}
|
|
383
|
-
<div className="flex items-center gap-1">
|
|
384
|
-
<Popover open={showFilters} onOpenChange={setShowFilters}>
|
|
385
|
-
<PopoverTrigger asChild>
|
|
386
|
-
<Button
|
|
387
|
-
variant={hasFilters ? "secondary" : "ghost"}
|
|
388
|
-
size="sm"
|
|
389
|
-
className={cn(
|
|
390
|
-
"h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary",
|
|
391
|
-
hasFilters && "text-primary bg-secondary/50"
|
|
392
|
-
)}
|
|
393
|
-
>
|
|
394
|
-
<SlidersHorizontal className="h-4 w-4 mr-2" />
|
|
395
|
-
<span className="hidden lg:inline">Filter</span>
|
|
396
|
-
{hasFilters && (
|
|
397
|
-
<span className="ml-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary">
|
|
398
|
-
{currentFilters.conditions?.length || 0}
|
|
399
|
-
</span>
|
|
400
|
-
)}
|
|
401
|
-
</Button>
|
|
402
|
-
</PopoverTrigger>
|
|
403
|
-
<PopoverContent align="start" className="w-[600px] p-4">
|
|
404
|
-
<div className="space-y-4">
|
|
405
|
-
<div className="flex items-center justify-between border-b pb-2">
|
|
406
|
-
<h4 className="font-medium text-sm">Filter Records</h4>
|
|
407
|
-
</div>
|
|
408
|
-
<FilterBuilder
|
|
409
|
-
fields={filterFields}
|
|
410
|
-
value={currentFilters}
|
|
411
|
-
onChange={(newFilters) => {
|
|
412
|
-
console.log('Filter Changed:', newFilters);
|
|
413
|
-
setCurrentFilters(newFilters);
|
|
414
|
-
// Convert FilterBuilder format to OData $filter string if needed
|
|
415
|
-
// For now we just update state and notify listener
|
|
416
|
-
// In a real app, this would likely build an OData string
|
|
417
|
-
if (onFilterChange) onFilterChange(newFilters);
|
|
418
|
-
}}
|
|
419
|
-
/>
|
|
420
|
-
</div>
|
|
421
|
-
</PopoverContent>
|
|
422
|
-
</Popover>
|
|
423
|
-
|
|
424
|
-
<Popover open={showSort} onOpenChange={setShowSort}>
|
|
425
|
-
<PopoverTrigger asChild>
|
|
426
|
-
<Button
|
|
427
|
-
variant={currentSort.length > 0 ? "secondary" : "ghost"}
|
|
428
|
-
size="sm"
|
|
429
|
-
className={cn(
|
|
430
|
-
"h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary",
|
|
431
|
-
currentSort.length > 0 && "text-primary bg-secondary/50"
|
|
432
|
-
)}
|
|
433
|
-
>
|
|
434
|
-
<ArrowUpDown className="h-4 w-4 mr-2" />
|
|
435
|
-
<span className="hidden lg:inline">Sort</span>
|
|
436
|
-
{currentSort.length > 0 && (
|
|
437
|
-
<span className="ml-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary">
|
|
438
|
-
{currentSort.length}
|
|
439
|
-
</span>
|
|
440
|
-
)}
|
|
441
|
-
</Button>
|
|
442
|
-
</PopoverTrigger>
|
|
443
|
-
<PopoverContent align="start" className="w-[600px] p-4">
|
|
444
|
-
<div className="space-y-4">
|
|
445
|
-
<div className="flex items-center justify-between border-b pb-2">
|
|
446
|
-
<h4 className="font-medium text-sm">Sort Records</h4>
|
|
447
|
-
</div>
|
|
448
|
-
<SortBuilder
|
|
449
|
-
fields={filterFields}
|
|
450
|
-
value={currentSort}
|
|
451
|
-
onChange={(newSort) => {
|
|
452
|
-
console.log('Sort Changed:', newSort);
|
|
453
|
-
setCurrentSort(newSort);
|
|
454
|
-
if (onSortChange) onSortChange(newSort);
|
|
455
|
-
}}
|
|
456
|
-
/>
|
|
457
|
-
</div>
|
|
458
|
-
</PopoverContent>
|
|
459
|
-
</Popover>
|
|
460
|
-
|
|
461
|
-
{/* Future: Group, Color, Height */}
|
|
462
|
-
</div>
|
|
397
|
+
{/* Airtable-style Toolbar — Row 1: View tabs */}
|
|
398
|
+
{showViewSwitcher && (
|
|
399
|
+
<div className="border-b px-4 py-1 flex items-center bg-background">
|
|
400
|
+
<ViewSwitcher
|
|
401
|
+
currentView={currentView}
|
|
402
|
+
availableViews={availableViews}
|
|
403
|
+
onViewChange={handleViewChange}
|
|
404
|
+
/>
|
|
463
405
|
</div>
|
|
406
|
+
)}
|
|
464
407
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
408
|
+
{/* Airtable-style Toolbar — Row 2: Tool buttons */}
|
|
409
|
+
<div className="border-b px-4 py-1 flex items-center justify-between gap-2 bg-background">
|
|
410
|
+
<div className="flex items-center gap-0.5 overflow-hidden">
|
|
411
|
+
{/* Hide Fields */}
|
|
412
|
+
<Button
|
|
413
|
+
variant="ghost"
|
|
414
|
+
size="sm"
|
|
415
|
+
className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
|
|
416
|
+
disabled
|
|
417
|
+
>
|
|
418
|
+
<EyeOff className="h-3.5 w-3.5 mr-1.5" />
|
|
419
|
+
<span className="hidden sm:inline">Hide fields</span>
|
|
420
|
+
</Button>
|
|
421
|
+
|
|
422
|
+
{/* Filter */}
|
|
423
|
+
<Popover open={showFilters} onOpenChange={setShowFilters}>
|
|
424
|
+
<PopoverTrigger asChild>
|
|
425
|
+
<Button
|
|
426
|
+
variant="ghost"
|
|
427
|
+
size="sm"
|
|
428
|
+
className={cn(
|
|
429
|
+
"h-7 px-2 text-muted-foreground hover:text-primary text-xs",
|
|
430
|
+
hasFilters && "text-primary"
|
|
431
|
+
)}
|
|
432
|
+
>
|
|
433
|
+
<SlidersHorizontal className="h-3.5 w-3.5 mr-1.5" />
|
|
434
|
+
<span className="hidden sm:inline">Filter</span>
|
|
435
|
+
{hasFilters && (
|
|
436
|
+
<span className="ml-1 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary">
|
|
437
|
+
{currentFilters.conditions?.length || 0}
|
|
438
|
+
</span>
|
|
439
|
+
)}
|
|
440
|
+
</Button>
|
|
441
|
+
</PopoverTrigger>
|
|
442
|
+
<PopoverContent align="start" className="w-[600px] p-4">
|
|
443
|
+
<div className="space-y-4">
|
|
444
|
+
<div className="flex items-center justify-between border-b pb-2">
|
|
445
|
+
<h4 className="font-medium text-sm">Filter Records</h4>
|
|
446
|
+
</div>
|
|
447
|
+
<FilterBuilder
|
|
448
|
+
fields={filterFields}
|
|
449
|
+
value={currentFilters}
|
|
450
|
+
onChange={(newFilters) => {
|
|
451
|
+
setCurrentFilters(newFilters);
|
|
452
|
+
if (onFilterChange) onFilterChange(newFilters);
|
|
453
|
+
}}
|
|
474
454
|
/>
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
455
|
+
</div>
|
|
456
|
+
</PopoverContent>
|
|
457
|
+
</Popover>
|
|
458
|
+
|
|
459
|
+
{/* Group */}
|
|
460
|
+
<Button
|
|
461
|
+
variant="ghost"
|
|
462
|
+
size="sm"
|
|
463
|
+
className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
|
|
464
|
+
disabled
|
|
465
|
+
>
|
|
466
|
+
<Group className="h-3.5 w-3.5 mr-1.5" />
|
|
467
|
+
<span className="hidden sm:inline">Group</span>
|
|
468
|
+
</Button>
|
|
469
|
+
|
|
470
|
+
{/* Sort */}
|
|
471
|
+
<Popover open={showSort} onOpenChange={setShowSort}>
|
|
472
|
+
<PopoverTrigger asChild>
|
|
473
|
+
<Button
|
|
474
|
+
variant="ghost"
|
|
475
|
+
size="sm"
|
|
476
|
+
className={cn(
|
|
477
|
+
"h-7 px-2 text-muted-foreground hover:text-primary text-xs",
|
|
478
|
+
currentSort.length > 0 && "text-primary"
|
|
479
|
+
)}
|
|
480
|
+
>
|
|
481
|
+
<ArrowUpDown className="h-3.5 w-3.5 mr-1.5" />
|
|
482
|
+
<span className="hidden sm:inline">Sort</span>
|
|
483
|
+
{currentSort.length > 0 && (
|
|
484
|
+
<span className="ml-1 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary">
|
|
485
|
+
{currentSort.length}
|
|
486
|
+
</span>
|
|
484
487
|
)}
|
|
485
|
-
|
|
488
|
+
</Button>
|
|
489
|
+
</PopoverTrigger>
|
|
490
|
+
<PopoverContent align="start" className="w-[600px] p-4">
|
|
491
|
+
<div className="space-y-4">
|
|
492
|
+
<div className="flex items-center justify-between border-b pb-2">
|
|
493
|
+
<h4 className="font-medium text-sm">Sort Records</h4>
|
|
494
|
+
</div>
|
|
495
|
+
<SortBuilder
|
|
496
|
+
fields={filterFields}
|
|
497
|
+
value={currentSort}
|
|
498
|
+
onChange={(newSort) => {
|
|
499
|
+
setCurrentSort(newSort);
|
|
500
|
+
if (onSortChange) onSortChange(newSort);
|
|
501
|
+
}}
|
|
502
|
+
/>
|
|
503
|
+
</div>
|
|
504
|
+
</PopoverContent>
|
|
505
|
+
</Popover>
|
|
506
|
+
|
|
507
|
+
{/* Color */}
|
|
508
|
+
<Button
|
|
509
|
+
variant="ghost"
|
|
510
|
+
size="sm"
|
|
511
|
+
className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
|
|
512
|
+
disabled
|
|
513
|
+
>
|
|
514
|
+
<Paintbrush className="h-3.5 w-3.5 mr-1.5" />
|
|
515
|
+
<span className="hidden sm:inline">Color</span>
|
|
516
|
+
</Button>
|
|
517
|
+
|
|
518
|
+
{/* Row Height */}
|
|
519
|
+
<Button
|
|
520
|
+
variant="ghost"
|
|
521
|
+
size="sm"
|
|
522
|
+
className="h-7 px-2 text-muted-foreground hover:text-primary text-xs hidden lg:flex"
|
|
523
|
+
disabled
|
|
524
|
+
>
|
|
525
|
+
<Ruler className="h-3.5 w-3.5 mr-1.5" />
|
|
526
|
+
<span className="hidden sm:inline">Row height</span>
|
|
527
|
+
</Button>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
{/* Right: Search */}
|
|
531
|
+
<div className="flex items-center gap-1">
|
|
532
|
+
{searchExpanded ? (
|
|
533
|
+
<div className="relative w-48 lg:w-64">
|
|
534
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
|
535
|
+
<Input
|
|
536
|
+
placeholder="Find..."
|
|
537
|
+
value={searchTerm}
|
|
538
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
|
539
|
+
className="pl-7 h-7 text-xs bg-muted/50 border-transparent hover:bg-muted focus:bg-background focus:border-input transition-colors"
|
|
540
|
+
autoFocus
|
|
541
|
+
onBlur={() => {
|
|
542
|
+
if (!searchTerm) setSearchExpanded(false);
|
|
543
|
+
}}
|
|
544
|
+
/>
|
|
545
|
+
<Button
|
|
546
|
+
variant="ghost"
|
|
547
|
+
size="sm"
|
|
548
|
+
className="absolute right-0.5 top-1/2 -translate-y-1/2 h-5 w-5 p-0 hover:bg-muted-foreground/20"
|
|
549
|
+
onClick={() => {
|
|
550
|
+
handleSearchChange('');
|
|
551
|
+
setSearchExpanded(false);
|
|
552
|
+
}}
|
|
553
|
+
>
|
|
554
|
+
<X className="h-3 w-3" />
|
|
555
|
+
</Button>
|
|
556
|
+
</div>
|
|
557
|
+
) : (
|
|
558
|
+
<Button
|
|
559
|
+
variant="ghost"
|
|
560
|
+
size="sm"
|
|
561
|
+
className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
|
|
562
|
+
onClick={() => setSearchExpanded(true)}
|
|
563
|
+
>
|
|
564
|
+
<Search className="h-3.5 w-3.5 mr-1.5" />
|
|
565
|
+
<span className="hidden sm:inline">Search</span>
|
|
566
|
+
</Button>
|
|
567
|
+
)}
|
|
486
568
|
</div>
|
|
487
569
|
</div>
|
|
488
570
|
|
|
@@ -498,6 +580,33 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
498
580
|
loading={loading}
|
|
499
581
|
/>
|
|
500
582
|
</div>
|
|
583
|
+
|
|
584
|
+
{/* Navigation Overlay (drawer/modal/popover) */}
|
|
585
|
+
{navigation.isOverlay && (
|
|
586
|
+
<NavigationOverlay
|
|
587
|
+
{...navigation}
|
|
588
|
+
title={
|
|
589
|
+
schema.label
|
|
590
|
+
? `${schema.label} Detail`
|
|
591
|
+
: schema.objectName
|
|
592
|
+
? `${schema.objectName.charAt(0).toUpperCase() + schema.objectName.slice(1)} Detail`
|
|
593
|
+
: 'Record Detail'
|
|
594
|
+
}
|
|
595
|
+
>
|
|
596
|
+
{(record) => (
|
|
597
|
+
<div className="space-y-3">
|
|
598
|
+
{Object.entries(record).map(([key, value]) => (
|
|
599
|
+
<div key={key} className="flex flex-col">
|
|
600
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
601
|
+
{key.replace(/_/g, ' ')}
|
|
602
|
+
</span>
|
|
603
|
+
<span className="text-sm">{String(value ?? '—')}</span>
|
|
604
|
+
</div>
|
|
605
|
+
))}
|
|
606
|
+
</div>
|
|
607
|
+
)}
|
|
608
|
+
</NavigationOverlay>
|
|
609
|
+
)}
|
|
501
610
|
</div>
|
|
502
611
|
);
|
|
503
612
|
};
|
|
@@ -66,7 +66,7 @@ describe('ListView', () => {
|
|
|
66
66
|
expect(container).toBeTruthy();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it('should render search
|
|
69
|
+
it('should render search button', () => {
|
|
70
70
|
const schema: ListViewSchema = {
|
|
71
71
|
type: 'list-view',
|
|
72
72
|
objectName: 'contacts',
|
|
@@ -75,11 +75,11 @@ describe('ListView', () => {
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
renderWithProvider(<ListView schema={schema} />);
|
|
78
|
-
const
|
|
79
|
-
expect(
|
|
78
|
+
const searchButton = screen.getByRole('button', { name: /search/i });
|
|
79
|
+
expect(searchButton).toBeInTheDocument();
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
it('should call onSearchChange when search input changes', () => {
|
|
82
|
+
it('should expand search and call onSearchChange when search input changes', () => {
|
|
83
83
|
const onSearchChange = vi.fn();
|
|
84
84
|
const schema: ListViewSchema = {
|
|
85
85
|
type: 'list-view',
|
|
@@ -89,8 +89,12 @@ describe('ListView', () => {
|
|
|
89
89
|
};
|
|
90
90
|
|
|
91
91
|
renderWithProvider(<ListView schema={schema} onSearchChange={onSearchChange} />);
|
|
92
|
-
const searchInput = screen.getByPlaceholderText(/find/i);
|
|
93
92
|
|
|
93
|
+
// Click search button to expand
|
|
94
|
+
const searchButton = screen.getByRole('button', { name: /search/i });
|
|
95
|
+
fireEvent.click(searchButton);
|
|
96
|
+
|
|
97
|
+
const searchInput = screen.getByPlaceholderText(/find/i);
|
|
94
98
|
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
95
99
|
expect(onSearchChange).toHaveBeenCalledWith('test');
|
|
96
100
|
});
|
|
@@ -108,7 +112,7 @@ describe('ListView', () => {
|
|
|
108
112
|
},
|
|
109
113
|
};
|
|
110
114
|
|
|
111
|
-
renderWithProvider(<ListView schema={schema} />);
|
|
115
|
+
renderWithProvider(<ListView schema={schema} showViewSwitcher={true} />);
|
|
112
116
|
|
|
113
117
|
// Find kanban view button and click it
|
|
114
118
|
// ViewSwitcher uses buttons with aria-label
|
|
@@ -196,13 +200,18 @@ describe('ListView', () => {
|
|
|
196
200
|
};
|
|
197
201
|
|
|
198
202
|
renderWithProvider(<ListView schema={schema} />);
|
|
203
|
+
|
|
204
|
+
// Click search button to expand search input
|
|
205
|
+
const searchButton = screen.getByRole('button', { name: /search/i });
|
|
206
|
+
fireEvent.click(searchButton);
|
|
207
|
+
|
|
199
208
|
const searchInput = screen.getByPlaceholderText(/find/i) as HTMLInputElement;
|
|
200
209
|
|
|
201
210
|
// Type in search
|
|
202
211
|
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
203
212
|
expect(searchInput.value).toBe('test');
|
|
204
213
|
|
|
205
|
-
// Find and click clear button
|
|
214
|
+
// Find and click clear button (the X button inside the expanded search)
|
|
206
215
|
const buttons = screen.getAllByRole('button');
|
|
207
216
|
const clearButton = buttons.find(btn =>
|
|
208
217
|
btn.querySelector('svg') !== null && searchInput.value !== ''
|
|
@@ -56,7 +56,7 @@ describe('ListView Persistence', () => {
|
|
|
56
56
|
},
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
renderWithProvider(<ListView schema={schema} />);
|
|
59
|
+
renderWithProvider(<ListView schema={schema} showViewSwitcher={true} />);
|
|
60
60
|
|
|
61
61
|
// Simulate changing to kanban view
|
|
62
62
|
const kanbanButton = screen.getByLabelText('Kanban');
|
|
@@ -89,7 +89,7 @@ describe('ListView Persistence', () => {
|
|
|
89
89
|
},
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
renderWithProvider(<ListView schema={viewB_Schema} />);
|
|
92
|
+
renderWithProvider(<ListView schema={viewB_Schema} showViewSwitcher={true} />);
|
|
93
93
|
|
|
94
94
|
// Should use the schema default 'kanban' (since no storage exists for THIS view id)
|
|
95
95
|
// It should NOT use 'grid' from the global/default view.
|
|
@@ -117,10 +117,13 @@ describe('ListView Persistence', () => {
|
|
|
117
117
|
},
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
-
renderWithProvider(<ListView schema={schema} />);
|
|
120
|
+
renderWithProvider(<ListView schema={schema} showViewSwitcher={true} />);
|
|
121
121
|
|
|
122
|
-
// Should respect
|
|
122
|
+
// Should respect schema ('grid') because storage persistence is currently disabled
|
|
123
123
|
const kanbanButton = screen.getByLabelText('Kanban');
|
|
124
|
-
expect(kanbanButton.getAttribute('data-state')).toBe('
|
|
124
|
+
expect(kanbanButton.getAttribute('data-state')).toBe('off');
|
|
125
|
+
|
|
126
|
+
const gridButton = screen.getByLabelText('Grid');
|
|
127
|
+
expect(gridButton.getAttribute('data-state')).toBe('on');
|
|
125
128
|
});
|
|
126
129
|
});
|
package/src/index.tsx
CHANGED
|
@@ -45,3 +45,25 @@ ComponentRegistry.register('list-view', ListView, {
|
|
|
45
45
|
options: {},
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
// Alias for generic view
|
|
50
|
+
ComponentRegistry.register('list', ListView, {
|
|
51
|
+
namespace: 'view',
|
|
52
|
+
category: 'view',
|
|
53
|
+
label: 'List',
|
|
54
|
+
icon: 'LayoutList',
|
|
55
|
+
inputs: [
|
|
56
|
+
{ name: 'objectName', type: 'string', label: 'Object Name', required: true },
|
|
57
|
+
{ name: 'viewType', type: 'enum', label: 'Default View', enum: [
|
|
58
|
+
{ label: 'Grid', value: 'grid' },
|
|
59
|
+
{ label: 'List', value: 'list' },
|
|
60
|
+
{ label: 'Kanban', value: 'kanban' },
|
|
61
|
+
{ label: 'Calendar', value: 'calendar' },
|
|
62
|
+
{ label: 'Chart', value: 'chart' }
|
|
63
|
+
], defaultValue: 'grid' },
|
|
64
|
+
{ name: 'fields', type: 'array', label: 'Fields' },
|
|
65
|
+
{ name: 'filters', type: 'array', label: 'Filters' },
|
|
66
|
+
{ name: 'sort', type: 'array', label: 'Sort' },
|
|
67
|
+
{ name: 'options', type: 'object', label: 'View Options' },
|
|
68
|
+
]
|
|
69
|
+
});
|