@ifc-lite/viewer 1.16.0 → 1.17.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.
- package/.turbo/turbo-build.log +42 -0
- package/.turbo/turbo-typecheck.log +44 -0
- package/CHANGELOG.md +25 -0
- package/dist/assets/{Arrow.dom--gdrQd-q.js → Arrow.dom-DuPUrOxJ.js} +1 -1
- package/dist/assets/{basketViewActivator-CI3y6VYQ.js → basketViewActivator-DetjPnvt.js} +1 -1
- package/dist/assets/{browser-vWDubxDI.js → browser-BQdwnOUt.js} +1 -1
- package/dist/assets/geometry.worker-Bjm-ukng.js +1 -0
- package/dist/assets/ifc-lite_bg-DD0A7Yow.wasm +0 -0
- package/dist/assets/{index-RXIK18da.js → index-B3X21yXA.js} +4 -4
- package/dist/assets/index-Ba4eoTe7.css +1 -0
- package/dist/assets/{index-BImINgzG.js → index-BybGZJTW.js} +29281 -27174
- package/dist/assets/{native-bridge-4rLidc3f.js → native-bridge-CN0ZMR2t.js} +1 -1
- package/dist/assets/{wasm-bridge-BkfXfw8O.js → wasm-bridge-D0bALkma.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +14 -13
- package/src/components/viewer/BCFPanel.tsx +12 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +315 -154
- package/src/components/viewer/CommandPalette.tsx +0 -6
- package/src/components/viewer/DataConnector.tsx +489 -284
- package/src/components/viewer/ExportDialog.tsx +66 -6
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +44 -1
- package/src/components/viewer/MainToolbar.tsx +1 -5
- package/src/components/viewer/PropertiesPanel.tsx +6 -7
- package/src/components/viewer/Viewport.tsx +42 -56
- package/src/components/viewer/ViewportContainer.tsx +3 -0
- package/src/components/viewer/ViewportOverlays.tsx +12 -10
- package/src/components/viewer/bcf/BCFOverlay.tsx +254 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +70 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +35 -10
- package/src/components/viewer/hierarchy/types.ts +24 -2
- package/src/components/viewer/lists/ListPanel.tsx +0 -21
- package/src/components/viewer/lists/ListResultsTable.tsx +93 -5
- package/src/components/viewer/measureHandlers.ts +558 -0
- package/src/components/viewer/mouseHandlerTypes.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +86 -0
- package/src/components/viewer/useAnimationLoop.ts +116 -44
- package/src/components/viewer/useGeometryStreaming.ts +155 -367
- package/src/components/viewer/useKeyboardControls.ts +30 -46
- package/src/components/viewer/useMouseControls.ts +169 -695
- package/src/components/viewer/useRenderUpdates.ts +9 -59
- package/src/components/viewer/useTouchControls.ts +55 -40
- package/src/hooks/bcfIdLookup.ts +70 -0
- package/src/hooks/useBCF.ts +12 -31
- package/src/hooks/useIfcCache.ts +2 -20
- package/src/hooks/useIfcFederation.ts +5 -11
- package/src/hooks/useIfcLoader.ts +47 -56
- package/src/hooks/useIfcServer.ts +9 -1
- package/src/hooks/useKeyboardShortcuts.ts +0 -10
- package/src/hooks/useLatestRef.ts +24 -0
- package/src/sdk/adapters/export-adapter.ts +2 -2
- package/src/sdk/adapters/model-adapter.ts +1 -0
- package/src/sdk/adapters/visibility-adapter.ts +7 -49
- package/src/sdk/local-backend.ts +2 -0
- package/src/store/basketVisibleSet.test.ts +73 -3
- package/src/store/basketVisibleSet.ts +58 -75
- package/src/store/slices/bcfSlice.ts +9 -0
- package/src/utils/loadingUtils.ts +46 -0
- package/src/utils/serverDataModel.test.ts +90 -0
- package/src/utils/serverDataModel.ts +26 -37
- package/src/utils/spatialHierarchy.test.ts +38 -0
- package/src/utils/spatialHierarchy.ts +13 -23
- package/dist/assets/ifc-lite_bg-CyWQTvp5.wasm +0 -0
- package/dist/assets/index-ax1X2WPd.css +0 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Full integration with CsvConnector from @ifc-lite/mutations
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
|
11
|
+
import { useState, useCallback, useMemo, useRef, useEffect, type DragEvent } from 'react';
|
|
12
12
|
import {
|
|
13
13
|
Upload,
|
|
14
14
|
FileSpreadsheet,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
Eye,
|
|
23
23
|
Play,
|
|
24
24
|
Wand2,
|
|
25
|
+
ChevronRight,
|
|
25
26
|
} from 'lucide-react';
|
|
26
27
|
import { Button } from '@/components/ui/button';
|
|
27
28
|
import { Input } from '@/components/ui/input';
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
TableRow,
|
|
58
59
|
} from '@/components/ui/table';
|
|
59
60
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
61
|
+
import { Progress } from '@/components/ui/progress';
|
|
60
62
|
import { Separator } from '@/components/ui/separator';
|
|
61
63
|
import { useViewerStore } from '@/store';
|
|
62
64
|
import { useIfc } from '@/hooks/useIfc';
|
|
@@ -71,6 +73,7 @@ import {
|
|
|
71
73
|
type DataMapping,
|
|
72
74
|
type MatchResult,
|
|
73
75
|
type ImportStats,
|
|
76
|
+
type ImportProgress,
|
|
74
77
|
} from '@ifc-lite/mutations';
|
|
75
78
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
76
79
|
|
|
@@ -102,6 +105,7 @@ export function DataConnector({ trigger }: DataConnectorProps) {
|
|
|
102
105
|
const legacyGeometryResult = useViewerStore((s) => s.geometryResult);
|
|
103
106
|
|
|
104
107
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
108
|
+
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
105
109
|
const [open, setOpen] = useState(false);
|
|
106
110
|
const [selectedModelId, setSelectedModelId] = useState<string>('');
|
|
107
111
|
|
|
@@ -126,7 +130,10 @@ export function DataConnector({ trigger }: DataConnectorProps) {
|
|
|
126
130
|
const [matchResults, setMatchResults] = useState<MatchResult[] | null>(null);
|
|
127
131
|
const [importStats, setImportStats] = useState<ImportStats | null>(null);
|
|
128
132
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
133
|
+
const [importProgress, setImportProgress] = useState<ImportProgress | null>(null);
|
|
129
134
|
const [error, setError] = useState<string | null>(null);
|
|
135
|
+
// Track whether config changed since last import (disables button after success)
|
|
136
|
+
const [importDirty, setImportDirty] = useState(true);
|
|
130
137
|
|
|
131
138
|
// Get list of models - includes both federated models and legacy single-model
|
|
132
139
|
const modelList = useMemo(() => {
|
|
@@ -212,6 +219,7 @@ export function DataConnector({ trigger }: DataConnectorProps) {
|
|
|
212
219
|
setMatchResults(null);
|
|
213
220
|
setImportStats(null);
|
|
214
221
|
setError(null);
|
|
222
|
+
setImportDirty(true);
|
|
215
223
|
|
|
216
224
|
const reader = new FileReader();
|
|
217
225
|
reader.onload = (event) => {
|
|
@@ -315,6 +323,7 @@ export function DataConnector({ trigger }: DataConnectorProps) {
|
|
|
315
323
|
setMatchResults(null);
|
|
316
324
|
setImportStats(null);
|
|
317
325
|
setError(null);
|
|
326
|
+
setImportDirty(true);
|
|
318
327
|
}, []);
|
|
319
328
|
|
|
320
329
|
// Add a mapping row
|
|
@@ -423,12 +432,13 @@ export function DataConnector({ trigger }: DataConnectorProps) {
|
|
|
423
432
|
}
|
|
424
433
|
}, [csvConnector, csvContent, matchColumn, buildDataMapping]);
|
|
425
434
|
|
|
426
|
-
// Import using CsvConnector.
|
|
427
|
-
const handleImport = useCallback(() => {
|
|
435
|
+
// Import using CsvConnector.importAsync for non-blocking progress
|
|
436
|
+
const handleImport = useCallback(async () => {
|
|
428
437
|
if (!csvConnector || !csvContent) return;
|
|
429
438
|
|
|
430
439
|
setIsProcessing(true);
|
|
431
440
|
setImportStats(null);
|
|
441
|
+
setImportProgress(null);
|
|
432
442
|
setError(null);
|
|
433
443
|
|
|
434
444
|
try {
|
|
@@ -439,10 +449,15 @@ export function DataConnector({ trigger }: DataConnectorProps) {
|
|
|
439
449
|
return;
|
|
440
450
|
}
|
|
441
451
|
|
|
442
|
-
|
|
443
|
-
|
|
452
|
+
const stats = await csvConnector.importAsync(
|
|
453
|
+
csvContent,
|
|
454
|
+
dataMapping,
|
|
455
|
+
(progress) => setImportProgress(progress)
|
|
456
|
+
);
|
|
444
457
|
|
|
445
458
|
setImportStats(stats);
|
|
459
|
+
setImportProgress(null);
|
|
460
|
+
setImportDirty(false);
|
|
446
461
|
|
|
447
462
|
if (stats.errors.length > 0) {
|
|
448
463
|
setError(stats.errors.join('\n'));
|
|
@@ -455,6 +470,36 @@ export function DataConnector({ trigger }: DataConnectorProps) {
|
|
|
455
470
|
}
|
|
456
471
|
}, [csvConnector, csvContent, buildDataMapping]);
|
|
457
472
|
|
|
473
|
+
// Scroll to bottom of the body area — double rAF ensures DOM is painted
|
|
474
|
+
const scrollToBottom = useCallback(() => {
|
|
475
|
+
requestAnimationFrame(() => {
|
|
476
|
+
requestAnimationFrame(() => {
|
|
477
|
+
scrollAreaRef.current?.scrollTo({
|
|
478
|
+
top: scrollAreaRef.current.scrollHeight,
|
|
479
|
+
behavior: 'smooth',
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
}, []);
|
|
484
|
+
|
|
485
|
+
// Auto-scroll when import completes or errors appear
|
|
486
|
+
useEffect(() => {
|
|
487
|
+
if (importStats || error) scrollToBottom();
|
|
488
|
+
}, [importStats, error, scrollToBottom]);
|
|
489
|
+
|
|
490
|
+
// Auto-scroll when progress first appears (so user sees the bar)
|
|
491
|
+
const prevProgressRef = useRef<ImportProgress | null>(null);
|
|
492
|
+
useEffect(() => {
|
|
493
|
+
if (importProgress && !prevProgressRef.current) scrollToBottom();
|
|
494
|
+
prevProgressRef.current = importProgress;
|
|
495
|
+
}, [importProgress, scrollToBottom]);
|
|
496
|
+
|
|
497
|
+
// Mark config dirty when match/mapping settings change after a completed import
|
|
498
|
+
useEffect(() => {
|
|
499
|
+
if (importStats) setImportDirty(true);
|
|
500
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- only fire on config changes, not importStats itself
|
|
501
|
+
}, [matchType, matchColumn, matchPset, matchProp, mappings]);
|
|
502
|
+
|
|
458
503
|
// Stats from match results
|
|
459
504
|
const matchStats = useMemo(() => {
|
|
460
505
|
if (!matchResults) return null;
|
|
@@ -473,6 +518,51 @@ export function DataConnector({ trigger }: DataConnectorProps) {
|
|
|
473
518
|
});
|
|
474
519
|
}, [parsedRows, csvColumns]);
|
|
475
520
|
|
|
521
|
+
// Drag-and-drop handlers
|
|
522
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
523
|
+
|
|
524
|
+
const handleDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {
|
|
525
|
+
e.preventDefault();
|
|
526
|
+
e.stopPropagation();
|
|
527
|
+
setIsDragging(true);
|
|
528
|
+
}, []);
|
|
529
|
+
|
|
530
|
+
const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
|
|
531
|
+
e.preventDefault();
|
|
532
|
+
e.stopPropagation();
|
|
533
|
+
setIsDragging(false);
|
|
534
|
+
}, []);
|
|
535
|
+
|
|
536
|
+
const handleDrop = useCallback(
|
|
537
|
+
(e: DragEvent<HTMLDivElement>) => {
|
|
538
|
+
e.preventDefault();
|
|
539
|
+
e.stopPropagation();
|
|
540
|
+
setIsDragging(false);
|
|
541
|
+
|
|
542
|
+
const file = e.dataTransfer.files[0];
|
|
543
|
+
if (!file || !file.name.endsWith('.csv')) return;
|
|
544
|
+
|
|
545
|
+
// Reuse the same file-reading logic via a synthetic event
|
|
546
|
+
const dataTransfer = new DataTransfer();
|
|
547
|
+
dataTransfer.items.add(file);
|
|
548
|
+
if (fileInputRef.current) {
|
|
549
|
+
fileInputRef.current.files = dataTransfer.files;
|
|
550
|
+
fileInputRef.current.dispatchEvent(new Event('change', { bubbles: true }));
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
[]
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
// Derive the current step for the step indicator
|
|
557
|
+
const currentStep = useMemo(() => {
|
|
558
|
+
if (importStats) return 3;
|
|
559
|
+
if (csvColumns.length > 0 && matchColumn && mappings.length > 0) return 2;
|
|
560
|
+
if (csvColumns.length > 0) return 1;
|
|
561
|
+
return 0;
|
|
562
|
+
}, [csvColumns.length, matchColumn, mappings.length, importStats]);
|
|
563
|
+
|
|
564
|
+
const steps = ['Upload CSV', 'Configure Mapping', 'Import'];
|
|
565
|
+
|
|
476
566
|
return (
|
|
477
567
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
478
568
|
<DialogTrigger asChild>
|
|
@@ -483,317 +573,432 @@ export function DataConnector({ trigger }: DataConnectorProps) {
|
|
|
483
573
|
</Button>
|
|
484
574
|
)}
|
|
485
575
|
</DialogTrigger>
|
|
486
|
-
<DialogContent className="sm:max-w-
|
|
487
|
-
|
|
576
|
+
<DialogContent className="sm:max-w-4xl max-h-[85vh] flex flex-col p-0 gap-0">
|
|
577
|
+
{/* Fixed Header */}
|
|
578
|
+
<DialogHeader className="px-6 pt-6 pb-4 shrink-0 border-b">
|
|
488
579
|
<DialogTitle className="flex items-center gap-2">
|
|
489
580
|
<FileSpreadsheet className="h-5 w-5" />
|
|
490
581
|
Import External Data
|
|
491
582
|
</DialogTitle>
|
|
492
583
|
<DialogDescription>
|
|
493
|
-
|
|
584
|
+
Map CSV data to IFC entity properties
|
|
494
585
|
</DialogDescription>
|
|
495
|
-
</DialogHeader>
|
|
496
586
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
<Label className="text-sm font-medium">CSV File</Label>
|
|
523
|
-
<input
|
|
524
|
-
ref={fileInputRef}
|
|
525
|
-
type="file"
|
|
526
|
-
accept=".csv"
|
|
527
|
-
onChange={handleFileSelect}
|
|
528
|
-
className="hidden"
|
|
529
|
-
/>
|
|
530
|
-
<div className="flex items-center gap-2">
|
|
531
|
-
<Button variant="outline" onClick={() => fileInputRef.current?.click()}>
|
|
532
|
-
<Upload className="h-4 w-4 mr-2" />
|
|
533
|
-
Choose File
|
|
534
|
-
</Button>
|
|
535
|
-
{fileName && <Badge variant="secondary">{fileName}</Badge>}
|
|
536
|
-
</div>
|
|
587
|
+
{/* Step Indicator */}
|
|
588
|
+
<div className="flex items-center gap-1 pt-3">
|
|
589
|
+
{steps.map((step, idx) => (
|
|
590
|
+
<div key={step} className="flex items-center gap-1">
|
|
591
|
+
<div
|
|
592
|
+
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium transition-colors ${
|
|
593
|
+
idx < currentStep
|
|
594
|
+
? 'bg-primary/10 text-primary'
|
|
595
|
+
: idx === currentStep
|
|
596
|
+
? 'bg-primary text-primary-foreground'
|
|
597
|
+
: 'bg-muted text-muted-foreground'
|
|
598
|
+
}`}
|
|
599
|
+
>
|
|
600
|
+
{idx < currentStep ? (
|
|
601
|
+
<Check className="h-3 w-3" />
|
|
602
|
+
) : (
|
|
603
|
+
<span className="w-4 text-center">{idx + 1}</span>
|
|
604
|
+
)}
|
|
605
|
+
{step}
|
|
606
|
+
</div>
|
|
607
|
+
{idx < steps.length - 1 && (
|
|
608
|
+
<ChevronRight className="h-3 w-3 text-muted-foreground shrink-0" />
|
|
609
|
+
)}
|
|
610
|
+
</div>
|
|
611
|
+
))}
|
|
537
612
|
</div>
|
|
613
|
+
</DialogHeader>
|
|
538
614
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
<
|
|
545
|
-
|
|
546
|
-
<
|
|
547
|
-
<
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
{row.map((cell, cellIdx) => (
|
|
561
|
-
<TableCell key={cellIdx} className="text-xs py-1">
|
|
562
|
-
{cell || '—'}
|
|
563
|
-
</TableCell>
|
|
564
|
-
))}
|
|
565
|
-
</TableRow>
|
|
566
|
-
))}
|
|
567
|
-
</TableBody>
|
|
568
|
-
</Table>
|
|
569
|
-
</ScrollArea>
|
|
570
|
-
<p className="text-xs text-muted-foreground">
|
|
571
|
-
{parsedRows.length > 0
|
|
572
|
-
? `${parsedRows.length} rows parsed`
|
|
573
|
-
: `${csvColumns[0]?.sampleValues.length || 0} sample rows`}
|
|
615
|
+
{/* Scrollable Body */}
|
|
616
|
+
<div ref={scrollAreaRef} className="flex-1 overflow-y-auto px-6 py-4">
|
|
617
|
+
<div className="space-y-6">
|
|
618
|
+
{/* Model selector */}
|
|
619
|
+
<div className="space-y-2">
|
|
620
|
+
<Label className="text-sm font-medium">Target Model</Label>
|
|
621
|
+
<Select value={selectedModelId} onValueChange={handleModelChange}>
|
|
622
|
+
<SelectTrigger>
|
|
623
|
+
<SelectValue placeholder="Select a model" />
|
|
624
|
+
</SelectTrigger>
|
|
625
|
+
<SelectContent>
|
|
626
|
+
{modelList.map((m) => (
|
|
627
|
+
<SelectItem key={m.id} value={m.id}>
|
|
628
|
+
{m.name}
|
|
629
|
+
</SelectItem>
|
|
630
|
+
))}
|
|
631
|
+
</SelectContent>
|
|
632
|
+
</Select>
|
|
633
|
+
{selectedModelId && !csvConnector && (
|
|
634
|
+
<p className="text-xs text-amber-600">
|
|
635
|
+
Note: MutationView not available for this model. Some features may be limited.
|
|
574
636
|
</p>
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
<Separator />
|
|
578
|
-
|
|
579
|
-
{/* Matching Configuration */}
|
|
580
|
-
<div className="space-y-4">
|
|
581
|
-
<Label className="text-sm font-medium flex items-center gap-2">
|
|
582
|
-
<Link2 className="h-4 w-4" />
|
|
583
|
-
Match Configuration
|
|
584
|
-
</Label>
|
|
637
|
+
)}
|
|
638
|
+
</div>
|
|
585
639
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
640
|
+
{/* File Upload - Drag and Drop Zone */}
|
|
641
|
+
<div className="space-y-2">
|
|
642
|
+
<Label className="text-sm font-medium">CSV File</Label>
|
|
643
|
+
<input
|
|
644
|
+
ref={fileInputRef}
|
|
645
|
+
type="file"
|
|
646
|
+
accept=".csv"
|
|
647
|
+
onChange={handleFileSelect}
|
|
648
|
+
className="hidden"
|
|
649
|
+
/>
|
|
650
|
+
{!fileName ? (
|
|
651
|
+
<div
|
|
652
|
+
onDragOver={handleDragOver}
|
|
653
|
+
onDragLeave={handleDragLeave}
|
|
654
|
+
onDrop={handleDrop}
|
|
655
|
+
onClick={() => fileInputRef.current?.click()}
|
|
656
|
+
className={`flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-8 cursor-pointer transition-colors ${
|
|
657
|
+
isDragging
|
|
658
|
+
? 'border-primary bg-primary/5'
|
|
659
|
+
: 'border-muted-foreground/25 hover:border-muted-foreground/50 hover:bg-muted/50'
|
|
660
|
+
}`}
|
|
661
|
+
>
|
|
662
|
+
<Upload className={`h-8 w-8 ${isDragging ? 'text-primary' : 'text-muted-foreground'}`} />
|
|
663
|
+
<div className="text-center">
|
|
664
|
+
<p className="text-sm font-medium">
|
|
665
|
+
{isDragging ? 'Drop CSV file here' : 'Drag & drop a CSV file'}
|
|
666
|
+
</p>
|
|
667
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
668
|
+
or click to browse
|
|
669
|
+
</p>
|
|
600
670
|
</div>
|
|
671
|
+
</div>
|
|
672
|
+
) : (
|
|
673
|
+
<div className="flex items-center gap-2">
|
|
674
|
+
<Badge variant="secondary" className="gap-1.5">
|
|
675
|
+
<FileSpreadsheet className="h-3 w-3" />
|
|
676
|
+
{fileName}
|
|
677
|
+
</Badge>
|
|
678
|
+
<Button
|
|
679
|
+
variant="ghost"
|
|
680
|
+
size="sm"
|
|
681
|
+
className="h-7 text-xs"
|
|
682
|
+
onClick={() => fileInputRef.current?.click()}
|
|
683
|
+
>
|
|
684
|
+
Change
|
|
685
|
+
</Button>
|
|
686
|
+
</div>
|
|
687
|
+
)}
|
|
688
|
+
</div>
|
|
601
689
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
690
|
+
{csvColumns.length > 0 && (
|
|
691
|
+
<>
|
|
692
|
+
<Separator />
|
|
693
|
+
|
|
694
|
+
{/* CSV Preview */}
|
|
695
|
+
<div className="space-y-2">
|
|
696
|
+
<Label className="text-sm font-medium">Data Preview</Label>
|
|
697
|
+
<ScrollArea className="h-32 border rounded-md">
|
|
698
|
+
<Table>
|
|
699
|
+
<TableHeader>
|
|
700
|
+
<TableRow>
|
|
701
|
+
{csvColumns.map((col) => (
|
|
702
|
+
<TableHead key={col.name} className="text-xs whitespace-nowrap">
|
|
703
|
+
{col.name}
|
|
704
|
+
</TableHead>
|
|
705
|
+
))}
|
|
706
|
+
</TableRow>
|
|
707
|
+
</TableHeader>
|
|
708
|
+
<TableBody>
|
|
709
|
+
{previewData.map((row, rowIdx) => (
|
|
710
|
+
<TableRow key={rowIdx}>
|
|
711
|
+
{row.map((cell, cellIdx) => (
|
|
712
|
+
<TableCell key={cellIdx} className="text-xs py-1">
|
|
713
|
+
{cell || '\u2014'}
|
|
714
|
+
</TableCell>
|
|
715
|
+
))}
|
|
716
|
+
</TableRow>
|
|
618
717
|
))}
|
|
619
|
-
</
|
|
620
|
-
</
|
|
621
|
-
</
|
|
718
|
+
</TableBody>
|
|
719
|
+
</Table>
|
|
720
|
+
</ScrollArea>
|
|
721
|
+
<p className="text-xs text-muted-foreground">
|
|
722
|
+
{parsedRows.length > 0
|
|
723
|
+
? `${parsedRows.length} rows parsed`
|
|
724
|
+
: `${csvColumns[0]?.sampleValues.length || 0} sample rows`}
|
|
725
|
+
</p>
|
|
622
726
|
</div>
|
|
623
727
|
|
|
624
|
-
|
|
728
|
+
<Separator />
|
|
729
|
+
|
|
730
|
+
{/* Matching Configuration */}
|
|
731
|
+
<div className="space-y-4">
|
|
732
|
+
<Label className="text-sm font-medium flex items-center gap-2">
|
|
733
|
+
<Link2 className="h-4 w-4" />
|
|
734
|
+
Entity Matching
|
|
735
|
+
</Label>
|
|
736
|
+
|
|
625
737
|
<div className="grid grid-cols-2 gap-4">
|
|
626
738
|
<div className="space-y-2">
|
|
627
|
-
<Label className="text-xs text-muted-foreground">
|
|
628
|
-
<
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
739
|
+
<Label className="text-xs text-muted-foreground">Match By</Label>
|
|
740
|
+
<Select value={matchType} onValueChange={(v) => setMatchType(v as MatchType)}>
|
|
741
|
+
<SelectTrigger>
|
|
742
|
+
<SelectValue />
|
|
743
|
+
</SelectTrigger>
|
|
744
|
+
<SelectContent>
|
|
745
|
+
<SelectItem value="globalId">GlobalId</SelectItem>
|
|
746
|
+
<SelectItem value="expressId">EXPRESS ID</SelectItem>
|
|
747
|
+
<SelectItem value="name">Entity Name</SelectItem>
|
|
748
|
+
<SelectItem value="property">Property Value</SelectItem>
|
|
749
|
+
</SelectContent>
|
|
750
|
+
</Select>
|
|
633
751
|
</div>
|
|
752
|
+
|
|
634
753
|
<div className="space-y-2">
|
|
635
|
-
<Label className="text-xs text-muted-foreground">
|
|
636
|
-
<
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
754
|
+
<Label className="text-xs text-muted-foreground">CSV Column</Label>
|
|
755
|
+
<Select value={matchColumn} onValueChange={setMatchColumn}>
|
|
756
|
+
<SelectTrigger>
|
|
757
|
+
<SelectValue placeholder="Select column" />
|
|
758
|
+
</SelectTrigger>
|
|
759
|
+
<SelectContent>
|
|
760
|
+
{csvColumns.map((col) => (
|
|
761
|
+
<SelectItem key={col.name} value={col.name}>
|
|
762
|
+
{col.name}
|
|
763
|
+
{col.sampleValues[0] && (
|
|
764
|
+
<span className="ml-2 text-muted-foreground">
|
|
765
|
+
(e.g., {col.sampleValues[0].slice(0, 20)})
|
|
766
|
+
</span>
|
|
767
|
+
)}
|
|
768
|
+
</SelectItem>
|
|
769
|
+
))}
|
|
770
|
+
</SelectContent>
|
|
771
|
+
</Select>
|
|
641
772
|
</div>
|
|
642
773
|
</div>
|
|
643
|
-
)}
|
|
644
|
-
</div>
|
|
645
|
-
|
|
646
|
-
<Separator />
|
|
647
|
-
|
|
648
|
-
{/* Property Mappings */}
|
|
649
|
-
<div className="space-y-4">
|
|
650
|
-
<div className="flex items-center justify-between">
|
|
651
|
-
<Label className="text-sm font-medium">Property Mappings</Label>
|
|
652
|
-
<div className="flex items-center gap-2">
|
|
653
|
-
{csvConnector && (
|
|
654
|
-
<Button variant="ghost" size="sm" onClick={handleAutoDetect}>
|
|
655
|
-
<Wand2 className="h-3 w-3 mr-1" />
|
|
656
|
-
Auto-detect
|
|
657
|
-
</Button>
|
|
658
|
-
)}
|
|
659
|
-
<Button variant="ghost" size="sm" onClick={addMapping}>
|
|
660
|
-
<Plus className="h-3 w-3 mr-1" />
|
|
661
|
-
Add Mapping
|
|
662
|
-
</Button>
|
|
663
|
-
</div>
|
|
664
|
-
</div>
|
|
665
|
-
|
|
666
|
-
{mappings.length === 0 ? (
|
|
667
|
-
<p className="text-sm text-muted-foreground text-center py-4">
|
|
668
|
-
Add mappings to import column values as IFC properties
|
|
669
|
-
</p>
|
|
670
|
-
) : (
|
|
671
|
-
<div className="space-y-2">
|
|
672
|
-
{mappings.map((mapping) => (
|
|
673
|
-
<div
|
|
674
|
-
key={mapping.id}
|
|
675
|
-
className="flex items-center gap-2 p-2 border rounded-md bg-muted/30"
|
|
676
|
-
>
|
|
677
|
-
<Select
|
|
678
|
-
value={mapping.sourceColumn}
|
|
679
|
-
onValueChange={(v) => updateMapping(mapping.id, 'sourceColumn', v)}
|
|
680
|
-
>
|
|
681
|
-
<SelectTrigger className="h-8 w-32">
|
|
682
|
-
<SelectValue placeholder="Column" />
|
|
683
|
-
</SelectTrigger>
|
|
684
|
-
<SelectContent>
|
|
685
|
-
{csvColumns.map((col) => (
|
|
686
|
-
<SelectItem key={col.name} value={col.name}>
|
|
687
|
-
{col.name}
|
|
688
|
-
</SelectItem>
|
|
689
|
-
))}
|
|
690
|
-
</SelectContent>
|
|
691
|
-
</Select>
|
|
692
|
-
|
|
693
|
-
<ArrowRight className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
694
774
|
|
|
775
|
+
{matchType === 'property' && (
|
|
776
|
+
<div className="grid grid-cols-2 gap-4">
|
|
777
|
+
<div className="space-y-2">
|
|
778
|
+
<Label className="text-xs text-muted-foreground">Property Set</Label>
|
|
695
779
|
<Input
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
className="h-8 text-xs w-32"
|
|
780
|
+
value={matchPset}
|
|
781
|
+
onChange={(e) => setMatchPset(e.target.value)}
|
|
782
|
+
placeholder="e.g., Pset_WallCommon"
|
|
700
783
|
/>
|
|
701
|
-
|
|
784
|
+
</div>
|
|
785
|
+
<div className="space-y-2">
|
|
786
|
+
<Label className="text-xs text-muted-foreground">Property Name</Label>
|
|
702
787
|
<Input
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
updateMapping(mapping.id, 'targetProperty', e.target.value)
|
|
707
|
-
}
|
|
708
|
-
className="h-8 text-xs flex-1"
|
|
788
|
+
value={matchProp}
|
|
789
|
+
onChange={(e) => setMatchProp(e.target.value)}
|
|
790
|
+
placeholder="e.g., Reference"
|
|
709
791
|
/>
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
)}
|
|
795
|
+
</div>
|
|
710
796
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
<SelectItem value={PropertyValueType.Real.toString()}>Real</SelectItem>
|
|
723
|
-
<SelectItem value={PropertyValueType.Integer.toString()}>
|
|
724
|
-
Integer
|
|
725
|
-
</SelectItem>
|
|
726
|
-
<SelectItem value={PropertyValueType.Boolean.toString()}>
|
|
727
|
-
Boolean
|
|
728
|
-
</SelectItem>
|
|
729
|
-
</SelectContent>
|
|
730
|
-
</Select>
|
|
731
|
-
|
|
732
|
-
<Button
|
|
733
|
-
variant="ghost"
|
|
734
|
-
size="icon"
|
|
735
|
-
className="h-8 w-8"
|
|
736
|
-
onClick={() => removeMapping(mapping.id)}
|
|
737
|
-
>
|
|
738
|
-
<Trash2 className="h-3 w-3 text-destructive" />
|
|
797
|
+
<Separator />
|
|
798
|
+
|
|
799
|
+
{/* Property Mappings */}
|
|
800
|
+
<div className="space-y-4">
|
|
801
|
+
<div className="flex items-center justify-between">
|
|
802
|
+
<Label className="text-sm font-medium">Property Mappings</Label>
|
|
803
|
+
<div className="flex items-center gap-2">
|
|
804
|
+
{csvConnector && (
|
|
805
|
+
<Button variant="ghost" size="sm" onClick={handleAutoDetect}>
|
|
806
|
+
<Wand2 className="h-3 w-3 mr-1" />
|
|
807
|
+
Auto-detect
|
|
739
808
|
</Button>
|
|
740
|
-
|
|
741
|
-
|
|
809
|
+
)}
|
|
810
|
+
<Button variant="ghost" size="sm" onClick={addMapping}>
|
|
811
|
+
<Plus className="h-3 w-3 mr-1" />
|
|
812
|
+
Add
|
|
813
|
+
</Button>
|
|
814
|
+
</div>
|
|
742
815
|
</div>
|
|
743
|
-
)}
|
|
744
|
-
</div>
|
|
745
816
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
817
|
+
{mappings.length === 0 ? (
|
|
818
|
+
<div className="text-center py-6 border rounded-lg border-dashed">
|
|
819
|
+
<p className="text-sm text-muted-foreground">
|
|
820
|
+
No property mappings configured
|
|
821
|
+
</p>
|
|
822
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
823
|
+
Click "Auto-detect" or "Add" to map CSV columns to IFC properties
|
|
824
|
+
</p>
|
|
825
|
+
</div>
|
|
826
|
+
) : (
|
|
827
|
+
<div className="space-y-2">
|
|
828
|
+
{/* Column headers for mapping rows */}
|
|
829
|
+
<div className="grid grid-cols-[1fr_auto_1fr_1fr_auto_auto] gap-2 px-2 text-xs text-muted-foreground">
|
|
830
|
+
<span>Source Column</span>
|
|
831
|
+
<span />
|
|
832
|
+
<span>Target Pset</span>
|
|
833
|
+
<span>Target Property</span>
|
|
834
|
+
<span>Type</span>
|
|
835
|
+
<span />
|
|
836
|
+
</div>
|
|
837
|
+
{mappings.map((mapping) => (
|
|
838
|
+
<div
|
|
839
|
+
key={mapping.id}
|
|
840
|
+
className="grid grid-cols-[1fr_auto_1fr_1fr_auto_auto] gap-2 items-center p-2 border rounded-md bg-muted/30"
|
|
841
|
+
>
|
|
842
|
+
<Select
|
|
843
|
+
value={mapping.sourceColumn}
|
|
844
|
+
onValueChange={(v) => updateMapping(mapping.id, 'sourceColumn', v)}
|
|
845
|
+
>
|
|
846
|
+
<SelectTrigger className="h-8">
|
|
847
|
+
<SelectValue placeholder="Column" />
|
|
848
|
+
</SelectTrigger>
|
|
849
|
+
<SelectContent>
|
|
850
|
+
{csvColumns.map((col) => (
|
|
851
|
+
<SelectItem key={col.name} value={col.name}>
|
|
852
|
+
{col.name}
|
|
853
|
+
</SelectItem>
|
|
854
|
+
))}
|
|
855
|
+
</SelectContent>
|
|
856
|
+
</Select>
|
|
857
|
+
|
|
858
|
+
<ArrowRight className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
859
|
+
|
|
860
|
+
<Input
|
|
861
|
+
placeholder="Pset name"
|
|
862
|
+
value={mapping.targetPset}
|
|
863
|
+
onChange={(e) =>
|
|
864
|
+
updateMapping(mapping.id, 'targetPset', e.target.value)
|
|
865
|
+
}
|
|
866
|
+
className="h-8 text-xs"
|
|
867
|
+
/>
|
|
868
|
+
|
|
869
|
+
<Input
|
|
870
|
+
placeholder="Property"
|
|
871
|
+
value={mapping.targetProperty}
|
|
872
|
+
onChange={(e) =>
|
|
873
|
+
updateMapping(mapping.id, 'targetProperty', e.target.value)
|
|
874
|
+
}
|
|
875
|
+
className="h-8 text-xs"
|
|
876
|
+
/>
|
|
877
|
+
|
|
878
|
+
<Select
|
|
879
|
+
value={mapping.valueType.toString()}
|
|
880
|
+
onValueChange={(v) =>
|
|
881
|
+
updateMapping(mapping.id, 'valueType', parseInt(v))
|
|
882
|
+
}
|
|
883
|
+
>
|
|
884
|
+
<SelectTrigger className="h-8 w-24">
|
|
885
|
+
<SelectValue />
|
|
886
|
+
</SelectTrigger>
|
|
887
|
+
<SelectContent>
|
|
888
|
+
<SelectItem value={PropertyValueType.String.toString()}>
|
|
889
|
+
String
|
|
890
|
+
</SelectItem>
|
|
891
|
+
<SelectItem value={PropertyValueType.Real.toString()}>
|
|
892
|
+
Real
|
|
893
|
+
</SelectItem>
|
|
894
|
+
<SelectItem value={PropertyValueType.Integer.toString()}>
|
|
895
|
+
Integer
|
|
896
|
+
</SelectItem>
|
|
897
|
+
<SelectItem value={PropertyValueType.Boolean.toString()}>
|
|
898
|
+
Boolean
|
|
899
|
+
</SelectItem>
|
|
900
|
+
</SelectContent>
|
|
901
|
+
</Select>
|
|
902
|
+
|
|
903
|
+
<Button
|
|
904
|
+
variant="ghost"
|
|
905
|
+
size="icon"
|
|
906
|
+
className="h-8 w-8"
|
|
907
|
+
onClick={() => removeMapping(mapping.id)}
|
|
908
|
+
>
|
|
909
|
+
<Trash2 className="h-3 w-3 text-destructive" />
|
|
910
|
+
</Button>
|
|
911
|
+
</div>
|
|
912
|
+
))}
|
|
913
|
+
</div>
|
|
914
|
+
)}
|
|
915
|
+
</div>
|
|
761
916
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
<Badge variant="
|
|
770
|
-
|
|
917
|
+
{/* Match Results */}
|
|
918
|
+
{matchStats && (
|
|
919
|
+
<Alert>
|
|
920
|
+
<Eye className="h-4 w-4" />
|
|
921
|
+
<AlertTitle>Match Results</AlertTitle>
|
|
922
|
+
<AlertDescription className="flex flex-wrap items-center gap-2">
|
|
923
|
+
<Badge variant="default">{matchStats.matched} matched</Badge>
|
|
924
|
+
<Badge variant="secondary">{matchStats.unmatched} unmatched</Badge>
|
|
925
|
+
<Badge variant="outline">
|
|
926
|
+
{matchStats.highConfidence} high confidence
|
|
771
927
|
</Badge>
|
|
772
|
-
|
|
773
|
-
|
|
928
|
+
{matchStats.multiMatch > 0 && (
|
|
929
|
+
<Badge variant="destructive">
|
|
930
|
+
{matchStats.multiMatch} multi-match
|
|
931
|
+
</Badge>
|
|
932
|
+
)}
|
|
933
|
+
</AlertDescription>
|
|
934
|
+
</Alert>
|
|
935
|
+
)}
|
|
936
|
+
|
|
937
|
+
{/* Live Import Progress */}
|
|
938
|
+
{importProgress && (
|
|
939
|
+
<div className="space-y-2">
|
|
940
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
941
|
+
<span className="flex items-center gap-2">
|
|
942
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
943
|
+
{importProgress.phase === 'parsing' && 'Parsing CSV...'}
|
|
944
|
+
{importProgress.phase === 'matching' && 'Matching entities...'}
|
|
945
|
+
{importProgress.phase === 'applying' && 'Applying properties...'}
|
|
946
|
+
</span>
|
|
947
|
+
<span className="tabular-nums">
|
|
948
|
+
{importProgress.matchedRows.toLocaleString()} matched
|
|
949
|
+
{importProgress.mutationsCreated > 0 &&
|
|
950
|
+
` \u00b7 ${importProgress.mutationsCreated.toLocaleString()} written`}
|
|
951
|
+
</span>
|
|
774
952
|
</div>
|
|
775
|
-
{
|
|
776
|
-
|
|
777
|
-
|
|
953
|
+
<Progress value={importProgress.percent * 100} />
|
|
954
|
+
</div>
|
|
955
|
+
)}
|
|
956
|
+
|
|
957
|
+
{/* Import Stats */}
|
|
958
|
+
{importStats && (
|
|
959
|
+
<Alert
|
|
960
|
+
variant={importStats.errors.length === 0 ? 'default' : 'destructive'}
|
|
961
|
+
>
|
|
962
|
+
<Check className="h-4 w-4" />
|
|
963
|
+
<AlertTitle>Import Complete</AlertTitle>
|
|
964
|
+
<AlertDescription>
|
|
965
|
+
<div className="flex flex-wrap items-center gap-2 mt-1">
|
|
966
|
+
<Badge variant="default">
|
|
967
|
+
{importStats.mutationsCreated} properties updated
|
|
968
|
+
</Badge>
|
|
969
|
+
<Badge variant="secondary">
|
|
970
|
+
{importStats.matchedRows} rows matched
|
|
971
|
+
</Badge>
|
|
972
|
+
<Badge variant="outline">
|
|
973
|
+
{importStats.unmatchedRows} rows unmatched
|
|
974
|
+
</Badge>
|
|
778
975
|
</div>
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
976
|
+
{importStats.warnings.length > 0 && (
|
|
977
|
+
<div className="mt-2 text-xs text-amber-600">
|
|
978
|
+
{importStats.warnings.length} warning(s)
|
|
979
|
+
</div>
|
|
980
|
+
)}
|
|
981
|
+
</AlertDescription>
|
|
982
|
+
</Alert>
|
|
983
|
+
)}
|
|
783
984
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
985
|
+
{/* Error Display */}
|
|
986
|
+
{error && (
|
|
987
|
+
<Alert variant="destructive">
|
|
988
|
+
<AlertCircle className="h-4 w-4" />
|
|
989
|
+
<AlertTitle>Error</AlertTitle>
|
|
990
|
+
<AlertDescription className="whitespace-pre-wrap">
|
|
991
|
+
{error}
|
|
992
|
+
</AlertDescription>
|
|
993
|
+
</Alert>
|
|
994
|
+
)}
|
|
995
|
+
</>
|
|
996
|
+
)}
|
|
997
|
+
</div>
|
|
794
998
|
</div>
|
|
795
999
|
|
|
796
|
-
|
|
1000
|
+
{/* Fixed Footer */}
|
|
1001
|
+
<DialogFooter className="px-6 py-4 border-t shrink-0 gap-2">
|
|
797
1002
|
<Button
|
|
798
1003
|
variant="secondary"
|
|
799
1004
|
onClick={handlePreview}
|
|
@@ -811,21 +1016,21 @@ export function DataConnector({ trigger }: DataConnectorProps) {
|
|
|
811
1016
|
disabled={
|
|
812
1017
|
!csvConnector ||
|
|
813
1018
|
!csvContent ||
|
|
814
|
-
!
|
|
815
|
-
matchStats?.matched === 0 ||
|
|
1019
|
+
!matchColumn ||
|
|
816
1020
|
mappings.length === 0 ||
|
|
817
|
-
isProcessing
|
|
1021
|
+
isProcessing ||
|
|
1022
|
+
!importDirty
|
|
818
1023
|
}
|
|
819
1024
|
>
|
|
820
|
-
{isProcessing ? (
|
|
1025
|
+
{isProcessing && importProgress ? (
|
|
821
1026
|
<>
|
|
822
1027
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
823
|
-
|
|
1028
|
+
{Math.round(importProgress.percent * 100)}%
|
|
824
1029
|
</>
|
|
825
1030
|
) : (
|
|
826
1031
|
<>
|
|
827
1032
|
<Play className="h-4 w-4 mr-2" />
|
|
828
|
-
Import {matchStats
|
|
1033
|
+
{matchStats ? `Import ${matchStats.matched} rows` : 'Import'}
|
|
829
1034
|
</>
|
|
830
1035
|
)}
|
|
831
1036
|
</Button>
|