@object-ui/plugin-view 0.5.0 → 3.0.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 +7 -6
- package/CHANGELOG.md +38 -0
- package/README.md +58 -0
- package/dist/index.js +1168 -349
- package/dist/index.umd.cjs +2 -2
- package/dist/plugin-view/src/FilterUI.d.ts +16 -0
- package/dist/plugin-view/src/ObjectView.d.ts +85 -5
- package/dist/plugin-view/src/SortUI.d.ts +16 -0
- package/dist/plugin-view/src/ViewSwitcher.d.ts +16 -0
- package/dist/plugin-view/src/index.d.ts +7 -1
- package/package.json +9 -8
- package/src/FilterUI.tsx +317 -0
- package/src/ObjectView.tsx +668 -148
- package/src/SortUI.tsx +210 -0
- package/src/ViewSwitcher.tsx +311 -0
- package/src/__tests__/FilterUI.test.tsx +544 -0
- package/src/__tests__/ObjectView.test.tsx +375 -0
- package/src/__tests__/SortUI.test.tsx +380 -0
- package/src/__tests__/registration.test.tsx +32 -0
- package/src/__tests__/toolbar-consistency.test.tsx +755 -0
- package/src/index.tsx +147 -5
- package/vite.config.ts +1 -0
- package/vitest.config.ts +12 -0
- package/vitest.setup.ts +1 -0
package/src/SortUI.tsx
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
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
|
+
Select,
|
|
14
|
+
SelectContent,
|
|
15
|
+
SelectItem,
|
|
16
|
+
SelectTrigger,
|
|
17
|
+
SelectValue,
|
|
18
|
+
SortBuilder,
|
|
19
|
+
} from '@object-ui/components';
|
|
20
|
+
import { cva } from 'class-variance-authority';
|
|
21
|
+
import { ArrowDown, ArrowUp } from 'lucide-react';
|
|
22
|
+
import type { SortItem } from '@object-ui/components';
|
|
23
|
+
import type { SortUISchema } from '@object-ui/types';
|
|
24
|
+
|
|
25
|
+
export type SortUIProps = {
|
|
26
|
+
schema: SortUISchema;
|
|
27
|
+
className?: string;
|
|
28
|
+
onChange?: (sort: SortUISchema['sort']) => void;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type SortEntry = {
|
|
33
|
+
field: string;
|
|
34
|
+
direction: 'asc' | 'desc';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const sortContainerVariants = cva('', {
|
|
38
|
+
variants: {
|
|
39
|
+
variant: {
|
|
40
|
+
buttons: 'flex flex-wrap gap-2',
|
|
41
|
+
dropdown: 'flex flex-wrap items-center gap-3',
|
|
42
|
+
builder: 'space-y-3',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultVariants: {
|
|
46
|
+
variant: 'dropdown',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const toSortEntries = (sort?: SortUISchema['sort']): SortEntry[] => {
|
|
51
|
+
if (!sort) return [];
|
|
52
|
+
return sort.map(item => ({
|
|
53
|
+
field: item.field,
|
|
54
|
+
direction: item.direction,
|
|
55
|
+
}));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const toSortItems = (sort: SortEntry[]): SortItem[] => {
|
|
59
|
+
return sort.map(item => ({
|
|
60
|
+
id: `${item.field}-${item.direction}`,
|
|
61
|
+
field: item.field,
|
|
62
|
+
order: item.direction,
|
|
63
|
+
}));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const toSortEntriesFromItems = (items: SortItem[]): SortEntry[] => {
|
|
67
|
+
return items
|
|
68
|
+
.filter(item => item.field)
|
|
69
|
+
.map(item => ({
|
|
70
|
+
field: item.field,
|
|
71
|
+
direction: item.order,
|
|
72
|
+
}));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const SortUI: React.FC<SortUIProps> = ({
|
|
76
|
+
schema,
|
|
77
|
+
className,
|
|
78
|
+
onChange,
|
|
79
|
+
}) => {
|
|
80
|
+
const [sortState, setSortState] = React.useState<SortEntry[]>(() => toSortEntries(schema.sort));
|
|
81
|
+
const [builderItems, setBuilderItems] = React.useState<SortItem[]>(() => toSortItems(toSortEntries(schema.sort)));
|
|
82
|
+
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
const entries = toSortEntries(schema.sort);
|
|
85
|
+
setSortState(entries);
|
|
86
|
+
setBuilderItems(toSortItems(entries));
|
|
87
|
+
}, [schema.sort]);
|
|
88
|
+
|
|
89
|
+
const notifyChange = React.useCallback((nextSort: SortEntry[]) => {
|
|
90
|
+
setSortState(nextSort);
|
|
91
|
+
onChange?.(nextSort);
|
|
92
|
+
|
|
93
|
+
if (schema.onChange && typeof window !== 'undefined') {
|
|
94
|
+
window.dispatchEvent(
|
|
95
|
+
new CustomEvent(schema.onChange, {
|
|
96
|
+
detail: { sort: nextSort },
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}, [onChange, schema.onChange]);
|
|
101
|
+
|
|
102
|
+
const handleToggle = React.useCallback((field: string) => {
|
|
103
|
+
const existing = sortState.find(item => item.field === field);
|
|
104
|
+
const multiple = Boolean(schema.multiple);
|
|
105
|
+
|
|
106
|
+
if (!existing) {
|
|
107
|
+
const next = multiple
|
|
108
|
+
? [...sortState, { field, direction: 'asc' as const }]
|
|
109
|
+
: [{ field, direction: 'asc' as const }];
|
|
110
|
+
notifyChange(next);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (existing.direction === 'asc') {
|
|
115
|
+
const next = sortState.map(item =>
|
|
116
|
+
item.field === field ? { ...item, direction: 'desc' as const } : item
|
|
117
|
+
);
|
|
118
|
+
notifyChange(next);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const next = sortState.filter(item => item.field !== field);
|
|
123
|
+
notifyChange(next);
|
|
124
|
+
}, [notifyChange, schema.multiple, sortState]);
|
|
125
|
+
|
|
126
|
+
const variant = schema.variant || 'dropdown';
|
|
127
|
+
|
|
128
|
+
if (variant === 'buttons') {
|
|
129
|
+
return (
|
|
130
|
+
<div className={cn(sortContainerVariants({ variant: 'buttons' }), className)}>
|
|
131
|
+
{schema.fields.map(field => {
|
|
132
|
+
const current = sortState.find(item => item.field === field.field);
|
|
133
|
+
const Icon = current?.direction === 'asc' ? ArrowUp : ArrowDown;
|
|
134
|
+
return (
|
|
135
|
+
<Button
|
|
136
|
+
key={field.field}
|
|
137
|
+
type="button"
|
|
138
|
+
variant={current ? 'secondary' : 'outline'}
|
|
139
|
+
size="sm"
|
|
140
|
+
onClick={() => handleToggle(field.field)}
|
|
141
|
+
className="gap-2"
|
|
142
|
+
>
|
|
143
|
+
<span>{field.label || field.field}</span>
|
|
144
|
+
{current && <Icon className="h-3.5 w-3.5" />}
|
|
145
|
+
</Button>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (schema.multiple) {
|
|
153
|
+
return (
|
|
154
|
+
<div className={cn(sortContainerVariants({ variant: 'builder' }), className)}>
|
|
155
|
+
<SortBuilder
|
|
156
|
+
fields={schema.fields.map(field => ({ value: field.field, label: field.label || field.field }))}
|
|
157
|
+
value={builderItems}
|
|
158
|
+
onChange={(items) => {
|
|
159
|
+
setBuilderItems(items);
|
|
160
|
+
notifyChange(toSortEntriesFromItems(items));
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const singleSort = sortState[0];
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className={cn(sortContainerVariants({ variant: 'dropdown' }), className)}>
|
|
171
|
+
<Select
|
|
172
|
+
value={singleSort?.field || ''}
|
|
173
|
+
onValueChange={(value) => {
|
|
174
|
+
if (!value) {
|
|
175
|
+
notifyChange([]);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
notifyChange([{ field: value, direction: singleSort?.direction || 'asc' }]);
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<SelectTrigger className="w-56">
|
|
182
|
+
<SelectValue placeholder="Select field" />
|
|
183
|
+
</SelectTrigger>
|
|
184
|
+
<SelectContent>
|
|
185
|
+
{schema.fields.map(field => (
|
|
186
|
+
<SelectItem key={field.field} value={field.field}>
|
|
187
|
+
{field.label || field.field}
|
|
188
|
+
</SelectItem>
|
|
189
|
+
))}
|
|
190
|
+
</SelectContent>
|
|
191
|
+
</Select>
|
|
192
|
+
|
|
193
|
+
<Select
|
|
194
|
+
value={singleSort?.direction || 'asc'}
|
|
195
|
+
onValueChange={(value) => {
|
|
196
|
+
if (!singleSort?.field) return;
|
|
197
|
+
notifyChange([{ field: singleSort.field, direction: value as 'asc' | 'desc' }]);
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<SelectTrigger className="w-36">
|
|
201
|
+
<SelectValue />
|
|
202
|
+
</SelectTrigger>
|
|
203
|
+
<SelectContent>
|
|
204
|
+
<SelectItem value="asc">Ascending</SelectItem>
|
|
205
|
+
<SelectItem value="desc">Descending</SelectItem>
|
|
206
|
+
</SelectContent>
|
|
207
|
+
</Select>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
};
|
|
@@ -0,0 +1,311 @@
|
|
|
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
|
+
Grid,
|
|
30
|
+
LayoutGrid,
|
|
31
|
+
List,
|
|
32
|
+
Map,
|
|
33
|
+
icons,
|
|
34
|
+
type LucideIcon,
|
|
35
|
+
} from 'lucide-react';
|
|
36
|
+
|
|
37
|
+
type ViewSwitcherItem = ViewSwitcherSchema['views'][number];
|
|
38
|
+
|
|
39
|
+
export type ViewSwitcherProps = {
|
|
40
|
+
schema: ViewSwitcherSchema;
|
|
41
|
+
className?: string;
|
|
42
|
+
onViewChange?: (view: ViewType) => void;
|
|
43
|
+
[key: string]: any;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const DEFAULT_VIEW_LABELS: Record<ViewType, string> = {
|
|
47
|
+
list: 'List',
|
|
48
|
+
detail: 'Detail',
|
|
49
|
+
grid: 'Grid',
|
|
50
|
+
kanban: 'Kanban',
|
|
51
|
+
calendar: 'Calendar',
|
|
52
|
+
timeline: 'Timeline',
|
|
53
|
+
map: 'Map',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const DEFAULT_VIEW_ICONS: Record<ViewType, LucideIcon> = {
|
|
57
|
+
list: List,
|
|
58
|
+
detail: FileText,
|
|
59
|
+
grid: Grid,
|
|
60
|
+
kanban: LayoutGrid,
|
|
61
|
+
calendar: Calendar,
|
|
62
|
+
timeline: Activity,
|
|
63
|
+
map: Map,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const viewSwitcherLayout = cva('flex gap-4', {
|
|
67
|
+
variants: {
|
|
68
|
+
position: {
|
|
69
|
+
top: 'flex-col',
|
|
70
|
+
bottom: 'flex-col-reverse',
|
|
71
|
+
left: 'flex-row',
|
|
72
|
+
right: 'flex-row-reverse',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
defaultVariants: {
|
|
76
|
+
position: 'top',
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const viewSwitcherWidth = cva('w-full', {
|
|
81
|
+
variants: {
|
|
82
|
+
orientation: {
|
|
83
|
+
horizontal: 'w-full',
|
|
84
|
+
vertical: 'w-48',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
defaultVariants: {
|
|
88
|
+
orientation: 'horizontal',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const viewSwitcherList = cva('flex gap-2', {
|
|
93
|
+
variants: {
|
|
94
|
+
orientation: {
|
|
95
|
+
horizontal: 'flex-row flex-wrap',
|
|
96
|
+
vertical: 'flex-col',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
defaultVariants: {
|
|
100
|
+
orientation: 'horizontal',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const viewSwitcherTabsList = cva('', {
|
|
105
|
+
variants: {
|
|
106
|
+
orientation: {
|
|
107
|
+
horizontal: '',
|
|
108
|
+
vertical: 'flex h-auto flex-col items-stretch',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
defaultVariants: {
|
|
112
|
+
orientation: 'horizontal',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function toPascalCase(str: string): string {
|
|
117
|
+
return str
|
|
118
|
+
.split('-')
|
|
119
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
120
|
+
.join('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const iconNameMap: Record<string, string> = {
|
|
124
|
+
Home: 'House',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function resolveIcon(name?: string): LucideIcon | null {
|
|
128
|
+
if (!name) return null;
|
|
129
|
+
const iconName = toPascalCase(name);
|
|
130
|
+
const mapped = iconNameMap[iconName] || iconName;
|
|
131
|
+
return (icons as any)[mapped] || null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getViewLabel(view: ViewSwitcherItem): string {
|
|
135
|
+
if (view.label) return view.label;
|
|
136
|
+
return DEFAULT_VIEW_LABELS[view.type] || view.type;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getViewIcon(view: ViewSwitcherItem): LucideIcon | null {
|
|
140
|
+
if (view.icon) {
|
|
141
|
+
return resolveIcon(view.icon);
|
|
142
|
+
}
|
|
143
|
+
return DEFAULT_VIEW_ICONS[view.type] || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getInitialView(schema: ViewSwitcherSchema): ViewType | undefined {
|
|
147
|
+
if (schema.activeView) return schema.activeView;
|
|
148
|
+
if (schema.defaultView) return schema.defaultView;
|
|
149
|
+
return schema.views?.[0]?.type;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const ViewSwitcher: React.FC<ViewSwitcherProps> = ({
|
|
153
|
+
schema,
|
|
154
|
+
className,
|
|
155
|
+
onViewChange,
|
|
156
|
+
...props
|
|
157
|
+
}) => {
|
|
158
|
+
const storageKey = React.useMemo(() => {
|
|
159
|
+
if (schema.storageKey) return schema.storageKey;
|
|
160
|
+
const idPart = schema.id ? `-${schema.id}` : '';
|
|
161
|
+
return `view-switcher${idPart}`;
|
|
162
|
+
}, [schema.id, schema.storageKey]);
|
|
163
|
+
|
|
164
|
+
const [activeView, setActiveView] = React.useState<ViewType | undefined>(() => getInitialView(schema));
|
|
165
|
+
|
|
166
|
+
React.useEffect(() => {
|
|
167
|
+
if (schema.activeView) {
|
|
168
|
+
setActiveView(schema.activeView);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!schema.persistPreference) return;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const saved = localStorage.getItem(storageKey);
|
|
176
|
+
if (saved) {
|
|
177
|
+
const view = schema.views.find(v => v.type === saved)?.type as ViewType | undefined;
|
|
178
|
+
if (view) {
|
|
179
|
+
setActiveView(view);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Ignore storage errors
|
|
184
|
+
}
|
|
185
|
+
}, [schema.activeView, schema.persistPreference, schema.views, storageKey]);
|
|
186
|
+
|
|
187
|
+
React.useEffect(() => {
|
|
188
|
+
if (!schema.persistPreference || !activeView || schema.activeView) return;
|
|
189
|
+
try {
|
|
190
|
+
localStorage.setItem(storageKey, activeView);
|
|
191
|
+
} catch {
|
|
192
|
+
// Ignore storage errors
|
|
193
|
+
}
|
|
194
|
+
}, [activeView, schema.activeView, schema.persistPreference, storageKey]);
|
|
195
|
+
|
|
196
|
+
const notifyChange = React.useCallback((nextView: ViewType) => {
|
|
197
|
+
onViewChange?.(nextView);
|
|
198
|
+
|
|
199
|
+
if (schema.onViewChange && typeof window !== 'undefined') {
|
|
200
|
+
window.dispatchEvent(
|
|
201
|
+
new CustomEvent(schema.onViewChange, {
|
|
202
|
+
detail: { view: nextView },
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}, [onViewChange, schema.onViewChange]);
|
|
207
|
+
|
|
208
|
+
const handleViewChange = React.useCallback((nextView: ViewType) => {
|
|
209
|
+
setActiveView(nextView);
|
|
210
|
+
notifyChange(nextView);
|
|
211
|
+
}, [notifyChange]);
|
|
212
|
+
|
|
213
|
+
const currentView = activeView || schema.views?.[0]?.type;
|
|
214
|
+
const currentViewValue = currentView || '';
|
|
215
|
+
const currentViewConfig = schema.views.find(v => v.type === currentView) || schema.views?.[0];
|
|
216
|
+
|
|
217
|
+
const variant = schema.variant || 'tabs';
|
|
218
|
+
const position = schema.position || 'top';
|
|
219
|
+
const isVertical = position === 'left' || position === 'right';
|
|
220
|
+
const orientation = isVertical ? 'vertical' : 'horizontal';
|
|
221
|
+
|
|
222
|
+
const switcher = (
|
|
223
|
+
<div className={cn(viewSwitcherWidth({ orientation }))}>
|
|
224
|
+
{variant === 'dropdown' && (
|
|
225
|
+
<Select value={currentViewValue} onValueChange={(value) => handleViewChange(value as ViewType)}>
|
|
226
|
+
<SelectTrigger className={cn('w-full', isVertical ? 'h-10' : 'h-9')}>
|
|
227
|
+
<SelectValue placeholder="Select view" />
|
|
228
|
+
</SelectTrigger>
|
|
229
|
+
<SelectContent>
|
|
230
|
+
{schema.views.map((view, index) => (
|
|
231
|
+
<SelectItem key={`${view.type}-${index}`} value={view.type}>
|
|
232
|
+
{getViewLabel(view)}
|
|
233
|
+
</SelectItem>
|
|
234
|
+
))}
|
|
235
|
+
</SelectContent>
|
|
236
|
+
</Select>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{variant === 'buttons' && (
|
|
240
|
+
<div className={cn(viewSwitcherList({ orientation }))}>
|
|
241
|
+
{schema.views.map((view, index) => {
|
|
242
|
+
const isActive = view.type === currentView;
|
|
243
|
+
const Icon = getViewIcon(view);
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<Button
|
|
247
|
+
key={`${view.type}-${index}`}
|
|
248
|
+
type="button"
|
|
249
|
+
size="sm"
|
|
250
|
+
variant={isActive ? 'secondary' : 'ghost'}
|
|
251
|
+
className={cn('justify-start gap-2', isVertical ? 'w-full' : '')}
|
|
252
|
+
onClick={() => handleViewChange(view.type)}
|
|
253
|
+
>
|
|
254
|
+
{Icon ? <Icon className="h-4 w-4" /> : null}
|
|
255
|
+
<span>{getViewLabel(view)}</span>
|
|
256
|
+
</Button>
|
|
257
|
+
);
|
|
258
|
+
})}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
{variant === 'tabs' && (
|
|
263
|
+
<Tabs value={currentViewValue} onValueChange={(value) => handleViewChange(value as ViewType)}>
|
|
264
|
+
<TabsList className={cn(viewSwitcherTabsList({ orientation }))}>
|
|
265
|
+
{schema.views.map((view, index) => {
|
|
266
|
+
const Icon = getViewIcon(view);
|
|
267
|
+
return (
|
|
268
|
+
<TabsTrigger
|
|
269
|
+
key={`${view.type}-${index}`}
|
|
270
|
+
value={view.type}
|
|
271
|
+
className={cn('gap-2', isVertical ? 'justify-start' : '')}
|
|
272
|
+
>
|
|
273
|
+
{Icon ? <Icon className="h-4 w-4" /> : null}
|
|
274
|
+
<span>{getViewLabel(view)}</span>
|
|
275
|
+
</TabsTrigger>
|
|
276
|
+
);
|
|
277
|
+
})}
|
|
278
|
+
</TabsList>
|
|
279
|
+
</Tabs>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const viewContent = (() => {
|
|
285
|
+
if (!currentViewConfig?.schema) return null;
|
|
286
|
+
|
|
287
|
+
if (Array.isArray(currentViewConfig.schema)) {
|
|
288
|
+
return (
|
|
289
|
+
<div className="space-y-4">
|
|
290
|
+
{currentViewConfig.schema.map((node, index) => (
|
|
291
|
+
<SchemaRenderer key={`${currentViewConfig.type}-${index}`} schema={node} {...props} />
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return <SchemaRenderer schema={currentViewConfig.schema} {...props} />;
|
|
298
|
+
})();
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div
|
|
302
|
+
className={cn(
|
|
303
|
+
viewSwitcherLayout({ position }),
|
|
304
|
+
className
|
|
305
|
+
)}
|
|
306
|
+
>
|
|
307
|
+
<div className={cn('shrink-0', isVertical ? 'flex flex-col' : 'flex')}>{switcher}</div>
|
|
308
|
+
<div className="flex-1 min-w-0">{viewContent}</div>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
};
|