@object-ui/plugin-list 0.5.1 → 3.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 +35 -0
- package/dist/index.js +23567 -21568
- package/dist/index.umd.cjs +37 -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/dist/src/ListView.stories.d.ts +24 -0
- package/dist/src/ListView.stories.d.ts.map +1 -0
- package/dist/src/ObjectGallery.d.ts +23 -1
- package/dist/src/ObjectGallery.d.ts.map +1 -1
- package/dist/src/ViewSwitcher.d.ts +2 -0
- package/dist/src/ViewSwitcher.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/ListView.stories.tsx +64 -0
- package/src/ListView.tsx +239 -120
- package/src/ObjectGallery.tsx +122 -53
- package/src/ViewSwitcher.tsx +23 -2
- package/src/__tests__/ListView.test.tsx +16 -7
- package/src/__tests__/ListViewPersistence.test.tsx +3 -3
- package/src/index.tsx +23 -1
- package/src/registration.test.tsx +1 -2
- package/vitest.config.ts +0 -1
|
@@ -12,6 +12,8 @@ export interface ViewSwitcherProps {
|
|
|
12
12
|
availableViews?: ViewType[];
|
|
13
13
|
onViewChange: (view: ViewType) => void;
|
|
14
14
|
className?: string;
|
|
15
|
+
/** Enable animated transitions between views (default: true) */
|
|
16
|
+
animated?: boolean;
|
|
15
17
|
}
|
|
16
18
|
export declare const ViewSwitcher: React.FC<ViewSwitcherProps>;
|
|
17
19
|
//# sourceMappingURL=ViewSwitcher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ViewSwitcher.d.ts","sourceRoot":"","sources":["../../src/ViewSwitcher.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAY/B,MAAM,MAAM,QAAQ,GAChB,MAAM,GACN,QAAQ,GACR,SAAS,GACT,UAAU,GACV,UAAU,GACV,OAAO,GACP,KAAK,CAAC;AAEV,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,QAAQ,CAAC;IACtB,cAAc,CAAC,EAAE,QAAQ,EAAE,CAAC;IAC5B,YAAY,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsBD,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,
|
|
1
|
+
{"version":3,"file":"ViewSwitcher.d.ts","sourceRoot":"","sources":["../../src/ViewSwitcher.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAY/B,MAAM,MAAM,QAAQ,GAChB,MAAM,GACN,QAAQ,GACR,SAAS,GACT,UAAU,GACV,UAAU,GACV,OAAO,GACP,KAAK,CAAC;AAEV,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,QAAQ,CAAC;IACtB,cAAc,CAAC,EAAE,QAAQ,EAAE,CAAC;IAC5B,YAAY,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAsBD,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAsDpD,CAAC"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ import { ViewSwitcher } from './ViewSwitcher';
|
|
|
3
3
|
import { ObjectGallery } from './ObjectGallery';
|
|
4
4
|
export { ListView, ViewSwitcher, ObjectGallery };
|
|
5
5
|
export type { ListViewProps } from './ListView';
|
|
6
|
+
export type { ObjectGalleryProps } from './ObjectGallery';
|
|
6
7
|
export type { ViewSwitcherProps, ViewType } from './ViewSwitcher';
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,YAAY,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/plugin-list",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "ListView plugin for Object UI - unified view component with view type switching",
|
|
@@ -25,19 +25,20 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"lucide-react": "^0.563.0",
|
|
28
|
-
"@object-ui/components": "0.
|
|
29
|
-
"@object-ui/
|
|
30
|
-
"@object-ui/
|
|
31
|
-
"@object-ui/
|
|
28
|
+
"@object-ui/components": "3.0.0",
|
|
29
|
+
"@object-ui/core": "3.0.0",
|
|
30
|
+
"@object-ui/mobile": "3.0.0",
|
|
31
|
+
"@object-ui/react": "3.0.0",
|
|
32
|
+
"@object-ui/types": "3.0.0"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"react": "^18.0.0 || ^19.0.0",
|
|
35
36
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
|
-
"@types/react": "
|
|
39
|
-
"@types/react-dom": "
|
|
40
|
-
"@vitejs/plugin-react": "^5.1.
|
|
39
|
+
"@types/react": "19.2.13",
|
|
40
|
+
"@types/react-dom": "19.2.3",
|
|
41
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
41
42
|
"typescript": "^5.9.3",
|
|
42
43
|
"vite": "^7.3.1",
|
|
43
44
|
"vite-plugin-dts": "^4.5.4",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { SchemaRenderer } from '@object-ui/react';
|
|
3
|
+
import type { BaseSchema } from '@object-ui/types';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Plugins/ListView',
|
|
7
|
+
component: SchemaRenderer,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'padded',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
argTypes: {
|
|
13
|
+
schema: { table: { disable: true } },
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<any>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
|
|
21
|
+
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
render: renderStory,
|
|
24
|
+
args: {
|
|
25
|
+
type: 'list-view',
|
|
26
|
+
objectName: 'contacts',
|
|
27
|
+
viewType: 'grid',
|
|
28
|
+
fields: ['name', 'email', 'phone', 'company'],
|
|
29
|
+
sort: [{ field: 'name', order: 'asc' }],
|
|
30
|
+
} as any,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const KanbanView: Story = {
|
|
34
|
+
render: renderStory,
|
|
35
|
+
args: {
|
|
36
|
+
type: 'list-view',
|
|
37
|
+
objectName: 'deals',
|
|
38
|
+
viewType: 'kanban',
|
|
39
|
+
fields: ['name', 'amount', 'stage', 'close_date'],
|
|
40
|
+
options: {
|
|
41
|
+
kanban: {
|
|
42
|
+
groupField: 'stage',
|
|
43
|
+
titleField: 'name',
|
|
44
|
+
cardFields: ['amount', 'close_date'],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
} as any,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const WithFilters: Story = {
|
|
51
|
+
render: renderStory,
|
|
52
|
+
args: {
|
|
53
|
+
type: 'list-view',
|
|
54
|
+
objectName: 'opportunities',
|
|
55
|
+
viewType: 'grid',
|
|
56
|
+
fields: ['name', 'amount', 'stage', 'owner', 'close_date'],
|
|
57
|
+
filters: [
|
|
58
|
+
['stage', '=', 'Prospecting'],
|
|
59
|
+
'OR',
|
|
60
|
+
['stage', '=', 'Qualification'],
|
|
61
|
+
],
|
|
62
|
+
sort: [{ field: 'amount', order: 'desc' }],
|
|
63
|
+
} as any,
|
|
64
|
+
};
|
package/src/ListView.tsx
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
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
|
+
import { usePullToRefresh } from '@object-ui/mobile';
|
|
17
18
|
|
|
18
19
|
export interface ListViewProps {
|
|
19
20
|
schema: ListViewSchema;
|
|
@@ -22,6 +23,10 @@ export interface ListViewProps {
|
|
|
22
23
|
onFilterChange?: (filters: any) => void;
|
|
23
24
|
onSortChange?: (sort: any) => void;
|
|
24
25
|
onSearchChange?: (search: string) => void;
|
|
26
|
+
/** Callback when a row/item is clicked (overrides NavigationConfig) */
|
|
27
|
+
onRowClick?: (record: Record<string, unknown>) => void;
|
|
28
|
+
/** Show view type switcher (Grid/Kanban/etc). Default: false (view type is fixed) */
|
|
29
|
+
showViewSwitcher?: boolean;
|
|
25
30
|
[key: string]: any;
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -65,6 +70,8 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
65
70
|
onFilterChange,
|
|
66
71
|
onSortChange,
|
|
67
72
|
onSearchChange,
|
|
73
|
+
onRowClick,
|
|
74
|
+
showViewSwitcher = false,
|
|
68
75
|
...props
|
|
69
76
|
}) => {
|
|
70
77
|
// Kernel level default: Ensure viewType is always defined (default to 'grid')
|
|
@@ -104,6 +111,16 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
104
111
|
const [data, setData] = React.useState<any[]>([]);
|
|
105
112
|
const [loading, setLoading] = React.useState(false);
|
|
106
113
|
const [objectDef, setObjectDef] = React.useState<any>(null);
|
|
114
|
+
const [refreshKey, setRefreshKey] = React.useState(0);
|
|
115
|
+
|
|
116
|
+
const handlePullRefresh = React.useCallback(async () => {
|
|
117
|
+
setRefreshKey(k => k + 1);
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const { ref: pullRef, isRefreshing, pullDistance } = usePullToRefresh<HTMLDivElement>({
|
|
121
|
+
onRefresh: handlePullRefresh,
|
|
122
|
+
enabled: !!dataSource && !!schema.objectName,
|
|
123
|
+
});
|
|
107
124
|
|
|
108
125
|
const storageKey = React.useMemo(() => {
|
|
109
126
|
return schema.id
|
|
@@ -172,8 +189,8 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
172
189
|
} else if (results && typeof results === 'object') {
|
|
173
190
|
if (Array.isArray((results as any).data)) {
|
|
174
191
|
items = (results as any).data;
|
|
175
|
-
} else if (Array.isArray((results as any).
|
|
176
|
-
items = (results as any).
|
|
192
|
+
} else if (Array.isArray((results as any).records)) {
|
|
193
|
+
items = (results as any).records;
|
|
177
194
|
}
|
|
178
195
|
}
|
|
179
196
|
|
|
@@ -190,7 +207,7 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
190
207
|
fetchData();
|
|
191
208
|
|
|
192
209
|
return () => { isMounted = false; };
|
|
193
|
-
}, [schema.objectName, dataSource, schema.filters, currentSort, currentFilters]); // Re-fetch on filter/sort change
|
|
210
|
+
}, [schema.objectName, dataSource, schema.filters, currentSort, currentFilters, refreshKey]); // Re-fetch on filter/sort change
|
|
194
211
|
|
|
195
212
|
// Available view types based on schema configuration
|
|
196
213
|
const availableViews = React.useMemo(() => {
|
|
@@ -272,6 +289,14 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
272
289
|
onSearchChange?.(value);
|
|
273
290
|
}, [onSearchChange]);
|
|
274
291
|
|
|
292
|
+
// --- NavigationConfig support ---
|
|
293
|
+
const navigation = useNavigationOverlay({
|
|
294
|
+
navigation: schema.navigation,
|
|
295
|
+
objectName: schema.objectName,
|
|
296
|
+
onNavigate: schema.onNavigate,
|
|
297
|
+
onRowClick,
|
|
298
|
+
});
|
|
299
|
+
|
|
275
300
|
// Generate the appropriate view component schema
|
|
276
301
|
const viewComponentSchema = React.useMemo(() => {
|
|
277
302
|
const baseProps = {
|
|
@@ -282,6 +307,8 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
282
307
|
className: "h-full w-full",
|
|
283
308
|
// Disable internal controls that clash with ListView toolbar
|
|
284
309
|
showSearch: false,
|
|
310
|
+
// Pass navigation click handler to child views
|
|
311
|
+
onRowClick: navigation.handleClick,
|
|
285
312
|
};
|
|
286
313
|
|
|
287
314
|
switch (currentView) {
|
|
@@ -374,124 +401,189 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
374
401
|
}));
|
|
375
402
|
}, [objectDef, schema.fields]);
|
|
376
403
|
|
|
404
|
+
const [searchExpanded, setSearchExpanded] = React.useState(false);
|
|
405
|
+
|
|
377
406
|
return (
|
|
378
|
-
<div className={cn('flex flex-col h-full bg-background', className)}>
|
|
379
|
-
{
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
{
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
<Button
|
|
396
|
-
variant={hasFilters ? "secondary" : "ghost"}
|
|
397
|
-
size="sm"
|
|
398
|
-
className={cn(
|
|
399
|
-
"h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary",
|
|
400
|
-
hasFilters && "text-primary bg-secondary/50"
|
|
401
|
-
)}
|
|
402
|
-
>
|
|
403
|
-
<SlidersHorizontal className="h-4 w-4 mr-2" />
|
|
404
|
-
<span className="hidden lg:inline">Filter</span>
|
|
405
|
-
{hasFilters && (
|
|
406
|
-
<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">
|
|
407
|
-
{currentFilters.conditions?.length || 0}
|
|
408
|
-
</span>
|
|
409
|
-
)}
|
|
410
|
-
</Button>
|
|
411
|
-
</PopoverTrigger>
|
|
412
|
-
<PopoverContent align="start" className="w-[600px] p-4">
|
|
413
|
-
<div className="space-y-4">
|
|
414
|
-
<div className="flex items-center justify-between border-b pb-2">
|
|
415
|
-
<h4 className="font-medium text-sm">Filter Records</h4>
|
|
416
|
-
</div>
|
|
417
|
-
<FilterBuilder
|
|
418
|
-
fields={filterFields}
|
|
419
|
-
value={currentFilters}
|
|
420
|
-
onChange={(newFilters) => {
|
|
421
|
-
console.log('Filter Changed:', newFilters);
|
|
422
|
-
setCurrentFilters(newFilters);
|
|
423
|
-
// Convert FilterBuilder format to OData $filter string if needed
|
|
424
|
-
// For now we just update state and notify listener
|
|
425
|
-
// In a real app, this would likely build an OData string
|
|
426
|
-
if (onFilterChange) onFilterChange(newFilters);
|
|
427
|
-
}}
|
|
428
|
-
/>
|
|
429
|
-
</div>
|
|
430
|
-
</PopoverContent>
|
|
431
|
-
</Popover>
|
|
432
|
-
|
|
433
|
-
<Popover open={showSort} onOpenChange={setShowSort}>
|
|
434
|
-
<PopoverTrigger asChild>
|
|
435
|
-
<Button
|
|
436
|
-
variant={currentSort.length > 0 ? "secondary" : "ghost"}
|
|
437
|
-
size="sm"
|
|
438
|
-
className={cn(
|
|
439
|
-
"h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary",
|
|
440
|
-
currentSort.length > 0 && "text-primary bg-secondary/50"
|
|
441
|
-
)}
|
|
442
|
-
>
|
|
443
|
-
<ArrowUpDown className="h-4 w-4 mr-2" />
|
|
444
|
-
<span className="hidden lg:inline">Sort</span>
|
|
445
|
-
{currentSort.length > 0 && (
|
|
446
|
-
<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">
|
|
447
|
-
{currentSort.length}
|
|
448
|
-
</span>
|
|
449
|
-
)}
|
|
450
|
-
</Button>
|
|
451
|
-
</PopoverTrigger>
|
|
452
|
-
<PopoverContent align="start" className="w-[600px] p-4">
|
|
453
|
-
<div className="space-y-4">
|
|
454
|
-
<div className="flex items-center justify-between border-b pb-2">
|
|
455
|
-
<h4 className="font-medium text-sm">Sort Records</h4>
|
|
456
|
-
</div>
|
|
457
|
-
<SortBuilder
|
|
458
|
-
fields={filterFields}
|
|
459
|
-
value={currentSort}
|
|
460
|
-
onChange={(newSort) => {
|
|
461
|
-
console.log('Sort Changed:', newSort);
|
|
462
|
-
setCurrentSort(newSort);
|
|
463
|
-
if (onSortChange) onSortChange(newSort);
|
|
464
|
-
}}
|
|
465
|
-
/>
|
|
466
|
-
</div>
|
|
467
|
-
</PopoverContent>
|
|
468
|
-
</Popover>
|
|
469
|
-
|
|
470
|
-
{/* Future: Group, Color, Height */}
|
|
471
|
-
</div>
|
|
407
|
+
<div ref={pullRef} className={cn('flex flex-col h-full bg-background relative', className)}>
|
|
408
|
+
{pullDistance > 0 && (
|
|
409
|
+
<div
|
|
410
|
+
className="flex items-center justify-center text-xs text-muted-foreground"
|
|
411
|
+
style={{ height: pullDistance }}
|
|
412
|
+
>
|
|
413
|
+
{isRefreshing ? 'Refreshing…' : 'Pull to refresh'}
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
416
|
+
{/* Airtable-style Toolbar — Row 1: View tabs */}
|
|
417
|
+
{showViewSwitcher && (
|
|
418
|
+
<div className="border-b px-4 py-1 flex items-center bg-background">
|
|
419
|
+
<ViewSwitcher
|
|
420
|
+
currentView={currentView}
|
|
421
|
+
availableViews={availableViews}
|
|
422
|
+
onViewChange={handleViewChange}
|
|
423
|
+
/>
|
|
472
424
|
</div>
|
|
425
|
+
)}
|
|
426
|
+
|
|
427
|
+
{/* Airtable-style Toolbar — Row 2: Tool buttons */}
|
|
428
|
+
<div className="border-b px-2 sm:px-4 py-1 flex items-center justify-between gap-1 sm:gap-2 bg-background">
|
|
429
|
+
<div className="flex items-center gap-0.5 overflow-hidden flex-1 min-w-0">
|
|
430
|
+
{/* Hide Fields */}
|
|
431
|
+
<Button
|
|
432
|
+
variant="ghost"
|
|
433
|
+
size="sm"
|
|
434
|
+
className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
|
|
435
|
+
disabled
|
|
436
|
+
>
|
|
437
|
+
<EyeOff className="h-3.5 w-3.5 mr-1.5" />
|
|
438
|
+
<span className="hidden sm:inline">Hide fields</span>
|
|
439
|
+
</Button>
|
|
473
440
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
441
|
+
{/* Filter */}
|
|
442
|
+
<Popover open={showFilters} onOpenChange={setShowFilters}>
|
|
443
|
+
<PopoverTrigger asChild>
|
|
444
|
+
<Button
|
|
445
|
+
variant="ghost"
|
|
446
|
+
size="sm"
|
|
447
|
+
className={cn(
|
|
448
|
+
"h-7 px-2 text-muted-foreground hover:text-primary text-xs",
|
|
449
|
+
hasFilters && "text-primary"
|
|
450
|
+
)}
|
|
451
|
+
>
|
|
452
|
+
<SlidersHorizontal className="h-3.5 w-3.5 mr-1.5" />
|
|
453
|
+
<span className="hidden sm:inline">Filter</span>
|
|
454
|
+
{hasFilters && (
|
|
455
|
+
<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">
|
|
456
|
+
{currentFilters.conditions?.length || 0}
|
|
457
|
+
</span>
|
|
458
|
+
)}
|
|
459
|
+
</Button>
|
|
460
|
+
</PopoverTrigger>
|
|
461
|
+
<PopoverContent align="start" className="w-[calc(100vw-2rem)] sm:w-[600px] max-w-[600px] p-3 sm:p-4">
|
|
462
|
+
<div className="space-y-4">
|
|
463
|
+
<div className="flex items-center justify-between border-b pb-2">
|
|
464
|
+
<h4 className="font-medium text-sm">Filter Records</h4>
|
|
465
|
+
</div>
|
|
466
|
+
<FilterBuilder
|
|
467
|
+
fields={filterFields}
|
|
468
|
+
value={currentFilters}
|
|
469
|
+
onChange={(newFilters) => {
|
|
470
|
+
setCurrentFilters(newFilters);
|
|
471
|
+
if (onFilterChange) onFilterChange(newFilters);
|
|
472
|
+
}}
|
|
483
473
|
/>
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
474
|
+
</div>
|
|
475
|
+
</PopoverContent>
|
|
476
|
+
</Popover>
|
|
477
|
+
|
|
478
|
+
{/* Group */}
|
|
479
|
+
<Button
|
|
480
|
+
variant="ghost"
|
|
481
|
+
size="sm"
|
|
482
|
+
className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
|
|
483
|
+
disabled
|
|
484
|
+
>
|
|
485
|
+
<Group className="h-3.5 w-3.5 mr-1.5" />
|
|
486
|
+
<span className="hidden sm:inline">Group</span>
|
|
487
|
+
</Button>
|
|
488
|
+
|
|
489
|
+
{/* Sort */}
|
|
490
|
+
<Popover open={showSort} onOpenChange={setShowSort}>
|
|
491
|
+
<PopoverTrigger asChild>
|
|
492
|
+
<Button
|
|
493
|
+
variant="ghost"
|
|
494
|
+
size="sm"
|
|
495
|
+
className={cn(
|
|
496
|
+
"h-7 px-2 text-muted-foreground hover:text-primary text-xs",
|
|
497
|
+
currentSort.length > 0 && "text-primary"
|
|
498
|
+
)}
|
|
499
|
+
>
|
|
500
|
+
<ArrowUpDown className="h-3.5 w-3.5 mr-1.5" />
|
|
501
|
+
<span className="hidden sm:inline">Sort</span>
|
|
502
|
+
{currentSort.length > 0 && (
|
|
503
|
+
<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">
|
|
504
|
+
{currentSort.length}
|
|
505
|
+
</span>
|
|
493
506
|
)}
|
|
494
|
-
|
|
507
|
+
</Button>
|
|
508
|
+
</PopoverTrigger>
|
|
509
|
+
<PopoverContent align="start" className="w-[calc(100vw-2rem)] sm:w-[600px] max-w-[600px] p-3 sm:p-4">
|
|
510
|
+
<div className="space-y-4">
|
|
511
|
+
<div className="flex items-center justify-between border-b pb-2">
|
|
512
|
+
<h4 className="font-medium text-sm">Sort Records</h4>
|
|
513
|
+
</div>
|
|
514
|
+
<SortBuilder
|
|
515
|
+
fields={filterFields}
|
|
516
|
+
value={currentSort}
|
|
517
|
+
onChange={(newSort) => {
|
|
518
|
+
setCurrentSort(newSort);
|
|
519
|
+
if (onSortChange) onSortChange(newSort);
|
|
520
|
+
}}
|
|
521
|
+
/>
|
|
522
|
+
</div>
|
|
523
|
+
</PopoverContent>
|
|
524
|
+
</Popover>
|
|
525
|
+
|
|
526
|
+
{/* Color */}
|
|
527
|
+
<Button
|
|
528
|
+
variant="ghost"
|
|
529
|
+
size="sm"
|
|
530
|
+
className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
|
|
531
|
+
disabled
|
|
532
|
+
>
|
|
533
|
+
<Paintbrush className="h-3.5 w-3.5 mr-1.5" />
|
|
534
|
+
<span className="hidden sm:inline">Color</span>
|
|
535
|
+
</Button>
|
|
536
|
+
|
|
537
|
+
{/* Row Height */}
|
|
538
|
+
<Button
|
|
539
|
+
variant="ghost"
|
|
540
|
+
size="sm"
|
|
541
|
+
className="h-7 px-2 text-muted-foreground hover:text-primary text-xs hidden lg:flex"
|
|
542
|
+
disabled
|
|
543
|
+
>
|
|
544
|
+
<Ruler className="h-3.5 w-3.5 mr-1.5" />
|
|
545
|
+
<span className="hidden sm:inline">Row height</span>
|
|
546
|
+
</Button>
|
|
547
|
+
</div>
|
|
548
|
+
|
|
549
|
+
{/* Right: Search */}
|
|
550
|
+
<div className="flex items-center gap-1">
|
|
551
|
+
{searchExpanded ? (
|
|
552
|
+
<div className="relative w-36 sm:w-48 lg:w-64">
|
|
553
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
|
554
|
+
<Input
|
|
555
|
+
placeholder="Find..."
|
|
556
|
+
value={searchTerm}
|
|
557
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
|
558
|
+
className="pl-7 h-7 text-xs bg-muted/50 border-transparent hover:bg-muted focus:bg-background focus:border-input transition-colors"
|
|
559
|
+
autoFocus
|
|
560
|
+
onBlur={() => {
|
|
561
|
+
if (!searchTerm) setSearchExpanded(false);
|
|
562
|
+
}}
|
|
563
|
+
/>
|
|
564
|
+
<Button
|
|
565
|
+
variant="ghost"
|
|
566
|
+
size="sm"
|
|
567
|
+
className="absolute right-0.5 top-1/2 -translate-y-1/2 h-5 w-5 p-0 hover:bg-muted-foreground/20"
|
|
568
|
+
onClick={() => {
|
|
569
|
+
handleSearchChange('');
|
|
570
|
+
setSearchExpanded(false);
|
|
571
|
+
}}
|
|
572
|
+
>
|
|
573
|
+
<X className="h-3 w-3" />
|
|
574
|
+
</Button>
|
|
575
|
+
</div>
|
|
576
|
+
) : (
|
|
577
|
+
<Button
|
|
578
|
+
variant="ghost"
|
|
579
|
+
size="sm"
|
|
580
|
+
className="h-7 px-2 text-muted-foreground hover:text-primary text-xs"
|
|
581
|
+
onClick={() => setSearchExpanded(true)}
|
|
582
|
+
>
|
|
583
|
+
<Search className="h-3.5 w-3.5 mr-1.5" />
|
|
584
|
+
<span className="hidden sm:inline">Search</span>
|
|
585
|
+
</Button>
|
|
586
|
+
)}
|
|
495
587
|
</div>
|
|
496
588
|
</div>
|
|
497
589
|
|
|
@@ -499,7 +591,7 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
499
591
|
{/* Filters Panel - Removed as it is now in Popover */}
|
|
500
592
|
|
|
501
593
|
{/* View Content */}
|
|
502
|
-
<div className="flex-1 min-h-0 bg-background relative overflow-hidden">
|
|
594
|
+
<div key={currentView} className="flex-1 min-h-0 bg-background relative overflow-hidden animate-in fade-in-0 duration-200">
|
|
503
595
|
<SchemaRenderer
|
|
504
596
|
schema={viewComponentSchema}
|
|
505
597
|
{...props}
|
|
@@ -507,6 +599,33 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
507
599
|
loading={loading}
|
|
508
600
|
/>
|
|
509
601
|
</div>
|
|
602
|
+
|
|
603
|
+
{/* Navigation Overlay (drawer/modal/popover) */}
|
|
604
|
+
{navigation.isOverlay && (
|
|
605
|
+
<NavigationOverlay
|
|
606
|
+
{...navigation}
|
|
607
|
+
title={
|
|
608
|
+
schema.label
|
|
609
|
+
? `${schema.label} Detail`
|
|
610
|
+
: schema.objectName
|
|
611
|
+
? `${schema.objectName.charAt(0).toUpperCase() + schema.objectName.slice(1)} Detail`
|
|
612
|
+
: 'Record Detail'
|
|
613
|
+
}
|
|
614
|
+
>
|
|
615
|
+
{(record) => (
|
|
616
|
+
<div className="space-y-3">
|
|
617
|
+
{Object.entries(record).map(([key, value]) => (
|
|
618
|
+
<div key={key} className="flex flex-col">
|
|
619
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
620
|
+
{key.replace(/_/g, ' ')}
|
|
621
|
+
</span>
|
|
622
|
+
<span className="text-sm">{String(value ?? '—')}</span>
|
|
623
|
+
</div>
|
|
624
|
+
))}
|
|
625
|
+
</div>
|
|
626
|
+
)}
|
|
627
|
+
</NavigationOverlay>
|
|
628
|
+
)}
|
|
510
629
|
</div>
|
|
511
630
|
);
|
|
512
631
|
};
|