Skip to content

Commit a0e3467

Browse files
committed
Enhance ListView component; add filter functionality with Popover UI and fetch object definition from data source
1 parent d1631c4 commit a0e3467

File tree

1 file changed

+138
-24
lines changed

1 file changed

+138
-24
lines changed

packages/plugin-list/src/ListView.tsx

Lines changed: 138 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
*/
88

99
import * as React from 'react';
10-
import { cn, Button, Input } from '@object-ui/components';
10+
import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder } from '@object-ui/components';
1111
import { Search, SlidersHorizontal, ArrowUpDown, X } from 'lucide-react';
12+
import type { FilterGroup } from '@object-ui/components';
1213
import { ViewSwitcher, ViewType } from './ViewSwitcher';
1314
import { SchemaRenderer } from '@object-ui/react';
1415
import type { ListViewSchema } from '@object-ui/types';
@@ -23,6 +24,39 @@ export interface ListViewProps {
2324
[key: string]: any;
2425
}
2526

27+
// Helper to convert FilterBuilder group to ObjectStack AST
28+
function mapOperator(op: string) {
29+
switch (op) {
30+
case 'equals': return '=';
31+
case 'notEquals': return '!=';
32+
case 'contains': return 'contains';
33+
case 'notContains': return 'notcontains';
34+
case 'greaterThan': return '>';
35+
case 'greaterOrEqual': return '>=';
36+
case 'lessThan': return '<';
37+
case 'lessOrEqual': return '<=';
38+
case 'in': return 'in';
39+
case 'notIn': return 'not in';
40+
case 'before': return '<';
41+
case 'after': return '>';
42+
default: return '=';
43+
}
44+
}
45+
46+
function convertFilterGroupToAST(group: FilterGroup): any[] {
47+
if (!group || !group.conditions || group.conditions.length === 0) return [];
48+
49+
const conditions = group.conditions.map(c => {
50+
if (c.operator === 'isEmpty') return [c.field, '=', null];
51+
if (c.operator === 'isNotEmpty') return [c.field, '!=', null];
52+
return [c.field, mapOperator(c.operator), c.value];
53+
});
54+
55+
if (conditions.length === 1) return conditions[0];
56+
57+
return [group.logic, ...conditions];
58+
}
59+
2660
export const ListView: React.FC<ListViewProps> = ({
2761
schema,
2862
className,
@@ -40,17 +74,42 @@ export const ListView: React.FC<ListViewProps> = ({
4074
const [sortOrder, setSortOrder] = React.useState<'asc' | 'desc'>(schema.sort?.[0]?.order || 'asc');
4175
const [showFilters, setShowFilters] = React.useState(false);
4276

77+
const [currentFilters, setCurrentFilters] = React.useState<FilterGroup>({
78+
id: 'root',
79+
logic: 'and',
80+
conditions: []
81+
});
82+
4383
// Data State
4484
const dataSource = props.dataSource;
4585
const [data, setData] = React.useState<any[]>([]);
4686
const [loading, setLoading] = React.useState(false);
87+
const [objectDef, setObjectDef] = React.useState<any>(null);
4788

4889
const storageKey = React.useMemo(() => {
4990
return schema.id
5091
? `listview-${schema.objectName}-${schema.id}-view`
5192
: `listview-${schema.objectName}-view`;
5293
}, [schema.objectName, schema.id]);
5394

95+
// Fetch object definition
96+
React.useEffect(() => {
97+
let isMounted = true;
98+
const fetchObjectDef = async () => {
99+
if (!dataSource || !schema.objectName) return;
100+
try {
101+
const def = await dataSource.getObjectSchema(schema.objectName);
102+
if (isMounted) {
103+
setObjectDef(def);
104+
}
105+
} catch (err) {
106+
console.warn("Failed to fetch object schema for ListView:", err);
107+
}
108+
};
109+
fetchObjectDef();
110+
return () => { isMounted = false; };
111+
}, [schema.objectName, dataSource]);
112+
54113
// Fetch data effect
55114
React.useEffect(() => {
56115
let isMounted = true;
@@ -61,16 +120,25 @@ export const ListView: React.FC<ListViewProps> = ({
61120
setLoading(true);
62121
try {
63122
// Construct filter
64-
let filter: any = schema.filters || [];
65-
// TODO: Merge with searchTerm and user filters
66-
// For now, we rely on the backend/driver to handle $filter
123+
let finalFilter: any = [];
124+
const baseFilter = schema.filters || [];
125+
const userFilter = convertFilterGroupToAST(currentFilters);
126+
127+
// Merge base filters and user filters
128+
if (baseFilter.length > 0 && userFilter.length > 0) {
129+
finalFilter = ['and', baseFilter, userFilter];
130+
} else if (userFilter.length > 0) {
131+
finalFilter = userFilter;
132+
} else {
133+
finalFilter = baseFilter;
134+
}
67135

68136
// Convert sort to query format
69137
// ObjectQL uses simple object: { field: 'asc' }
70138
const sort: any = sortField ? { [sortField]: sortOrder } : undefined;
71139

72140
const results = await dataSource.find(schema.objectName, {
73-
$filter: filter,
141+
$filter: finalFilter,
74142
$orderby: sort,
75143
$top: 100 // Default pagination limit
76144
});
@@ -99,7 +167,7 @@ export const ListView: React.FC<ListViewProps> = ({
99167
fetchData();
100168

101169
return () => { isMounted = false; };
102-
}, [schema.objectName, dataSource, schema.filters, sortField, sortOrder]); // Re-fetch on filter/sort change
170+
}, [schema.objectName, dataSource, schema.filters, sortField, sortOrder, currentFilters]); // Re-fetch on filter/sort change
103171

104172
// Load saved view preference
105173
React.useEffect(() => {
@@ -251,6 +319,30 @@ export const ListView: React.FC<ListViewProps> = ({
251319
return views;
252320
}, [schema.options, schema.viewType]);
253321

322+
const hasFilters = currentFilters.conditions && currentFilters.conditions.length > 0;
323+
324+
const filterFields = React.useMemo(() => {
325+
if (!objectDef?.fields) {
326+
// Fallback to schema fields if objectDef not loaded yet
327+
return (schema.fields || []).map((f: any) => {
328+
if (typeof f === 'string') return { value: f, label: f, type: 'text' };
329+
return {
330+
value: f.name || f.fieldName,
331+
label: f.label || f.name,
332+
type: f.type || 'text',
333+
options: f.options
334+
};
335+
});
336+
}
337+
338+
return Object.entries(objectDef.fields).map(([key, field]: [string, any]) => ({
339+
value: key,
340+
label: field.label || key,
341+
type: field.type || 'text',
342+
options: field.options
343+
}));
344+
}, [objectDef, schema.fields]);
345+
254346
return (
255347
<div className={cn('flex flex-col h-full bg-background', className)}>
256348
{/* Airtable-style Toolbar */}
@@ -267,15 +359,45 @@ export const ListView: React.FC<ListViewProps> = ({
267359

268360
{/* Action Tools */}
269361
<div className="flex items-center gap-1">
270-
<Button
271-
variant={showFilters ? "secondary" : "ghost"}
272-
size="sm"
273-
onClick={() => setShowFilters(!showFilters)}
274-
className="h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary"
275-
>
276-
<SlidersHorizontal className="h-4 w-4 mr-2" />
277-
<span className="hidden lg:inline">Filter</span>
278-
</Button>
362+
<Popover open={showFilters} onOpenChange={setShowFilters}>
363+
<PopoverTrigger asChild>
364+
<Button
365+
variant={hasFilters ? "secondary" : "ghost"}
366+
size="sm"
367+
className={cn(
368+
"h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary",
369+
hasFilters && "text-primary bg-secondary/50"
370+
)}
371+
>
372+
<SlidersHorizontal className="h-4 w-4 mr-2" />
373+
<span className="hidden lg:inline">Filter</span>
374+
{hasFilters && (
375+
<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">
376+
{currentFilters.conditions?.length || 0}
377+
</span>
378+
)}
379+
</Button>
380+
</PopoverTrigger>
381+
<PopoverContent align="start" className="w-[600px] p-4">
382+
<div className="space-y-4">
383+
<div className="flex items-center justify-between border-b pb-2">
384+
<h4 className="font-medium text-sm">Filter Records</h4>
385+
</div>
386+
<FilterBuilder
387+
fields={filterFields}
388+
value={currentFilters}
389+
onChange={(newFilters) => {
390+
console.log('Filter Changed:', newFilters);
391+
setCurrentFilters(newFilters);
392+
// Convert FilterBuilder format to OData $filter string if needed
393+
// For now we just update state and notify listener
394+
// In a real app, this would likely build an OData string
395+
onFilterChange?.(newFilters);
396+
}}
397+
/>
398+
</div>
399+
</PopoverContent>
400+
</Popover>
279401

280402
{sortField && (
281403
<Button
@@ -318,15 +440,7 @@ export const ListView: React.FC<ListViewProps> = ({
318440
</div>
319441

320442

321-
{/* Filters Panel */}
322-
{showFilters && (
323-
<div className="p-4 border rounded-lg bg-muted/30">
324-
<div className="text-sm font-medium mb-2">Filters</div>
325-
<div className="text-xs text-muted-foreground">
326-
Advanced filter UI coming soon. Current filters: {JSON.stringify(schema.filters || [])}
327-
</div>
328-
</div>
329-
)}
443+
{/* Filters Panel - Removed as it is now in Popover */}
330444

331445
{/* View Content */}
332446
<div className="flex-1 min-h-0 bg-background relative overflow-hidden">

0 commit comments

Comments
 (0)