@object-ui/plugin-list 2.0.0 → 3.0.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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +32 -0
- package/dist/index.js +12196 -11942
- package/dist/index.umd.cjs +28 -19
- package/dist/plugin-list.css +1 -1
- 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 +29 -10
- package/src/ObjectGallery.tsx +122 -53
- package/src/ViewSwitcher.tsx +23 -2
- package/src/index.tsx +1 -1
- package/src/registration.test.tsx +1 -2
- package/vitest.config.ts +0 -1
|
@@ -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": "
|
|
3
|
+
"version": "3.0.1",
|
|
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": "
|
|
29
|
-
"@object-ui/core": "
|
|
30
|
-
"@object-ui/
|
|
31
|
-
"@object-ui/
|
|
28
|
+
"@object-ui/components": "3.0.1",
|
|
29
|
+
"@object-ui/core": "3.0.1",
|
|
30
|
+
"@object-ui/mobile": "3.0.1",
|
|
31
|
+
"@object-ui/react": "3.0.1",
|
|
32
|
+
"@object-ui/types": "3.0.1"
|
|
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
|
@@ -14,6 +14,7 @@ import type { FilterGroup } from '@object-ui/components';
|
|
|
14
14
|
import { ViewSwitcher, ViewType } from './ViewSwitcher';
|
|
15
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;
|
|
@@ -110,6 +111,16 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
110
111
|
const [data, setData] = React.useState<any[]>([]);
|
|
111
112
|
const [loading, setLoading] = React.useState(false);
|
|
112
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
|
+
});
|
|
113
124
|
|
|
114
125
|
const storageKey = React.useMemo(() => {
|
|
115
126
|
return schema.id
|
|
@@ -178,8 +189,8 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
178
189
|
} else if (results && typeof results === 'object') {
|
|
179
190
|
if (Array.isArray((results as any).data)) {
|
|
180
191
|
items = (results as any).data;
|
|
181
|
-
} else if (Array.isArray((results as any).
|
|
182
|
-
items = (results as any).
|
|
192
|
+
} else if (Array.isArray((results as any).records)) {
|
|
193
|
+
items = (results as any).records;
|
|
183
194
|
}
|
|
184
195
|
}
|
|
185
196
|
|
|
@@ -196,7 +207,7 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
196
207
|
fetchData();
|
|
197
208
|
|
|
198
209
|
return () => { isMounted = false; };
|
|
199
|
-
}, [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
|
|
200
211
|
|
|
201
212
|
// Available view types based on schema configuration
|
|
202
213
|
const availableViews = React.useMemo(() => {
|
|
@@ -393,7 +404,15 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
393
404
|
const [searchExpanded, setSearchExpanded] = React.useState(false);
|
|
394
405
|
|
|
395
406
|
return (
|
|
396
|
-
<div className={cn('flex flex-col h-full bg-background', className)}>
|
|
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
|
+
)}
|
|
397
416
|
{/* Airtable-style Toolbar — Row 1: View tabs */}
|
|
398
417
|
{showViewSwitcher && (
|
|
399
418
|
<div className="border-b px-4 py-1 flex items-center bg-background">
|
|
@@ -406,8 +425,8 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
406
425
|
)}
|
|
407
426
|
|
|
408
427
|
{/* 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">
|
|
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">
|
|
411
430
|
{/* Hide Fields */}
|
|
412
431
|
<Button
|
|
413
432
|
variant="ghost"
|
|
@@ -439,7 +458,7 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
439
458
|
)}
|
|
440
459
|
</Button>
|
|
441
460
|
</PopoverTrigger>
|
|
442
|
-
<PopoverContent align="start" className="w-[600px] p-4">
|
|
461
|
+
<PopoverContent align="start" className="w-[calc(100vw-2rem)] sm:w-[600px] max-w-[600px] p-3 sm:p-4">
|
|
443
462
|
<div className="space-y-4">
|
|
444
463
|
<div className="flex items-center justify-between border-b pb-2">
|
|
445
464
|
<h4 className="font-medium text-sm">Filter Records</h4>
|
|
@@ -487,7 +506,7 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
487
506
|
)}
|
|
488
507
|
</Button>
|
|
489
508
|
</PopoverTrigger>
|
|
490
|
-
<PopoverContent align="start" className="w-[600px] p-4">
|
|
509
|
+
<PopoverContent align="start" className="w-[calc(100vw-2rem)] sm:w-[600px] max-w-[600px] p-3 sm:p-4">
|
|
491
510
|
<div className="space-y-4">
|
|
492
511
|
<div className="flex items-center justify-between border-b pb-2">
|
|
493
512
|
<h4 className="font-medium text-sm">Sort Records</h4>
|
|
@@ -530,7 +549,7 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
530
549
|
{/* Right: Search */}
|
|
531
550
|
<div className="flex items-center gap-1">
|
|
532
551
|
{searchExpanded ? (
|
|
533
|
-
<div className="relative w-48 lg:w-64">
|
|
552
|
+
<div className="relative w-36 sm:w-48 lg:w-64">
|
|
534
553
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
|
535
554
|
<Input
|
|
536
555
|
placeholder="Find..."
|
|
@@ -572,7 +591,7 @@ export const ListView: React.FC<ListViewProps> = ({
|
|
|
572
591
|
{/* Filters Panel - Removed as it is now in Popover */}
|
|
573
592
|
|
|
574
593
|
{/* View Content */}
|
|
575
|
-
<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">
|
|
576
595
|
<SchemaRenderer
|
|
577
596
|
schema={viewComponentSchema}
|
|
578
597
|
{...props}
|
package/src/ObjectGallery.tsx
CHANGED
|
@@ -1,27 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
1
8
|
|
|
2
9
|
import React, { useState, useEffect } from 'react';
|
|
3
10
|
import { useDataScope, useSchemaContext } from '@object-ui/react';
|
|
4
11
|
import { ComponentRegistry } from '@object-ui/core';
|
|
12
|
+
import { cn, Card, CardContent } from '@object-ui/components';
|
|
13
|
+
import type { GalleryConfig } from '@object-ui/types';
|
|
5
14
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
export interface ObjectGalleryProps {
|
|
16
|
+
schema: {
|
|
17
|
+
objectName?: string;
|
|
18
|
+
bind?: string;
|
|
19
|
+
filter?: unknown;
|
|
20
|
+
data?: Record<string, unknown>[];
|
|
21
|
+
className?: string;
|
|
22
|
+
gallery?: GalleryConfig;
|
|
23
|
+
/** @deprecated Use gallery.coverField instead */
|
|
24
|
+
imageField?: string;
|
|
25
|
+
/** @deprecated Use gallery.titleField instead */
|
|
26
|
+
titleField?: string;
|
|
27
|
+
subtitleField?: string;
|
|
28
|
+
};
|
|
29
|
+
data?: Record<string, unknown>[];
|
|
30
|
+
dataSource?: { find: (name: string, query: unknown) => Promise<unknown> };
|
|
31
|
+
onCardClick?: (record: Record<string, unknown>) => void;
|
|
32
|
+
}
|
|
10
33
|
|
|
11
|
-
|
|
34
|
+
const GRID_CLASSES: Record<NonNullable<GalleryConfig['cardSize']>, string> = {
|
|
35
|
+
small: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6',
|
|
36
|
+
medium: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
|
|
37
|
+
large: 'grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const ASPECT_CLASSES: Record<NonNullable<GalleryConfig['cardSize']>, string> = {
|
|
41
|
+
small: 'aspect-square',
|
|
42
|
+
medium: 'aspect-[4/3]',
|
|
43
|
+
large: 'aspect-[16/10]',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const ObjectGallery: React.FC<ObjectGalleryProps> = (props) => {
|
|
12
47
|
const { schema } = props;
|
|
13
48
|
const context = useSchemaContext();
|
|
14
49
|
const dataSource = props.dataSource || context.dataSource;
|
|
15
50
|
const boundData = useDataScope(schema.bind);
|
|
16
51
|
|
|
17
|
-
const [fetchedData, setFetchedData] = useState<
|
|
52
|
+
const [fetchedData, setFetchedData] = useState<Record<string, unknown>[]>([]);
|
|
18
53
|
const [loading, setLoading] = useState(false);
|
|
19
54
|
|
|
55
|
+
// Resolve GalleryConfig with backwards-compatible fallbacks
|
|
56
|
+
const gallery = schema.gallery;
|
|
57
|
+
const coverField = gallery?.coverField ?? schema.imageField ?? 'image';
|
|
58
|
+
const coverFit = gallery?.coverFit ?? 'cover';
|
|
59
|
+
const cardSize = gallery?.cardSize ?? 'medium';
|
|
60
|
+
const titleField = gallery?.titleField ?? schema.titleField ?? 'name';
|
|
61
|
+
const visibleFields = gallery?.visibleFields;
|
|
62
|
+
|
|
20
63
|
useEffect(() => {
|
|
21
64
|
let isMounted = true;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if ((props.data && Array.isArray(props.data))) {
|
|
65
|
+
|
|
66
|
+
if (props.data && Array.isArray(props.data)) {
|
|
25
67
|
setFetchedData(props.data);
|
|
26
68
|
return;
|
|
27
69
|
}
|
|
@@ -30,22 +72,22 @@ export const ObjectGallery = (props: any) => {
|
|
|
30
72
|
if (!dataSource || !schema.objectName) return;
|
|
31
73
|
if (isMounted) setLoading(true);
|
|
32
74
|
try {
|
|
33
|
-
// Apply filtering?
|
|
34
75
|
const results = await dataSource.find(schema.objectName, {
|
|
35
|
-
|
|
76
|
+
$filter: schema.filter,
|
|
36
77
|
});
|
|
37
|
-
|
|
38
|
-
let data:
|
|
78
|
+
|
|
79
|
+
let data: Record<string, unknown>[] = [];
|
|
39
80
|
if (Array.isArray(results)) {
|
|
40
81
|
data = results;
|
|
41
82
|
} else if (results && typeof results === 'object') {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
83
|
+
const r = results as Record<string, unknown>;
|
|
84
|
+
if (Array.isArray(r.records)) {
|
|
85
|
+
data = r.records as Record<string, unknown>[];
|
|
86
|
+
} else if (Array.isArray(r.data)) {
|
|
87
|
+
data = r.data as Record<string, unknown>[];
|
|
46
88
|
}
|
|
47
89
|
}
|
|
48
|
-
|
|
90
|
+
|
|
49
91
|
if (isMounted) {
|
|
50
92
|
setFetchedData(data);
|
|
51
93
|
}
|
|
@@ -55,51 +97,78 @@ export const ObjectGallery = (props: any) => {
|
|
|
55
97
|
if (isMounted) setLoading(false);
|
|
56
98
|
}
|
|
57
99
|
};
|
|
58
|
-
|
|
100
|
+
|
|
59
101
|
if (schema.objectName && !boundData && !schema.data && !props.data) {
|
|
60
102
|
fetchData();
|
|
61
103
|
}
|
|
62
104
|
return () => { isMounted = false; };
|
|
63
|
-
|
|
105
|
+
}, [schema.objectName, dataSource, boundData, schema.data, schema.filter, props.data]);
|
|
64
106
|
|
|
65
|
-
const items = props.data || boundData || schema.data || fetchedData || [];
|
|
66
|
-
|
|
67
|
-
// Config
|
|
68
|
-
const imageField = schema.imageField || 'image';
|
|
69
|
-
const titleField = schema.titleField || 'name';
|
|
70
|
-
const subtitleField = schema.subtitleField;
|
|
107
|
+
const items: Record<string, unknown>[] = props.data || boundData || schema.data || fetchedData || [];
|
|
71
108
|
|
|
72
109
|
if (loading && !items.length) return <div className="p-4 text-sm text-muted-foreground">Loading Gallery...</div>;
|
|
73
110
|
if (!items.length) return <div className="p-4 text-sm text-muted-foreground">No items to display</div>;
|
|
74
111
|
|
|
75
112
|
return (
|
|
76
|
-
<div
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
</div>
|
|
93
|
-
)}
|
|
94
|
-
</div>
|
|
95
|
-
<div className="p-3 border-t">
|
|
96
|
-
<h3 className="font-medium truncate text-sm" title={item[titleField]}>{item[titleField] || 'Untitled'}</h3>
|
|
97
|
-
{subtitleField && (
|
|
98
|
-
<p className="text-xs text-muted-foreground truncate mt-1">{item[subtitleField]}</p>
|
|
113
|
+
<div
|
|
114
|
+
className={cn('grid gap-4 p-4', GRID_CLASSES[cardSize], schema.className)}
|
|
115
|
+
role="list"
|
|
116
|
+
>
|
|
117
|
+
{items.map((item, i) => {
|
|
118
|
+
const id = (item._id ?? item.id ?? i) as string | number;
|
|
119
|
+
const title = String(item[titleField] ?? 'Untitled');
|
|
120
|
+
const imageUrl = item[coverField] as string | undefined;
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Card
|
|
124
|
+
key={id}
|
|
125
|
+
role="listitem"
|
|
126
|
+
className={cn(
|
|
127
|
+
'group overflow-hidden transition-all hover:shadow-md',
|
|
128
|
+
props.onCardClick && 'cursor-pointer',
|
|
99
129
|
)}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
130
|
+
onClick={props.onCardClick ? () => props.onCardClick!(item) : undefined}
|
|
131
|
+
>
|
|
132
|
+
<div className={cn('w-full overflow-hidden bg-muted relative', ASPECT_CLASSES[cardSize])}>
|
|
133
|
+
{imageUrl ? (
|
|
134
|
+
<img
|
|
135
|
+
src={imageUrl}
|
|
136
|
+
alt={title}
|
|
137
|
+
className={cn(
|
|
138
|
+
'h-full w-full transition-transform group-hover:scale-105',
|
|
139
|
+
coverFit === 'cover' && 'object-cover',
|
|
140
|
+
coverFit === 'contain' && 'object-contain',
|
|
141
|
+
)}
|
|
142
|
+
/>
|
|
143
|
+
) : (
|
|
144
|
+
<div className="flex h-full w-full items-center justify-center bg-secondary/50 text-muted-foreground">
|
|
145
|
+
<span className="text-4xl font-light opacity-20">
|
|
146
|
+
{title[0]?.toUpperCase()}
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
<CardContent className="p-3 border-t">
|
|
152
|
+
<h3 className="font-medium truncate text-sm" title={title}>
|
|
153
|
+
{title}
|
|
154
|
+
</h3>
|
|
155
|
+
{visibleFields && visibleFields.length > 0 && (
|
|
156
|
+
<div className="mt-1 space-y-0.5">
|
|
157
|
+
{visibleFields.map((field) => {
|
|
158
|
+
const value = item[field];
|
|
159
|
+
if (value == null) return null;
|
|
160
|
+
return (
|
|
161
|
+
<p key={field} className="text-xs text-muted-foreground truncate">
|
|
162
|
+
{String(value)}
|
|
163
|
+
</p>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</CardContent>
|
|
169
|
+
</Card>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
103
172
|
</div>
|
|
104
173
|
);
|
|
105
174
|
};
|
|
@@ -107,5 +176,5 @@ export const ObjectGallery = (props: any) => {
|
|
|
107
176
|
ComponentRegistry.register('object-gallery', ObjectGallery, {
|
|
108
177
|
namespace: 'plugin-list',
|
|
109
178
|
label: 'Gallery View',
|
|
110
|
-
category: 'view'
|
|
179
|
+
category: 'view',
|
|
111
180
|
});
|
package/src/ViewSwitcher.tsx
CHANGED
|
@@ -32,6 +32,8 @@ export interface ViewSwitcherProps {
|
|
|
32
32
|
availableViews?: ViewType[];
|
|
33
33
|
onViewChange: (view: ViewType) => void;
|
|
34
34
|
className?: string;
|
|
35
|
+
/** Enable animated transitions between views (default: true) */
|
|
36
|
+
animated?: boolean;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const VIEW_ICONS: Record<ViewType, React.ReactNode> = {
|
|
@@ -59,16 +61,35 @@ export const ViewSwitcher: React.FC<ViewSwitcherProps> = ({
|
|
|
59
61
|
availableViews = ['grid', 'kanban'],
|
|
60
62
|
onViewChange,
|
|
61
63
|
className,
|
|
64
|
+
animated = true,
|
|
62
65
|
}) => {
|
|
66
|
+
const handleViewChange = React.useCallback(
|
|
67
|
+
(view: ViewType) => {
|
|
68
|
+
if (!animated || view === currentView) {
|
|
69
|
+
onViewChange(view);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof document !== 'undefined' && 'startViewTransition' in document) {
|
|
74
|
+
(document as Document & {
|
|
75
|
+
startViewTransition: (cb: () => void) => { finished: Promise<void> };
|
|
76
|
+
}).startViewTransition(() => onViewChange(view));
|
|
77
|
+
} else {
|
|
78
|
+
onViewChange(view);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[animated, currentView, onViewChange],
|
|
82
|
+
);
|
|
83
|
+
|
|
63
84
|
return (
|
|
64
|
-
<div className={cn("flex items-center gap-1 bg-transparent", className)}>
|
|
85
|
+
<div className={cn("flex items-center gap-1 bg-transparent oui-view-switcher", className)}>
|
|
65
86
|
{availableViews.map((view) => {
|
|
66
87
|
const isActive = currentView === view;
|
|
67
88
|
return (
|
|
68
89
|
<button
|
|
69
90
|
key={view}
|
|
70
91
|
type="button"
|
|
71
|
-
onClick={() =>
|
|
92
|
+
onClick={() => handleViewChange(view)}
|
|
72
93
|
aria-label={VIEW_LABELS[view]}
|
|
73
94
|
title={VIEW_LABELS[view]}
|
|
74
95
|
aria-pressed={isActive}
|
package/src/index.tsx
CHANGED
|
@@ -10,10 +10,10 @@ import { ComponentRegistry } from '@object-ui/core';
|
|
|
10
10
|
import { ListView } from './ListView';
|
|
11
11
|
import { ViewSwitcher } from './ViewSwitcher';
|
|
12
12
|
import { ObjectGallery } from './ObjectGallery';
|
|
13
|
-
import type { ListViewSchema } from '@object-ui/types';
|
|
14
13
|
|
|
15
14
|
export { ListView, ViewSwitcher, ObjectGallery };
|
|
16
15
|
export type { ListViewProps } from './ListView';
|
|
16
|
+
export type { ObjectGalleryProps } from './ObjectGallery';
|
|
17
17
|
export type { ViewSwitcherProps, ViewType } from './ViewSwitcher';
|
|
18
18
|
|
|
19
19
|
// Register ListView component
|