@object-ui/plugin-view 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 (34) 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 +2716 -3140
  5. package/dist/index.umd.cjs +1 -10
  6. package/dist/{plugin-view → packages/plugin-view}/src/ObjectView.d.ts +2 -0
  7. package/dist/packages/plugin-view/src/config/view-config-schema.d.ts +20 -0
  8. package/dist/packages/plugin-view/src/config/view-config-utils.d.ts +58 -0
  9. package/dist/{plugin-view → packages/plugin-view}/src/index.d.ts +4 -0
  10. package/package.json +42 -10
  11. package/.turbo/turbo-build.log +0 -34
  12. package/src/FilterUI.tsx +0 -350
  13. package/src/ObjectView.tsx +0 -1109
  14. package/src/SharedViewLink.tsx +0 -199
  15. package/src/SortUI.tsx +0 -210
  16. package/src/ViewSwitcher.tsx +0 -379
  17. package/src/ViewTabBar.tsx +0 -656
  18. package/src/__tests__/FilterUI.test.tsx +0 -641
  19. package/src/__tests__/ObjectView.test.tsx +0 -705
  20. package/src/__tests__/SharedViewLinkPassword.test.tsx +0 -172
  21. package/src/__tests__/SortUI.test.tsx +0 -380
  22. package/src/__tests__/ViewTabBar.test.tsx +0 -710
  23. package/src/__tests__/config-sync-integration.test.tsx +0 -588
  24. package/src/__tests__/toolbar-consistency.test.tsx +0 -755
  25. package/src/index.tsx +0 -197
  26. package/tsconfig.json +0 -8
  27. package/vite.config.ts +0 -44
  28. package/vitest.config.ts +0 -12
  29. package/vitest.setup.ts +0 -1
  30. /package/dist/{plugin-view → packages/plugin-view}/src/FilterUI.d.ts +0 -0
  31. /package/dist/{plugin-view → packages/plugin-view}/src/SharedViewLink.d.ts +0 -0
  32. /package/dist/{plugin-view → packages/plugin-view}/src/SortUI.d.ts +0 -0
  33. /package/dist/{plugin-view → packages/plugin-view}/src/ViewSwitcher.d.ts +0 -0
  34. /package/dist/{plugin-view → packages/plugin-view}/src/ViewTabBar.d.ts +0 -0
@@ -1,379 +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 * as React from 'react';
10
- import {
11
- cn,
12
- Button,
13
- Tabs,
14
- TabsList,
15
- TabsTrigger,
16
- Select,
17
- SelectContent,
18
- SelectItem,
19
- SelectTrigger,
20
- SelectValue,
21
- } from '@object-ui/components';
22
- import { cva } from 'class-variance-authority';
23
- import { SchemaRenderer } from '@object-ui/react';
24
- import type { ViewSwitcherSchema, ViewType } from '@object-ui/types';
25
- import {
26
- Activity,
27
- Calendar,
28
- FileText,
29
- GanttChartSquare,
30
- Grid,
31
- Images,
32
- LayoutGrid,
33
- List,
34
- Map,
35
- Plus,
36
- Share2,
37
- Settings,
38
- Copy,
39
- Trash2,
40
- icons,
41
- type LucideIcon,
42
- } from 'lucide-react';
43
-
44
- type ViewSwitcherItem = ViewSwitcherSchema['views'][number];
45
-
46
- export type ViewSwitcherProps = {
47
- schema: ViewSwitcherSchema;
48
- className?: string;
49
- onViewChange?: (view: ViewType) => void;
50
- onCreateView?: () => void;
51
- onViewAction?: (action: string, view: ViewType) => void;
52
- createViewLabel?: string;
53
- [key: string]: any;
54
- };
55
-
56
- const DEFAULT_VIEW_LABELS: Record<ViewType, string> = {
57
- list: 'List',
58
- detail: 'Detail',
59
- grid: 'Grid',
60
- kanban: 'Kanban',
61
- calendar: 'Calendar',
62
- timeline: 'Timeline',
63
- map: 'Map',
64
- gallery: 'Gallery',
65
- gantt: 'Gantt',
66
- };
67
-
68
- const DEFAULT_VIEW_ICONS: Record<ViewType, LucideIcon> = {
69
- list: List,
70
- detail: FileText,
71
- grid: Grid,
72
- kanban: LayoutGrid,
73
- calendar: Calendar,
74
- timeline: Activity,
75
- map: Map,
76
- gallery: Images,
77
- gantt: GanttChartSquare,
78
- };
79
-
80
- const viewSwitcherLayout = cva('flex gap-4', {
81
- variants: {
82
- position: {
83
- top: 'flex-col',
84
- bottom: 'flex-col-reverse',
85
- left: 'flex-row',
86
- right: 'flex-row-reverse',
87
- },
88
- },
89
- defaultVariants: {
90
- position: 'top',
91
- },
92
- });
93
-
94
- const viewSwitcherWidth = cva('w-full', {
95
- variants: {
96
- orientation: {
97
- horizontal: 'w-full',
98
- vertical: 'w-48',
99
- },
100
- },
101
- defaultVariants: {
102
- orientation: 'horizontal',
103
- },
104
- });
105
-
106
- const viewSwitcherList = cva('flex gap-2', {
107
- variants: {
108
- orientation: {
109
- horizontal: 'flex-row flex-wrap',
110
- vertical: 'flex-col',
111
- },
112
- },
113
- defaultVariants: {
114
- orientation: 'horizontal',
115
- },
116
- });
117
-
118
- const viewSwitcherTabsList = cva('', {
119
- variants: {
120
- orientation: {
121
- horizontal: '',
122
- vertical: 'flex h-auto flex-col items-stretch',
123
- },
124
- },
125
- defaultVariants: {
126
- orientation: 'horizontal',
127
- },
128
- });
129
-
130
- function toPascalCase(str: string): string {
131
- return str
132
- .split('-')
133
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
134
- .join('');
135
- }
136
-
137
- const iconNameMap: Record<string, string> = {
138
- Home: 'House',
139
- };
140
-
141
- function resolveIcon(name?: string): LucideIcon | null {
142
- if (!name) return null;
143
- const iconName = toPascalCase(name);
144
- const mapped = iconNameMap[iconName] || iconName;
145
- return (icons as any)[mapped] || null;
146
- }
147
-
148
- function getViewLabel(view: ViewSwitcherItem): string {
149
- if (view.label) return view.label;
150
- return DEFAULT_VIEW_LABELS[view.type] || view.type;
151
- }
152
-
153
- function getViewIcon(view: ViewSwitcherItem): LucideIcon | null {
154
- if (view.icon) {
155
- return resolveIcon(view.icon);
156
- }
157
- return DEFAULT_VIEW_ICONS[view.type] || null;
158
- }
159
-
160
- function getInitialView(schema: ViewSwitcherSchema): ViewType | undefined {
161
- if (schema.activeView) return schema.activeView;
162
- if (schema.defaultView) return schema.defaultView;
163
- return schema.views?.[0]?.type;
164
- }
165
-
166
- const DEFAULT_VIEW_ACTION_ICONS: Record<string, LucideIcon> = {
167
- share: Share2,
168
- settings: Settings,
169
- duplicate: Copy,
170
- delete: Trash2,
171
- };
172
-
173
- const DEFAULT_VIEW_ACTION_LABELS: Record<string, string> = {
174
- share: 'Share',
175
- settings: 'Settings',
176
- duplicate: 'Duplicate',
177
- delete: 'Delete',
178
- };
179
-
180
- export const ViewSwitcher: React.FC<ViewSwitcherProps> = ({
181
- schema,
182
- className,
183
- onViewChange,
184
- onCreateView,
185
- onViewAction,
186
- createViewLabel = 'Create view',
187
- ...props
188
- }) => {
189
- const storageKey = React.useMemo(() => {
190
- if (schema.storageKey) return schema.storageKey;
191
- const idPart = schema.id ? `-${schema.id}` : '';
192
- return `view-switcher${idPart}`;
193
- }, [schema.id, schema.storageKey]);
194
-
195
- const [activeView, setActiveView] = React.useState<ViewType | undefined>(() => getInitialView(schema));
196
-
197
- React.useEffect(() => {
198
- if (schema.activeView) {
199
- setActiveView(schema.activeView);
200
- return;
201
- }
202
-
203
- if (!schema.persistPreference) return;
204
-
205
- try {
206
- const saved = localStorage.getItem(storageKey);
207
- if (saved) {
208
- const view = schema.views.find(v => v.type === saved)?.type as ViewType | undefined;
209
- if (view) {
210
- setActiveView(view);
211
- }
212
- }
213
- } catch {
214
- // Ignore storage errors
215
- }
216
- }, [schema.activeView, schema.persistPreference, schema.views, storageKey]);
217
-
218
- React.useEffect(() => {
219
- if (!schema.persistPreference || !activeView || schema.activeView) return;
220
- try {
221
- localStorage.setItem(storageKey, activeView);
222
- } catch {
223
- // Ignore storage errors
224
- }
225
- }, [activeView, schema.activeView, schema.persistPreference, storageKey]);
226
-
227
- const notifyChange = React.useCallback((nextView: ViewType) => {
228
- onViewChange?.(nextView);
229
-
230
- if (schema.onViewChange && typeof window !== 'undefined') {
231
- window.dispatchEvent(
232
- new CustomEvent(schema.onViewChange, {
233
- detail: { view: nextView },
234
- })
235
- );
236
- }
237
- }, [onViewChange, schema.onViewChange]);
238
-
239
- const handleViewChange = React.useCallback((nextView: ViewType) => {
240
- setActiveView(nextView);
241
- notifyChange(nextView);
242
- }, [notifyChange]);
243
-
244
- const currentView = activeView || schema.views?.[0]?.type;
245
- const currentViewValue = currentView || '';
246
- const currentViewConfig = schema.views.find(v => v.type === currentView) || schema.views?.[0];
247
-
248
- const variant = schema.variant || 'tabs';
249
- const position = schema.position || 'top';
250
- const isVertical = position === 'left' || position === 'right';
251
- const orientation = isVertical ? 'vertical' : 'horizontal';
252
-
253
- const viewActionButtons = schema.viewActions && schema.viewActions.length > 0 ? (
254
- <div className="flex items-center gap-1">
255
- {schema.viewActions.map((action, idx) => {
256
- const ActionIcon = action.icon
257
- ? resolveIcon(action.icon) || DEFAULT_VIEW_ACTION_ICONS[action.type]
258
- : DEFAULT_VIEW_ACTION_ICONS[action.type];
259
- return (
260
- <Button
261
- key={`action-${action.type}-${idx}`}
262
- type="button"
263
- variant="ghost"
264
- size="icon-sm"
265
- onClick={() => onViewAction?.(action.type, currentView!)}
266
- title={DEFAULT_VIEW_ACTION_LABELS[action.type] || action.type}
267
- >
268
- {ActionIcon ? <ActionIcon className="h-3.5 w-3.5" /> : null}
269
- </Button>
270
- );
271
- })}
272
- </div>
273
- ) : null;
274
-
275
- const createViewButton = schema.allowCreateView ? (
276
- <Button
277
- type="button"
278
- variant="ghost"
279
- size="icon-sm"
280
- onClick={() => onCreateView?.()}
281
- title={createViewLabel}
282
- >
283
- <Plus className="h-3.5 w-3.5" />
284
- </Button>
285
- ) : null;
286
-
287
- const switcher = (
288
- <div className={cn(viewSwitcherWidth({ orientation }), 'flex items-center gap-1')}>
289
- {variant === 'dropdown' && (
290
- <Select value={currentViewValue} onValueChange={(value) => handleViewChange(value as ViewType)}>
291
- <SelectTrigger className={cn('w-full', isVertical ? 'h-10' : 'h-9')}>
292
- <SelectValue placeholder="Select view" />
293
- </SelectTrigger>
294
- <SelectContent>
295
- {schema.views.map((view, index) => (
296
- <SelectItem key={`${view.type}-${index}`} value={view.type}>
297
- {getViewLabel(view)}
298
- </SelectItem>
299
- ))}
300
- </SelectContent>
301
- </Select>
302
- )}
303
-
304
- {variant === 'buttons' && (
305
- <div className={cn(viewSwitcherList({ orientation }))}>
306
- {schema.views.map((view, index) => {
307
- const isActive = view.type === currentView;
308
- const Icon = getViewIcon(view);
309
-
310
- return (
311
- <Button
312
- key={`${view.type}-${index}`}
313
- type="button"
314
- size="sm"
315
- variant={isActive ? 'secondary' : 'ghost'}
316
- className={cn('justify-start gap-2', isVertical ? 'w-full' : '')}
317
- onClick={() => handleViewChange(view.type)}
318
- >
319
- {Icon ? <Icon className="h-4 w-4" /> : null}
320
- <span>{getViewLabel(view)}</span>
321
- </Button>
322
- );
323
- })}
324
- </div>
325
- )}
326
-
327
- {variant === 'tabs' && (
328
- <Tabs value={currentViewValue} onValueChange={(value) => handleViewChange(value as ViewType)}>
329
- <TabsList className={cn(viewSwitcherTabsList({ orientation }))}>
330
- {schema.views.map((view, index) => {
331
- const Icon = getViewIcon(view);
332
- return (
333
- <TabsTrigger
334
- key={`${view.type}-${index}`}
335
- value={view.type}
336
- className={cn('gap-2', isVertical ? 'justify-start' : '')}
337
- >
338
- {Icon ? <Icon className="h-4 w-4" /> : null}
339
- <span>{getViewLabel(view)}</span>
340
- </TabsTrigger>
341
- );
342
- })}
343
- </TabsList>
344
- </Tabs>
345
- )}
346
-
347
- {viewActionButtons}
348
- {createViewButton}
349
- </div>
350
- );
351
-
352
- const viewContent = (() => {
353
- if (!currentViewConfig?.schema) return null;
354
-
355
- if (Array.isArray(currentViewConfig.schema)) {
356
- return (
357
- <div className="space-y-4">
358
- {currentViewConfig.schema.map((node, index) => (
359
- <SchemaRenderer key={`${currentViewConfig.type}-${index}`} schema={node} {...props} />
360
- ))}
361
- </div>
362
- );
363
- }
364
-
365
- return <SchemaRenderer schema={currentViewConfig.schema} {...props} />;
366
- })();
367
-
368
- return (
369
- <div
370
- className={cn(
371
- viewSwitcherLayout({ position }),
372
- className
373
- )}
374
- >
375
- <div className={cn('shrink-0', isVertical ? 'flex flex-col' : 'flex')}>{switcher}</div>
376
- <div className="flex-1 min-w-0">{viewContent}</div>
377
- </div>
378
- );
379
- };