@object-ui/plugin-list 3.0.2 → 3.1.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 +10 -0
- package/dist/index.js +26941 -24204
- package/dist/index.umd.cjs +36 -34
- package/dist/plugin-list.css +1 -1
- package/dist/src/ListView.d.ts +20 -0
- package/dist/src/ListView.d.ts.map +1 -1
- package/dist/src/ObjectGallery.d.ts +7 -1
- package/dist/src/ObjectGallery.d.ts.map +1 -1
- package/dist/src/UserFilters.d.ts +23 -0
- package/dist/src/UserFilters.d.ts.map +1 -0
- package/dist/src/components/TabBar.d.ts +32 -0
- package/dist/src/components/TabBar.d.ts.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/ListView.tsx +1200 -159
- package/src/ObjectGallery.tsx +191 -63
- package/src/UserFilters.tsx +461 -0
- package/src/__tests__/ConditionalFormatting.test.ts +285 -0
- package/src/__tests__/DataFetch.test.tsx +224 -0
- package/src/__tests__/Export.test.tsx +175 -0
- package/src/__tests__/FilterNormalization.test.ts +162 -0
- package/src/__tests__/GalleryGrouping.test.tsx +237 -0
- package/src/__tests__/GalleryTimelineSpecConfig.test.tsx +203 -0
- package/src/__tests__/ListView.test.tsx +1884 -19
- package/src/__tests__/ListViewGroupingPropagation.test.tsx +250 -0
- package/src/__tests__/ObjectGallery.test.tsx +208 -0
- package/src/__tests__/TabBar.test.tsx +199 -0
- package/src/__tests__/UserFilters.test.tsx +494 -0
- package/src/components/TabBar.tsx +120 -0
- package/src/index.tsx +13 -4
package/src/ObjectGallery.tsx
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useState, useEffect } from 'react';
|
|
10
|
-
import { useDataScope,
|
|
11
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
12
|
-
import { cn, Card, CardContent } from '@object-ui/components';
|
|
13
|
-
import type { GalleryConfig } from '@object-ui/types';
|
|
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';
|
|
14
15
|
|
|
15
16
|
export interface ObjectGalleryProps {
|
|
16
17
|
schema: {
|
|
@@ -20,6 +21,10 @@ export interface ObjectGalleryProps {
|
|
|
20
21
|
data?: Record<string, unknown>[];
|
|
21
22
|
className?: string;
|
|
22
23
|
gallery?: GalleryConfig;
|
|
24
|
+
/** Navigation config for item click behavior */
|
|
25
|
+
navigation?: ViewNavigationConfig;
|
|
26
|
+
/** Grouping configuration for sectioned display */
|
|
27
|
+
grouping?: GroupingConfig;
|
|
23
28
|
/** @deprecated Use gallery.coverField instead */
|
|
24
29
|
imageField?: string;
|
|
25
30
|
/** @deprecated Use gallery.titleField instead */
|
|
@@ -29,6 +34,8 @@ export interface ObjectGalleryProps {
|
|
|
29
34
|
data?: Record<string, unknown>[];
|
|
30
35
|
dataSource?: { find: (name: string, query: unknown) => Promise<unknown> };
|
|
31
36
|
onCardClick?: (record: Record<string, unknown>) => void;
|
|
37
|
+
/** Callback when a row/item is clicked (overrides NavigationConfig) */
|
|
38
|
+
onRowClick?: (record: Record<string, unknown>) => void;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
const GRID_CLASSES: Record<NonNullable<GalleryConfig['cardSize']>, string> = {
|
|
@@ -45,12 +52,20 @@ const ASPECT_CLASSES: Record<NonNullable<GalleryConfig['cardSize']>, string> = {
|
|
|
45
52
|
|
|
46
53
|
export const ObjectGallery: React.FC<ObjectGalleryProps> = (props) => {
|
|
47
54
|
const { schema } = props;
|
|
48
|
-
const context =
|
|
49
|
-
const dataSource = props.dataSource || context
|
|
55
|
+
const context = useContext(SchemaRendererContext);
|
|
56
|
+
const dataSource = props.dataSource || context?.dataSource;
|
|
50
57
|
const boundData = useDataScope(schema.bind);
|
|
51
58
|
|
|
52
59
|
const [fetchedData, setFetchedData] = useState<Record<string, unknown>[]>([]);
|
|
53
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
|
+
});
|
|
54
69
|
|
|
55
70
|
// Resolve GalleryConfig with backwards-compatible fallbacks
|
|
56
71
|
const gallery = schema.gallery;
|
|
@@ -60,6 +75,22 @@ export const ObjectGallery: React.FC<ObjectGalleryProps> = (props) => {
|
|
|
60
75
|
const titleField = gallery?.titleField ?? schema.titleField ?? 'name';
|
|
61
76
|
const visibleFields = gallery?.visibleFields;
|
|
62
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
|
+
|
|
63
94
|
useEffect(() => {
|
|
64
95
|
let isMounted = true;
|
|
65
96
|
|
|
@@ -69,11 +100,14 @@ export const ObjectGallery: React.FC<ObjectGalleryProps> = (props) => {
|
|
|
69
100
|
}
|
|
70
101
|
|
|
71
102
|
const fetchData = async () => {
|
|
72
|
-
if (!dataSource || !schema.objectName) return;
|
|
103
|
+
if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
|
|
73
104
|
if (isMounted) setLoading(true);
|
|
74
105
|
try {
|
|
106
|
+
// Auto-inject $expand for lookup/master_detail fields
|
|
107
|
+
const expand = buildExpandFields(objectDef?.fields);
|
|
75
108
|
const results = await dataSource.find(schema.objectName, {
|
|
76
109
|
$filter: schema.filter,
|
|
110
|
+
...(expand.length > 0 ? { $expand: expand } : {}),
|
|
77
111
|
});
|
|
78
112
|
|
|
79
113
|
let data: Record<string, unknown>[] = [];
|
|
@@ -102,74 +136,168 @@ export const ObjectGallery: React.FC<ObjectGalleryProps> = (props) => {
|
|
|
102
136
|
fetchData();
|
|
103
137
|
}
|
|
104
138
|
return () => { isMounted = false; };
|
|
105
|
-
}, [schema.objectName, dataSource, boundData, schema.data, schema.filter, props.data]);
|
|
139
|
+
}, [schema.objectName, dataSource, boundData, schema.data, schema.filter, props.data, objectDef]);
|
|
106
140
|
|
|
107
141
|
const items: Record<string, unknown>[] = props.data || boundData || schema.data || fetchedData || [];
|
|
108
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
|
+
|
|
109
192
|
if (loading && !items.length) return <div className="p-4 text-sm text-muted-foreground">Loading Gallery...</div>;
|
|
110
193
|
if (!items.length) return <div className="p-4 text-sm text-muted-foreground">No items to display</div>;
|
|
111
194
|
|
|
112
|
-
|
|
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>[]) => (
|
|
113
252
|
<div
|
|
114
253
|
className={cn('grid gap-4 p-4', GRID_CLASSES[cardSize], schema.className)}
|
|
115
254
|
role="list"
|
|
116
255
|
>
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
className=
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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, ' ')}
|
|
147
292
|
</span>
|
|
293
|
+
<span className="text-sm">{String(value ?? '—')}</span>
|
|
148
294
|
</div>
|
|
149
|
-
)}
|
|
295
|
+
))}
|
|
150
296
|
</div>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
})}
|
|
172
|
-
</div>
|
|
297
|
+
)}
|
|
298
|
+
</NavigationOverlay>
|
|
299
|
+
)}
|
|
300
|
+
</>
|
|
173
301
|
);
|
|
174
302
|
};
|
|
175
303
|
|