@ifc-lite/viewer 1.6.0 → 1.7.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/CHANGELOG.md +78 -0
- package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
- package/dist/assets/index-yTqs8kgX.css +1 -0
- package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
- package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -15
- package/src/components/viewer/BCFPanel.tsx +7 -789
- package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
- package/src/components/viewer/HierarchyPanel.tsx +110 -842
- package/src/components/viewer/IDSExportDialog.tsx +281 -0
- package/src/components/viewer/IDSPanel.tsx +126 -17
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
- package/src/components/viewer/LensPanel.tsx +603 -0
- package/src/components/viewer/MainToolbar.tsx +188 -21
- package/src/components/viewer/PropertiesPanel.tsx +171 -663
- package/src/components/viewer/PropertyEditor.tsx +866 -77
- package/src/components/viewer/Section2DPanel.tsx +76 -2648
- package/src/components/viewer/ToolOverlays.tsx +3 -1097
- package/src/components/viewer/ViewerLayout.tsx +132 -45
- package/src/components/viewer/Viewport.tsx +237 -1659
- package/src/components/viewer/ViewportContainer.tsx +11 -3
- package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
- package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
- package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
- package/src/components/viewer/hierarchy/types.ts +54 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
- package/src/components/viewer/lists/ListBuilder.tsx +486 -0
- package/src/components/viewer/lists/ListPanel.tsx +540 -0
- package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
- package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
- package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
- package/src/components/viewer/properties/DocumentCard.tsx +89 -0
- package/src/components/viewer/properties/MaterialCard.tsx +201 -0
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
- package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
- package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
- package/src/components/viewer/properties/encodingUtils.ts +29 -0
- package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
- package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
- package/src/components/viewer/tools/SectionPanel.tsx +183 -0
- package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
- package/src/components/viewer/tools/formatDistance.ts +18 -0
- package/src/components/viewer/tools/sectionConstants.ts +14 -0
- package/src/components/viewer/useAnimationLoop.ts +166 -0
- package/src/components/viewer/useGeometryStreaming.ts +398 -0
- package/src/components/viewer/useKeyboardControls.ts +221 -0
- package/src/components/viewer/useMouseControls.ts +1009 -0
- package/src/components/viewer/useRenderUpdates.ts +165 -0
- package/src/components/viewer/useTouchControls.ts +245 -0
- package/src/hooks/ids/idsColorSystem.ts +125 -0
- package/src/hooks/ids/idsDataAccessor.ts +237 -0
- package/src/hooks/ids/idsExportService.ts +444 -0
- package/src/hooks/useBCF.ts +7 -0
- package/src/hooks/useDrawingExport.ts +627 -0
- package/src/hooks/useDrawingGeneration.ts +627 -0
- package/src/hooks/useFloorplanView.ts +108 -0
- package/src/hooks/useIDS.ts +270 -463
- package/src/hooks/useIfc.ts +26 -1628
- package/src/hooks/useIfcFederation.ts +803 -0
- package/src/hooks/useIfcLoader.ts +508 -0
- package/src/hooks/useIfcServer.ts +465 -0
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useLens.ts +129 -0
- package/src/hooks/useMeasure2D.ts +365 -0
- package/src/hooks/useViewControls.ts +218 -0
- package/src/lib/ifc4-pset-definitions.test.ts +161 -0
- package/src/lib/ifc4-pset-definitions.ts +621 -0
- package/src/lib/ifc4-qto-definitions.ts +315 -0
- package/src/lib/lens/adapter.ts +138 -0
- package/src/lib/lens/index.ts +5 -0
- package/src/lib/lists/adapter.ts +69 -0
- package/src/lib/lists/index.ts +28 -0
- package/src/lib/lists/persistence.ts +64 -0
- package/src/services/fs-cache.ts +1 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/index.ts +38 -2
- package/src/store/slices/cameraSlice.ts +14 -1
- package/src/store/slices/dataSlice.ts +14 -1
- package/src/store/slices/lensSlice.ts +184 -0
- package/src/store/slices/listSlice.ts +74 -0
- package/src/store/slices/pinboardSlice.ts +114 -0
- package/src/store/types.ts +5 -0
- package/src/utils/ifcConfig.ts +16 -3
- package/src/utils/serverDataModel.ts +64 -101
- package/src/vite-env.d.ts +3 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-v3mcCUPN.css +0 -1
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ListPanel - Main container for the Lists feature
|
|
7
|
+
*
|
|
8
|
+
* Shows either:
|
|
9
|
+
* - List builder (when creating/editing a list)
|
|
10
|
+
* - List results table (when a list has been executed)
|
|
11
|
+
* - List library (saved lists + presets)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { useCallback, useState, useMemo } from 'react';
|
|
15
|
+
import {
|
|
16
|
+
X,
|
|
17
|
+
Plus,
|
|
18
|
+
Play,
|
|
19
|
+
FileSpreadsheet,
|
|
20
|
+
Trash2,
|
|
21
|
+
Download,
|
|
22
|
+
Upload,
|
|
23
|
+
Loader2,
|
|
24
|
+
Table2,
|
|
25
|
+
Pencil,
|
|
26
|
+
Copy,
|
|
27
|
+
Settings2,
|
|
28
|
+
} from 'lucide-react';
|
|
29
|
+
import { Button } from '@/components/ui/button';
|
|
30
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
31
|
+
import { Separator } from '@/components/ui/separator';
|
|
32
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
33
|
+
import { useViewerStore } from '@/store';
|
|
34
|
+
import { useIfc } from '@/hooks/useIfc';
|
|
35
|
+
import {
|
|
36
|
+
executeList,
|
|
37
|
+
listResultToCSV,
|
|
38
|
+
LIST_PRESETS,
|
|
39
|
+
importListDefinition,
|
|
40
|
+
exportListDefinition,
|
|
41
|
+
createListDataProvider,
|
|
42
|
+
} from '@/lib/lists';
|
|
43
|
+
import type { ListDefinition, ListResult, ListDataProvider } from '@/lib/lists';
|
|
44
|
+
import { ListBuilder } from './ListBuilder';
|
|
45
|
+
import { ListResultsTable } from './ListResultsTable';
|
|
46
|
+
|
|
47
|
+
interface ListPanelProps {
|
|
48
|
+
onClose?: () => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type PanelView = 'library' | 'builder' | 'results';
|
|
52
|
+
|
|
53
|
+
export function ListPanel({ onClose }: ListPanelProps) {
|
|
54
|
+
const { ifcDataStore, models } = useIfc();
|
|
55
|
+
const [view, setView] = useState<PanelView>('library');
|
|
56
|
+
const [editingList, setEditingList] = useState<ListDefinition | null>(null);
|
|
57
|
+
|
|
58
|
+
const listDefinitions = useViewerStore((s) => s.listDefinitions);
|
|
59
|
+
const activeListId = useViewerStore((s) => s.activeListId);
|
|
60
|
+
const listResult = useViewerStore((s) => s.listResult);
|
|
61
|
+
const listExecuting = useViewerStore((s) => s.listExecuting);
|
|
62
|
+
const addListDefinition = useViewerStore((s) => s.addListDefinition);
|
|
63
|
+
const updateListDefinition = useViewerStore((s) => s.updateListDefinition);
|
|
64
|
+
const deleteListDefinition = useViewerStore((s) => s.deleteListDefinition);
|
|
65
|
+
const setActiveListId = useViewerStore((s) => s.setActiveListId);
|
|
66
|
+
const setListResult = useViewerStore((s) => s.setListResult);
|
|
67
|
+
const setListExecuting = useViewerStore((s) => s.setListExecuting);
|
|
68
|
+
|
|
69
|
+
const importInputRef = React.useRef<HTMLInputElement>(null);
|
|
70
|
+
|
|
71
|
+
// Collect all available data providers for multi-model support
|
|
72
|
+
const allProviders = useMemo(() => {
|
|
73
|
+
const providers: ListDataProvider[] = [];
|
|
74
|
+
if (models.size > 0) {
|
|
75
|
+
for (const [, model] of models) {
|
|
76
|
+
providers.push(createListDataProvider(model.ifcDataStore));
|
|
77
|
+
}
|
|
78
|
+
} else if (ifcDataStore) {
|
|
79
|
+
providers.push(createListDataProvider(ifcDataStore));
|
|
80
|
+
}
|
|
81
|
+
return providers;
|
|
82
|
+
}, [models, ifcDataStore]);
|
|
83
|
+
|
|
84
|
+
const hasData = allProviders.length > 0;
|
|
85
|
+
|
|
86
|
+
// Build a stable map of modelId → provider index for execution
|
|
87
|
+
const modelProviderPairs = useMemo(() => {
|
|
88
|
+
const pairs: Array<{ modelId: string; provider: ListDataProvider }> = [];
|
|
89
|
+
if (models.size > 0) {
|
|
90
|
+
let i = 0;
|
|
91
|
+
for (const [modelId] of models) {
|
|
92
|
+
pairs.push({ modelId, provider: allProviders[i++] });
|
|
93
|
+
}
|
|
94
|
+
} else if (allProviders.length > 0) {
|
|
95
|
+
pairs.push({ modelId: 'default', provider: allProviders[0] });
|
|
96
|
+
}
|
|
97
|
+
return pairs;
|
|
98
|
+
}, [models, allProviders]);
|
|
99
|
+
|
|
100
|
+
const handleExecuteList = useCallback((definition: ListDefinition) => {
|
|
101
|
+
if (!hasData) return;
|
|
102
|
+
|
|
103
|
+
setListExecuting(true);
|
|
104
|
+
setActiveListId(definition.id);
|
|
105
|
+
setEditingList(definition);
|
|
106
|
+
|
|
107
|
+
// Use requestAnimationFrame to avoid blocking UI during execution
|
|
108
|
+
requestAnimationFrame(() => {
|
|
109
|
+
try {
|
|
110
|
+
const resultParts: ListResult[] = [];
|
|
111
|
+
for (const { modelId, provider } of modelProviderPairs) {
|
|
112
|
+
resultParts.push(executeList(definition, provider, modelId));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const allRows = resultParts.flatMap(r => r.rows);
|
|
116
|
+
const totalTime = resultParts.reduce((sum, r) => sum + r.executionTime, 0);
|
|
117
|
+
|
|
118
|
+
setListResult({
|
|
119
|
+
columns: definition.columns,
|
|
120
|
+
rows: allRows,
|
|
121
|
+
totalCount: allRows.length,
|
|
122
|
+
executionTime: totalTime,
|
|
123
|
+
});
|
|
124
|
+
setView('results');
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.error('[Lists] Execution failed:', err);
|
|
127
|
+
} finally {
|
|
128
|
+
setListExecuting(false);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}, [hasData, modelProviderPairs, setActiveListId, setListResult, setListExecuting]);
|
|
132
|
+
|
|
133
|
+
const handleCreateNew = useCallback(() => {
|
|
134
|
+
setEditingList(null);
|
|
135
|
+
setView('builder');
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
const handleEdit = useCallback((definition: ListDefinition) => {
|
|
139
|
+
setEditingList(definition);
|
|
140
|
+
setView('builder');
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
const handleDuplicate = useCallback((definition: ListDefinition) => {
|
|
144
|
+
const clone: ListDefinition = {
|
|
145
|
+
...definition,
|
|
146
|
+
id: crypto.randomUUID(),
|
|
147
|
+
name: `${definition.name} (Copy)`,
|
|
148
|
+
createdAt: Date.now(),
|
|
149
|
+
updatedAt: Date.now(),
|
|
150
|
+
};
|
|
151
|
+
addListDefinition(clone);
|
|
152
|
+
}, [addListDefinition]);
|
|
153
|
+
|
|
154
|
+
const handleSaveList = useCallback((definition: ListDefinition) => {
|
|
155
|
+
// Check if updating existing or adding new
|
|
156
|
+
const exists = listDefinitions.some(d => d.id === definition.id);
|
|
157
|
+
if (exists) {
|
|
158
|
+
updateListDefinition(definition.id, definition);
|
|
159
|
+
} else {
|
|
160
|
+
addListDefinition(definition);
|
|
161
|
+
}
|
|
162
|
+
setView('library');
|
|
163
|
+
}, [listDefinitions, addListDefinition, updateListDefinition]);
|
|
164
|
+
|
|
165
|
+
const handleDelete = useCallback((id: string) => {
|
|
166
|
+
deleteListDefinition(id);
|
|
167
|
+
}, [deleteListDefinition]);
|
|
168
|
+
|
|
169
|
+
const handleEditFromResults = useCallback(() => {
|
|
170
|
+
if (editingList) {
|
|
171
|
+
setView('builder');
|
|
172
|
+
}
|
|
173
|
+
}, [editingList]);
|
|
174
|
+
|
|
175
|
+
const handleExportCSV = useCallback(() => {
|
|
176
|
+
if (!listResult) return;
|
|
177
|
+
const csv = listResultToCSV(listResult);
|
|
178
|
+
const blob = new Blob([csv], { type: 'text/csv' });
|
|
179
|
+
const url = URL.createObjectURL(blob);
|
|
180
|
+
const a = document.createElement('a');
|
|
181
|
+
a.href = url;
|
|
182
|
+
a.download = 'list-export.csv';
|
|
183
|
+
a.click();
|
|
184
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
185
|
+
}, [listResult]);
|
|
186
|
+
|
|
187
|
+
const handleImport = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
188
|
+
const file = e.target.files?.[0];
|
|
189
|
+
if (!file) return;
|
|
190
|
+
try {
|
|
191
|
+
const definition = await importListDefinition(file);
|
|
192
|
+
addListDefinition(definition);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error('[Lists] Import failed:', err);
|
|
195
|
+
}
|
|
196
|
+
e.target.value = '';
|
|
197
|
+
}, [addListDefinition]);
|
|
198
|
+
|
|
199
|
+
const handleExportDefinition = useCallback((definition: ListDefinition) => {
|
|
200
|
+
exportListDefinition(definition);
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div className="flex flex-col h-full">
|
|
205
|
+
{/* Header */}
|
|
206
|
+
<div className="flex items-center justify-between px-3 py-2 border-b">
|
|
207
|
+
<div className="flex items-center gap-2">
|
|
208
|
+
<Table2 className="h-4 w-4" />
|
|
209
|
+
<span className="font-medium text-sm">
|
|
210
|
+
{view === 'library' && 'Lists'}
|
|
211
|
+
{view === 'builder' && (editingList ? 'Edit List' : 'New List')}
|
|
212
|
+
{view === 'results' && 'Results'}
|
|
213
|
+
</span>
|
|
214
|
+
{view === 'results' && listResult && (
|
|
215
|
+
<span className="text-xs text-muted-foreground">
|
|
216
|
+
({listResult.totalCount} rows, {listResult.executionTime.toFixed(0)}ms)
|
|
217
|
+
</span>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
<div className="flex items-center gap-1">
|
|
221
|
+
{view === 'results' && (
|
|
222
|
+
<>
|
|
223
|
+
<Tooltip>
|
|
224
|
+
<TooltipTrigger asChild>
|
|
225
|
+
<Button variant="ghost" size="icon-sm" onClick={handleEditFromResults}>
|
|
226
|
+
<Settings2 className="h-3.5 w-3.5" />
|
|
227
|
+
</Button>
|
|
228
|
+
</TooltipTrigger>
|
|
229
|
+
<TooltipContent>Edit Configuration</TooltipContent>
|
|
230
|
+
</Tooltip>
|
|
231
|
+
<Tooltip>
|
|
232
|
+
<TooltipTrigger asChild>
|
|
233
|
+
<Button variant="ghost" size="icon-sm" onClick={handleExportCSV}>
|
|
234
|
+
<Download className="h-3.5 w-3.5" />
|
|
235
|
+
</Button>
|
|
236
|
+
</TooltipTrigger>
|
|
237
|
+
<TooltipContent>Export CSV</TooltipContent>
|
|
238
|
+
</Tooltip>
|
|
239
|
+
<Tooltip>
|
|
240
|
+
<TooltipTrigger asChild>
|
|
241
|
+
<Button variant="ghost" size="icon-sm" onClick={() => setView('library')}>
|
|
242
|
+
<Table2 className="h-3.5 w-3.5" />
|
|
243
|
+
</Button>
|
|
244
|
+
</TooltipTrigger>
|
|
245
|
+
<TooltipContent>Back to Lists</TooltipContent>
|
|
246
|
+
</Tooltip>
|
|
247
|
+
</>
|
|
248
|
+
)}
|
|
249
|
+
{view === 'builder' && (
|
|
250
|
+
<Button variant="ghost" size="sm" onClick={() => setView('library')} className="text-xs h-7">
|
|
251
|
+
Cancel
|
|
252
|
+
</Button>
|
|
253
|
+
)}
|
|
254
|
+
{onClose && (
|
|
255
|
+
<Button variant="ghost" size="icon-sm" onClick={onClose}>
|
|
256
|
+
<X className="h-3.5 w-3.5" />
|
|
257
|
+
</Button>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{/* Content */}
|
|
263
|
+
{view === 'library' && (
|
|
264
|
+
<ListLibrary
|
|
265
|
+
definitions={listDefinitions}
|
|
266
|
+
activeListId={activeListId}
|
|
267
|
+
executing={listExecuting}
|
|
268
|
+
hasData={hasData}
|
|
269
|
+
onExecute={handleExecuteList}
|
|
270
|
+
onCreateNew={handleCreateNew}
|
|
271
|
+
onEdit={handleEdit}
|
|
272
|
+
onDuplicate={handleDuplicate}
|
|
273
|
+
onDelete={handleDelete}
|
|
274
|
+
onExport={handleExportDefinition}
|
|
275
|
+
onImport={() => importInputRef.current?.click()}
|
|
276
|
+
/>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
{view === 'builder' && hasData && (
|
|
280
|
+
<ListBuilder
|
|
281
|
+
providers={allProviders}
|
|
282
|
+
initial={editingList}
|
|
283
|
+
onSave={handleSaveList}
|
|
284
|
+
onCancel={() => setView('library')}
|
|
285
|
+
onExecute={handleExecuteList}
|
|
286
|
+
/>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{view === 'results' && listResult && (
|
|
290
|
+
<ListResultsTable result={listResult} />
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
{/* Hidden import input */}
|
|
294
|
+
<input
|
|
295
|
+
ref={importInputRef}
|
|
296
|
+
type="file"
|
|
297
|
+
accept=".json"
|
|
298
|
+
onChange={handleImport}
|
|
299
|
+
className="hidden"
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ============================================================================
|
|
306
|
+
// List Library Sub-Component
|
|
307
|
+
// ============================================================================
|
|
308
|
+
|
|
309
|
+
interface ListLibraryProps {
|
|
310
|
+
definitions: ListDefinition[];
|
|
311
|
+
activeListId: string | null;
|
|
312
|
+
executing: boolean;
|
|
313
|
+
hasData: boolean;
|
|
314
|
+
onExecute: (def: ListDefinition) => void;
|
|
315
|
+
onCreateNew: () => void;
|
|
316
|
+
onEdit: (def: ListDefinition) => void;
|
|
317
|
+
onDuplicate: (def: ListDefinition) => void;
|
|
318
|
+
onDelete: (id: string) => void;
|
|
319
|
+
onExport: (def: ListDefinition) => void;
|
|
320
|
+
onImport: () => void;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function ListLibrary({
|
|
324
|
+
definitions,
|
|
325
|
+
activeListId,
|
|
326
|
+
executing,
|
|
327
|
+
hasData,
|
|
328
|
+
onExecute,
|
|
329
|
+
onCreateNew,
|
|
330
|
+
onEdit,
|
|
331
|
+
onDuplicate,
|
|
332
|
+
onDelete,
|
|
333
|
+
onExport,
|
|
334
|
+
onImport,
|
|
335
|
+
}: ListLibraryProps) {
|
|
336
|
+
return (
|
|
337
|
+
<div className="flex-1 flex flex-col min-h-0">
|
|
338
|
+
{/* Actions */}
|
|
339
|
+
<div className="flex items-center gap-1 px-3 py-2 border-b">
|
|
340
|
+
<Button
|
|
341
|
+
variant="outline"
|
|
342
|
+
size="sm"
|
|
343
|
+
onClick={onCreateNew}
|
|
344
|
+
disabled={!hasData}
|
|
345
|
+
className="text-xs h-7"
|
|
346
|
+
>
|
|
347
|
+
<Plus className="h-3 w-3 mr-1" />
|
|
348
|
+
New List
|
|
349
|
+
</Button>
|
|
350
|
+
<Button variant="ghost" size="sm" onClick={onImport} className="text-xs h-7">
|
|
351
|
+
<Upload className="h-3 w-3 mr-1" />
|
|
352
|
+
Import
|
|
353
|
+
</Button>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<ScrollArea className="flex-1">
|
|
357
|
+
{/* User's saved lists */}
|
|
358
|
+
{definitions.length > 0 && (
|
|
359
|
+
<div className="px-3 py-2">
|
|
360
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
361
|
+
Saved Lists
|
|
362
|
+
</span>
|
|
363
|
+
<div className="mt-1 space-y-1">
|
|
364
|
+
{definitions.map(def => (
|
|
365
|
+
<ListItem
|
|
366
|
+
key={def.id}
|
|
367
|
+
definition={def}
|
|
368
|
+
isActive={activeListId === def.id}
|
|
369
|
+
executing={executing && activeListId === def.id}
|
|
370
|
+
hasData={hasData}
|
|
371
|
+
onExecute={onExecute}
|
|
372
|
+
onEdit={onEdit}
|
|
373
|
+
onDuplicate={onDuplicate}
|
|
374
|
+
onDelete={onDelete}
|
|
375
|
+
onExport={onExport}
|
|
376
|
+
/>
|
|
377
|
+
))}
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
|
|
382
|
+
{definitions.length > 0 && <Separator className="my-1" />}
|
|
383
|
+
|
|
384
|
+
{/* Presets */}
|
|
385
|
+
<div className="px-3 py-2">
|
|
386
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
387
|
+
Templates
|
|
388
|
+
</span>
|
|
389
|
+
<div className="mt-1 space-y-1">
|
|
390
|
+
{LIST_PRESETS.map(preset => (
|
|
391
|
+
<ListItem
|
|
392
|
+
key={preset.id}
|
|
393
|
+
definition={preset}
|
|
394
|
+
isActive={activeListId === preset.id}
|
|
395
|
+
executing={executing && activeListId === preset.id}
|
|
396
|
+
hasData={hasData}
|
|
397
|
+
onExecute={onExecute}
|
|
398
|
+
onDuplicate={onDuplicate}
|
|
399
|
+
isPreset
|
|
400
|
+
/>
|
|
401
|
+
))}
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</ScrollArea>
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ============================================================================
|
|
410
|
+
// List Item
|
|
411
|
+
// ============================================================================
|
|
412
|
+
|
|
413
|
+
interface ListItemProps {
|
|
414
|
+
definition: ListDefinition;
|
|
415
|
+
isActive: boolean;
|
|
416
|
+
executing: boolean;
|
|
417
|
+
hasData: boolean;
|
|
418
|
+
onExecute: (def: ListDefinition) => void;
|
|
419
|
+
onEdit?: (def: ListDefinition) => void;
|
|
420
|
+
onDuplicate?: (def: ListDefinition) => void;
|
|
421
|
+
onDelete?: (id: string) => void;
|
|
422
|
+
onExport?: (def: ListDefinition) => void;
|
|
423
|
+
isPreset?: boolean;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function ListItem({ definition, isActive, executing, hasData, onExecute, onEdit, onDuplicate, onDelete, onExport, isPreset }: ListItemProps) {
|
|
427
|
+
return (
|
|
428
|
+
<div
|
|
429
|
+
className={`group flex items-center gap-2 px-2 py-1.5 rounded-md text-sm cursor-pointer hover:bg-muted/50 ${
|
|
430
|
+
isActive ? 'bg-muted' : ''
|
|
431
|
+
}`}
|
|
432
|
+
onClick={() => hasData && onExecute(definition)}
|
|
433
|
+
>
|
|
434
|
+
<FileSpreadsheet className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
435
|
+
<div className="flex-1 min-w-0">
|
|
436
|
+
<div className="truncate text-xs font-medium">{definition.name}</div>
|
|
437
|
+
{definition.description && (
|
|
438
|
+
<div className="truncate text-xs text-muted-foreground">{definition.description}</div>
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
441
|
+
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
442
|
+
{executing ? (
|
|
443
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
444
|
+
) : (
|
|
445
|
+
<>
|
|
446
|
+
<Tooltip>
|
|
447
|
+
<TooltipTrigger asChild>
|
|
448
|
+
<Button
|
|
449
|
+
variant="ghost"
|
|
450
|
+
size="icon-sm"
|
|
451
|
+
className="h-6 w-6"
|
|
452
|
+
onClick={(e) => {
|
|
453
|
+
e.stopPropagation();
|
|
454
|
+
if (hasData) onExecute(definition);
|
|
455
|
+
}}
|
|
456
|
+
disabled={!hasData}
|
|
457
|
+
>
|
|
458
|
+
<Play className="h-3 w-3" />
|
|
459
|
+
</Button>
|
|
460
|
+
</TooltipTrigger>
|
|
461
|
+
<TooltipContent>Run</TooltipContent>
|
|
462
|
+
</Tooltip>
|
|
463
|
+
{!isPreset && onEdit && (
|
|
464
|
+
<Tooltip>
|
|
465
|
+
<TooltipTrigger asChild>
|
|
466
|
+
<Button
|
|
467
|
+
variant="ghost"
|
|
468
|
+
size="icon-sm"
|
|
469
|
+
className="h-6 w-6"
|
|
470
|
+
onClick={(e) => {
|
|
471
|
+
e.stopPropagation();
|
|
472
|
+
onEdit(definition);
|
|
473
|
+
}}
|
|
474
|
+
>
|
|
475
|
+
<Pencil className="h-3 w-3" />
|
|
476
|
+
</Button>
|
|
477
|
+
</TooltipTrigger>
|
|
478
|
+
<TooltipContent>Edit</TooltipContent>
|
|
479
|
+
</Tooltip>
|
|
480
|
+
)}
|
|
481
|
+
{onDuplicate && (
|
|
482
|
+
<Tooltip>
|
|
483
|
+
<TooltipTrigger asChild>
|
|
484
|
+
<Button
|
|
485
|
+
variant="ghost"
|
|
486
|
+
size="icon-sm"
|
|
487
|
+
className="h-6 w-6"
|
|
488
|
+
onClick={(e) => {
|
|
489
|
+
e.stopPropagation();
|
|
490
|
+
onDuplicate(definition);
|
|
491
|
+
}}
|
|
492
|
+
>
|
|
493
|
+
<Copy className="h-3 w-3" />
|
|
494
|
+
</Button>
|
|
495
|
+
</TooltipTrigger>
|
|
496
|
+
<TooltipContent>{isPreset ? 'Use as Template' : 'Duplicate'}</TooltipContent>
|
|
497
|
+
</Tooltip>
|
|
498
|
+
)}
|
|
499
|
+
{!isPreset && onExport && (
|
|
500
|
+
<Tooltip>
|
|
501
|
+
<TooltipTrigger asChild>
|
|
502
|
+
<Button
|
|
503
|
+
variant="ghost"
|
|
504
|
+
size="icon-sm"
|
|
505
|
+
className="h-6 w-6"
|
|
506
|
+
onClick={(e) => {
|
|
507
|
+
e.stopPropagation();
|
|
508
|
+
onExport(definition);
|
|
509
|
+
}}
|
|
510
|
+
>
|
|
511
|
+
<Download className="h-3 w-3" />
|
|
512
|
+
</Button>
|
|
513
|
+
</TooltipTrigger>
|
|
514
|
+
<TooltipContent>Export</TooltipContent>
|
|
515
|
+
</Tooltip>
|
|
516
|
+
)}
|
|
517
|
+
{!isPreset && onDelete && (
|
|
518
|
+
<Tooltip>
|
|
519
|
+
<TooltipTrigger asChild>
|
|
520
|
+
<Button
|
|
521
|
+
variant="ghost"
|
|
522
|
+
size="icon-sm"
|
|
523
|
+
className="h-6 w-6 hover:text-destructive"
|
|
524
|
+
onClick={(e) => {
|
|
525
|
+
e.stopPropagation();
|
|
526
|
+
onDelete(definition.id);
|
|
527
|
+
}}
|
|
528
|
+
>
|
|
529
|
+
<Trash2 className="h-3 w-3" />
|
|
530
|
+
</Button>
|
|
531
|
+
</TooltipTrigger>
|
|
532
|
+
<TooltipContent>Delete</TooltipContent>
|
|
533
|
+
</Tooltip>
|
|
534
|
+
)}
|
|
535
|
+
</>
|
|
536
|
+
)}
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
}
|