@object-ui/plugin-list 3.1.5 → 3.3.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/CHANGELOG.md +34 -0
- package/README.md +21 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30492 -38346
- package/dist/index.umd.cjs +30 -38
- package/dist/{src → packages/plugin-list/src}/ListView.d.ts +17 -1
- package/dist/packages/plugin-list/src/ListView.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/ListView.stories.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/ObjectGallery.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/UserFilters.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/ViewSwitcher.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/components/TabBar.d.ts.map +1 -0
- package/dist/{src → packages/plugin-list/src}/index.d.ts +1 -1
- package/dist/packages/plugin-list/src/index.d.ts.map +1 -0
- package/dist/plugin-list.css +1 -2
- package/package.json +35 -13
- package/.turbo/turbo-build.log +0 -24
- package/dist/src/ListView.d.ts.map +0 -1
- package/dist/src/ListView.stories.d.ts.map +0 -1
- package/dist/src/ObjectGallery.d.ts.map +0 -1
- package/dist/src/UserFilters.d.ts.map +0 -1
- package/dist/src/ViewSwitcher.d.ts.map +0 -1
- package/dist/src/components/TabBar.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/src/ListView.stories.tsx +0 -64
- package/src/ListView.tsx +0 -1688
- package/src/ObjectGallery.tsx +0 -308
- package/src/UserFilters.tsx +0 -453
- package/src/ViewSwitcher.tsx +0 -113
- package/src/__tests__/ConditionalFormatting.test.ts +0 -285
- package/src/__tests__/DataFetch.test.tsx +0 -253
- package/src/__tests__/Export.test.tsx +0 -175
- package/src/__tests__/FilterNormalization.test.ts +0 -162
- package/src/__tests__/GalleryGrouping.test.tsx +0 -237
- package/src/__tests__/GalleryTimelineSpecConfig.test.tsx +0 -203
- package/src/__tests__/ListView.test.tsx +0 -2151
- package/src/__tests__/ListViewGroupingPropagation.test.tsx +0 -250
- package/src/__tests__/ListViewPersistence.test.tsx +0 -129
- package/src/__tests__/ObjectGallery.test.tsx +0 -208
- package/src/__tests__/TabBar.test.tsx +0 -199
- package/src/__tests__/UserFilters.test.tsx +0 -486
- package/src/components/TabBar.tsx +0 -120
- package/src/index.tsx +0 -78
- package/tsconfig.json +0 -18
- package/vite.config.ts +0 -56
- package/vitest.config.ts +0 -12
- package/vitest.setup.ts +0 -1
- /package/dist/{src → packages/plugin-list/src}/ListView.stories.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/ObjectGallery.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/UserFilters.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/ViewSwitcher.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/components/TabBar.d.ts +0 -0
package/src/ObjectGallery.tsx
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
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
|
-
*/
|
|
8
|
-
|
|
9
|
-
import React, { useState, useEffect, useCallback, useMemo, useContext } from 'react';
|
|
10
|
-
import { useDataScope, SchemaRendererContext, useNavigationOverlay } from '@object-ui/react';
|
|
11
|
-
import { ComponentRegistry, buildExpandFields } from '@object-ui/core';
|
|
12
|
-
import { cn, Card, CardContent, NavigationOverlay } from '@object-ui/components';
|
|
13
|
-
import type { GalleryConfig, ViewNavigationConfig, GroupingConfig } from '@object-ui/types';
|
|
14
|
-
import { ChevronRight, ChevronDown } from 'lucide-react';
|
|
15
|
-
|
|
16
|
-
export interface ObjectGalleryProps {
|
|
17
|
-
schema: {
|
|
18
|
-
objectName?: string;
|
|
19
|
-
bind?: string;
|
|
20
|
-
filter?: unknown;
|
|
21
|
-
data?: Record<string, unknown>[];
|
|
22
|
-
className?: string;
|
|
23
|
-
gallery?: GalleryConfig;
|
|
24
|
-
/** Navigation config for item click behavior */
|
|
25
|
-
navigation?: ViewNavigationConfig;
|
|
26
|
-
/** Grouping configuration for sectioned display */
|
|
27
|
-
grouping?: GroupingConfig;
|
|
28
|
-
/** @deprecated Use gallery.coverField instead */
|
|
29
|
-
imageField?: string;
|
|
30
|
-
/** @deprecated Use gallery.titleField instead */
|
|
31
|
-
titleField?: string;
|
|
32
|
-
subtitleField?: string;
|
|
33
|
-
};
|
|
34
|
-
data?: Record<string, unknown>[];
|
|
35
|
-
dataSource?: { find: (name: string, query: unknown) => Promise<unknown> };
|
|
36
|
-
onCardClick?: (record: Record<string, unknown>) => void;
|
|
37
|
-
/** Callback when a row/item is clicked (overrides NavigationConfig) */
|
|
38
|
-
onRowClick?: (record: Record<string, unknown>) => void;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const GRID_CLASSES: Record<NonNullable<GalleryConfig['cardSize']>, string> = {
|
|
42
|
-
small: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6',
|
|
43
|
-
medium: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
|
|
44
|
-
large: 'grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const ASPECT_CLASSES: Record<NonNullable<GalleryConfig['cardSize']>, string> = {
|
|
48
|
-
small: 'aspect-square',
|
|
49
|
-
medium: 'aspect-[4/3]',
|
|
50
|
-
large: 'aspect-[16/10]',
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const ObjectGallery: React.FC<ObjectGalleryProps> = (props) => {
|
|
54
|
-
const { schema } = props;
|
|
55
|
-
const context = useContext(SchemaRendererContext);
|
|
56
|
-
const dataSource = props.dataSource || context?.dataSource;
|
|
57
|
-
const boundData = useDataScope(schema.bind);
|
|
58
|
-
|
|
59
|
-
const [fetchedData, setFetchedData] = useState<Record<string, unknown>[]>([]);
|
|
60
|
-
const [loading, setLoading] = useState(false);
|
|
61
|
-
const [objectDef, setObjectDef] = useState<any>(null);
|
|
62
|
-
|
|
63
|
-
// --- NavigationConfig support ---
|
|
64
|
-
const navigation = useNavigationOverlay({
|
|
65
|
-
navigation: schema.navigation,
|
|
66
|
-
objectName: schema.objectName,
|
|
67
|
-
onRowClick: props.onRowClick ?? props.onCardClick,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Resolve GalleryConfig with backwards-compatible fallbacks
|
|
71
|
-
const gallery = schema.gallery;
|
|
72
|
-
const coverField = gallery?.coverField ?? schema.imageField ?? 'image';
|
|
73
|
-
const coverFit = gallery?.coverFit ?? 'cover';
|
|
74
|
-
const cardSize = gallery?.cardSize ?? 'medium';
|
|
75
|
-
const titleField = gallery?.titleField ?? schema.titleField ?? 'name';
|
|
76
|
-
const visibleFields = gallery?.visibleFields;
|
|
77
|
-
|
|
78
|
-
// Fetch object definition for metadata
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
let isMounted = true;
|
|
81
|
-
const fetchMeta = async () => {
|
|
82
|
-
if (!dataSource || typeof dataSource.getObjectSchema !== 'function' || !schema.objectName) return;
|
|
83
|
-
try {
|
|
84
|
-
const def = await dataSource.getObjectSchema(schema.objectName);
|
|
85
|
-
if (isMounted) setObjectDef(def);
|
|
86
|
-
} catch (e) {
|
|
87
|
-
console.warn('Failed to fetch object def for ObjectGallery', e);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
fetchMeta();
|
|
91
|
-
return () => { isMounted = false; };
|
|
92
|
-
}, [schema.objectName, dataSource]);
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
let isMounted = true;
|
|
96
|
-
|
|
97
|
-
if (props.data && Array.isArray(props.data)) {
|
|
98
|
-
setFetchedData(props.data);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const fetchData = async () => {
|
|
103
|
-
if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
|
|
104
|
-
if (isMounted) setLoading(true);
|
|
105
|
-
try {
|
|
106
|
-
// Auto-inject $expand for lookup/master_detail fields
|
|
107
|
-
const expand = buildExpandFields(objectDef?.fields);
|
|
108
|
-
const results = await dataSource.find(schema.objectName, {
|
|
109
|
-
$filter: schema.filter,
|
|
110
|
-
...(expand.length > 0 ? { $expand: expand } : {}),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
let data: Record<string, unknown>[] = [];
|
|
114
|
-
if (Array.isArray(results)) {
|
|
115
|
-
data = results;
|
|
116
|
-
} else if (results && typeof results === 'object') {
|
|
117
|
-
const r = results as Record<string, unknown>;
|
|
118
|
-
if (Array.isArray(r.records)) {
|
|
119
|
-
data = r.records as Record<string, unknown>[];
|
|
120
|
-
} else if (Array.isArray(r.data)) {
|
|
121
|
-
data = r.data as Record<string, unknown>[];
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (isMounted) {
|
|
126
|
-
setFetchedData(data);
|
|
127
|
-
}
|
|
128
|
-
} catch (e) {
|
|
129
|
-
console.error('[ObjectGallery] Fetch error:', e);
|
|
130
|
-
} finally {
|
|
131
|
-
if (isMounted) setLoading(false);
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
if (schema.objectName && !boundData && !schema.data && !props.data) {
|
|
136
|
-
fetchData();
|
|
137
|
-
}
|
|
138
|
-
return () => { isMounted = false; };
|
|
139
|
-
}, [schema.objectName, dataSource, boundData, schema.data, schema.filter, props.data, objectDef]);
|
|
140
|
-
|
|
141
|
-
const items: Record<string, unknown>[] = props.data || boundData || schema.data || fetchedData || [];
|
|
142
|
-
|
|
143
|
-
// --- Grouping support ---
|
|
144
|
-
const groupingFields = schema.grouping?.fields;
|
|
145
|
-
const isGrouped = !!(groupingFields && groupingFields.length > 0);
|
|
146
|
-
|
|
147
|
-
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
|
|
148
|
-
|
|
149
|
-
// Initialize collapsed state from grouping config
|
|
150
|
-
const defaultCollapsed = useMemo(() => {
|
|
151
|
-
if (!groupingFields) return false;
|
|
152
|
-
return groupingFields.some((f) => f.collapsed);
|
|
153
|
-
}, [groupingFields]);
|
|
154
|
-
|
|
155
|
-
const toggleGroup = useCallback((key: string) => {
|
|
156
|
-
setCollapsedGroups((prev) => ({
|
|
157
|
-
...prev,
|
|
158
|
-
[key]: prev[key] !== undefined ? !prev[key] : !defaultCollapsed,
|
|
159
|
-
}));
|
|
160
|
-
}, [defaultCollapsed]);
|
|
161
|
-
|
|
162
|
-
const groupedItems = useMemo(() => {
|
|
163
|
-
if (!isGrouped || !groupingFields) return [];
|
|
164
|
-
const map = new Map<string, { label: string; items: Record<string, unknown>[] }>();
|
|
165
|
-
const keyOrder: string[] = [];
|
|
166
|
-
for (const item of items) {
|
|
167
|
-
const key = groupingFields.map((f) => String(item[f.field] ?? '')).join(' / ');
|
|
168
|
-
if (!map.has(key)) {
|
|
169
|
-
const label = groupingFields
|
|
170
|
-
.map((f) => {
|
|
171
|
-
const val = item[f.field];
|
|
172
|
-
return val !== undefined && val !== null && val !== '' ? String(val) : '(empty)';
|
|
173
|
-
})
|
|
174
|
-
.join(' / ');
|
|
175
|
-
map.set(key, { label, items: [] });
|
|
176
|
-
keyOrder.push(key);
|
|
177
|
-
}
|
|
178
|
-
map.get(key)!.items.push(item);
|
|
179
|
-
}
|
|
180
|
-
const primaryOrder = groupingFields[0]?.order ?? 'asc';
|
|
181
|
-
keyOrder.sort((a, b) => {
|
|
182
|
-
const cmp = a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
|
|
183
|
-
return primaryOrder === 'desc' ? -cmp : cmp;
|
|
184
|
-
});
|
|
185
|
-
return keyOrder.map((key) => {
|
|
186
|
-
const entry = map.get(key)!;
|
|
187
|
-
const collapsed = key in collapsedGroups ? collapsedGroups[key] : defaultCollapsed;
|
|
188
|
-
return { key, label: entry.label, items: entry.items, collapsed };
|
|
189
|
-
});
|
|
190
|
-
}, [items, groupingFields, isGrouped, collapsedGroups, defaultCollapsed]);
|
|
191
|
-
|
|
192
|
-
if (loading && !items.length) return <div className="p-4 text-sm text-muted-foreground">Loading Gallery...</div>;
|
|
193
|
-
if (!items.length) return <div className="p-4 text-sm text-muted-foreground">No items to display</div>;
|
|
194
|
-
|
|
195
|
-
const renderCard = (item: Record<string, unknown>, i: number) => {
|
|
196
|
-
const id = (item.id ?? item._id ?? i) as string | number;
|
|
197
|
-
const title = String(item[titleField] ?? 'Untitled');
|
|
198
|
-
const imageUrl = item[coverField] as string | undefined;
|
|
199
|
-
|
|
200
|
-
return (
|
|
201
|
-
<Card
|
|
202
|
-
key={id}
|
|
203
|
-
role="listitem"
|
|
204
|
-
className={cn(
|
|
205
|
-
'group overflow-hidden transition-all hover:shadow-md',
|
|
206
|
-
(props.onCardClick || props.onRowClick || schema.navigation) && 'cursor-pointer',
|
|
207
|
-
)}
|
|
208
|
-
onClick={() => navigation.handleClick(item)}
|
|
209
|
-
>
|
|
210
|
-
<div className={cn('w-full overflow-hidden bg-muted relative', ASPECT_CLASSES[cardSize])}>
|
|
211
|
-
{imageUrl ? (
|
|
212
|
-
<img
|
|
213
|
-
src={imageUrl}
|
|
214
|
-
alt={title}
|
|
215
|
-
className={cn(
|
|
216
|
-
'h-full w-full transition-transform group-hover:scale-105',
|
|
217
|
-
coverFit === 'cover' && 'object-cover',
|
|
218
|
-
coverFit === 'contain' && 'object-contain',
|
|
219
|
-
)}
|
|
220
|
-
/>
|
|
221
|
-
) : (
|
|
222
|
-
<div className="flex h-full w-full items-center justify-center bg-secondary/50 text-muted-foreground">
|
|
223
|
-
<span className="text-4xl font-light opacity-20">
|
|
224
|
-
{title[0]?.toUpperCase()}
|
|
225
|
-
</span>
|
|
226
|
-
</div>
|
|
227
|
-
)}
|
|
228
|
-
</div>
|
|
229
|
-
<CardContent className="p-3 border-t">
|
|
230
|
-
<h3 className="font-medium truncate text-sm" title={title}>
|
|
231
|
-
{title}
|
|
232
|
-
</h3>
|
|
233
|
-
{visibleFields && visibleFields.length > 0 && (
|
|
234
|
-
<div className="mt-1 space-y-0.5">
|
|
235
|
-
{visibleFields.map((field) => {
|
|
236
|
-
const value = item[field];
|
|
237
|
-
if (value == null) return null;
|
|
238
|
-
return (
|
|
239
|
-
<p key={field} className="text-xs text-muted-foreground truncate">
|
|
240
|
-
{String(value)}
|
|
241
|
-
</p>
|
|
242
|
-
);
|
|
243
|
-
})}
|
|
244
|
-
</div>
|
|
245
|
-
)}
|
|
246
|
-
</CardContent>
|
|
247
|
-
</Card>
|
|
248
|
-
);
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const renderGrid = (gridItems: Record<string, unknown>[]) => (
|
|
252
|
-
<div
|
|
253
|
-
className={cn('grid gap-4 p-4', GRID_CLASSES[cardSize], schema.className)}
|
|
254
|
-
role="list"
|
|
255
|
-
>
|
|
256
|
-
{gridItems.map((item, i) => renderCard(item, i))}
|
|
257
|
-
</div>
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
return (
|
|
261
|
-
<>
|
|
262
|
-
{isGrouped ? (
|
|
263
|
-
<div className="space-y-2">
|
|
264
|
-
{groupedItems.map((group) => (
|
|
265
|
-
<div key={group.key} className="border rounded-md">
|
|
266
|
-
<button
|
|
267
|
-
type="button"
|
|
268
|
-
className="flex w-full items-center gap-2 px-3 py-2 text-sm font-medium text-left bg-muted/50 hover:bg-muted transition-colors"
|
|
269
|
-
onClick={() => toggleGroup(group.key)}
|
|
270
|
-
>
|
|
271
|
-
{group.collapsed
|
|
272
|
-
? <ChevronRight className="h-4 w-4 shrink-0" />
|
|
273
|
-
: <ChevronDown className="h-4 w-4 shrink-0" />}
|
|
274
|
-
<span>{group.label}</span>
|
|
275
|
-
<span className="ml-auto text-xs text-muted-foreground">{group.items.length}</span>
|
|
276
|
-
</button>
|
|
277
|
-
{!group.collapsed && renderGrid(group.items)}
|
|
278
|
-
</div>
|
|
279
|
-
))}
|
|
280
|
-
</div>
|
|
281
|
-
) : (
|
|
282
|
-
renderGrid(items)
|
|
283
|
-
)}
|
|
284
|
-
{navigation.isOverlay && (
|
|
285
|
-
<NavigationOverlay {...navigation} title="Gallery Item">
|
|
286
|
-
{(record) => (
|
|
287
|
-
<div className="space-y-3">
|
|
288
|
-
{Object.entries(record).map(([key, value]) => (
|
|
289
|
-
<div key={key} className="flex flex-col">
|
|
290
|
-
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
291
|
-
{key.replace(/_/g, ' ')}
|
|
292
|
-
</span>
|
|
293
|
-
<span className="text-sm">{String(value ?? '—')}</span>
|
|
294
|
-
</div>
|
|
295
|
-
))}
|
|
296
|
-
</div>
|
|
297
|
-
)}
|
|
298
|
-
</NavigationOverlay>
|
|
299
|
-
)}
|
|
300
|
-
</>
|
|
301
|
-
);
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
ComponentRegistry.register('object-gallery', ObjectGallery, {
|
|
305
|
-
namespace: 'plugin-list',
|
|
306
|
-
label: 'Gallery View',
|
|
307
|
-
category: 'view',
|
|
308
|
-
});
|