@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +21 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +30492 -38346
  5. package/dist/index.umd.cjs +30 -38
  6. package/dist/{src → packages/plugin-list/src}/ListView.d.ts +17 -1
  7. package/dist/packages/plugin-list/src/ListView.d.ts.map +1 -0
  8. package/dist/packages/plugin-list/src/ListView.stories.d.ts.map +1 -0
  9. package/dist/packages/plugin-list/src/ObjectGallery.d.ts.map +1 -0
  10. package/dist/packages/plugin-list/src/UserFilters.d.ts.map +1 -0
  11. package/dist/packages/plugin-list/src/ViewSwitcher.d.ts.map +1 -0
  12. package/dist/packages/plugin-list/src/components/TabBar.d.ts.map +1 -0
  13. package/dist/{src → packages/plugin-list/src}/index.d.ts +1 -1
  14. package/dist/packages/plugin-list/src/index.d.ts.map +1 -0
  15. package/dist/plugin-list.css +1 -2
  16. package/package.json +35 -13
  17. package/.turbo/turbo-build.log +0 -24
  18. package/dist/src/ListView.d.ts.map +0 -1
  19. package/dist/src/ListView.stories.d.ts.map +0 -1
  20. package/dist/src/ObjectGallery.d.ts.map +0 -1
  21. package/dist/src/UserFilters.d.ts.map +0 -1
  22. package/dist/src/ViewSwitcher.d.ts.map +0 -1
  23. package/dist/src/components/TabBar.d.ts.map +0 -1
  24. package/dist/src/index.d.ts.map +0 -1
  25. package/src/ListView.stories.tsx +0 -64
  26. package/src/ListView.tsx +0 -1688
  27. package/src/ObjectGallery.tsx +0 -308
  28. package/src/UserFilters.tsx +0 -453
  29. package/src/ViewSwitcher.tsx +0 -113
  30. package/src/__tests__/ConditionalFormatting.test.ts +0 -285
  31. package/src/__tests__/DataFetch.test.tsx +0 -253
  32. package/src/__tests__/Export.test.tsx +0 -175
  33. package/src/__tests__/FilterNormalization.test.ts +0 -162
  34. package/src/__tests__/GalleryGrouping.test.tsx +0 -237
  35. package/src/__tests__/GalleryTimelineSpecConfig.test.tsx +0 -203
  36. package/src/__tests__/ListView.test.tsx +0 -2151
  37. package/src/__tests__/ListViewGroupingPropagation.test.tsx +0 -250
  38. package/src/__tests__/ListViewPersistence.test.tsx +0 -129
  39. package/src/__tests__/ObjectGallery.test.tsx +0 -208
  40. package/src/__tests__/TabBar.test.tsx +0 -199
  41. package/src/__tests__/UserFilters.test.tsx +0 -486
  42. package/src/components/TabBar.tsx +0 -120
  43. package/src/index.tsx +0 -78
  44. package/tsconfig.json +0 -18
  45. package/vite.config.ts +0 -56
  46. package/vitest.config.ts +0 -12
  47. package/vitest.setup.ts +0 -1
  48. /package/dist/{src → packages/plugin-list/src}/ListView.stories.d.ts +0 -0
  49. /package/dist/{src → packages/plugin-list/src}/ObjectGallery.d.ts +0 -0
  50. /package/dist/{src → packages/plugin-list/src}/UserFilters.d.ts +0 -0
  51. /package/dist/{src → packages/plugin-list/src}/ViewSwitcher.d.ts +0 -0
  52. /package/dist/{src → packages/plugin-list/src}/components/TabBar.d.ts +0 -0
@@ -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
- });