T20: Add export/import UI to RibbonBar.tsx
This commit is contained in:
@@ -1,12 +1,21 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useRef, useCallback } from 'react';
|
||||||
|
import type { YjsDocument } from '../crdt/YjsDocument';
|
||||||
|
import { exportDrawing, type ExportFormat } from '../services/exportService';
|
||||||
|
import { importDrawing, type ImportFormat, type ImportResult } from '../services/importService';
|
||||||
import './RibbonBar.css';
|
import './RibbonBar.css';
|
||||||
|
|
||||||
interface RibbonBarProps {
|
interface RibbonBarProps {
|
||||||
onTabChange: (tab: string) => void;
|
onTabChange: (tab: string) => void;
|
||||||
|
yjsDoc?: React.RefObject<YjsDocument | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange }) => {
|
const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange, yjsDoc }) => {
|
||||||
const [activeTab, setActiveTab] = useState('draw');
|
const [activeTab, setActiveTab] = useState('draw');
|
||||||
|
const [showExportMenu, setShowExportMenu] = useState(false);
|
||||||
|
const [showImportMenu, setShowImportMenu] = useState(false);
|
||||||
|
const [importStatus, setImportStatus] = useState<string | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const pendingImportFormat = useRef<ImportFormat>('json');
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'draw', label: 'Draw', icon: '✏️' },
|
{ id: 'draw', label: 'Draw', icon: '✏️' },
|
||||||
@@ -21,6 +30,57 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange }) => {
|
|||||||
onTabChange(tabId);
|
onTabChange(tabId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExport = useCallback(async (format: ExportFormat) => {
|
||||||
|
setShowExportMenu(false);
|
||||||
|
const doc = yjsDoc?.current;
|
||||||
|
if (!doc) {
|
||||||
|
setImportStatus('Error: No document available');
|
||||||
|
setTimeout(() => setImportStatus(null), 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await exportDrawing(doc, format);
|
||||||
|
} catch (e) {
|
||||||
|
setImportStatus(`Export error: ${(e as Error).message}`);
|
||||||
|
setTimeout(() => setImportStatus(null), 5000);
|
||||||
|
}
|
||||||
|
}, [yjsDoc]);
|
||||||
|
|
||||||
|
const triggerImport = useCallback((format: ImportFormat) => {
|
||||||
|
setShowImportMenu(false);
|
||||||
|
pendingImportFormat.current = format;
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.accept = format === 'json' ? '.json,application/json' : '.dxf,application/dxf';
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFileSelected = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const doc = yjsDoc?.current;
|
||||||
|
if (!doc) {
|
||||||
|
setImportStatus('Error: No document available');
|
||||||
|
setTimeout(() => setImportStatus(null), 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result: ImportResult = await importDrawing(file, pendingImportFormat.current, doc);
|
||||||
|
if (result.success) {
|
||||||
|
setImportStatus(`Imported: ${result.elementsImported} elements, ${result.layersImported} layers`);
|
||||||
|
} else {
|
||||||
|
setImportStatus(`Import failed: ${result.errors.join(', ')}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setImportStatus(`Import error: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
|
setTimeout(() => setImportStatus(null), 5000);
|
||||||
|
// Reset input so same file can be selected again
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
}, [yjsDoc]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ribbon-bar" role="menubar" aria-label="CAD Functions">
|
<div className="ribbon-bar" role="menubar" aria-label="CAD Functions">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
@@ -35,6 +95,74 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange }) => {
|
|||||||
<span className="tab-label">{tab.label}</span>
|
<span className="tab-label">{tab.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Export/Import section */}
|
||||||
|
<div className="ribbon-divider" aria-hidden="true" />
|
||||||
|
|
||||||
|
<div className="ribbon-dropdown-wrapper">
|
||||||
|
<button
|
||||||
|
className="ribbon-tab"
|
||||||
|
onClick={() => setShowExportMenu(!showExportMenu)}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={showExportMenu}
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
<span className="tab-icon" aria-hidden="true">📤</span>
|
||||||
|
<span className="tab-label">Export</span>
|
||||||
|
</button>
|
||||||
|
{showExportMenu && (
|
||||||
|
<div className="ribbon-dropdown" role="menu">
|
||||||
|
<button className="ribbon-dropdown-item" onClick={() => handleExport('json')} role="menuitem">
|
||||||
|
JSON
|
||||||
|
</button>
|
||||||
|
<button className="ribbon-dropdown-item" onClick={() => handleExport('svg')} role="menuitem">
|
||||||
|
SVG
|
||||||
|
</button>
|
||||||
|
<button className="ribbon-dropdown-item" onClick={() => handleExport('dxf')} role="menuitem">
|
||||||
|
DXF
|
||||||
|
</button>
|
||||||
|
<button className="ribbon-dropdown-item" onClick={() => handleExport('pdf')} role="menuitem">
|
||||||
|
PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ribbon-dropdown-wrapper">
|
||||||
|
<button
|
||||||
|
className="ribbon-tab"
|
||||||
|
onClick={() => setShowImportMenu(!showImportMenu)}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={showImportMenu}
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
<span className="tab-icon" aria-hidden="true">📥</span>
|
||||||
|
<span className="tab-label">Import</span>
|
||||||
|
</button>
|
||||||
|
{showImportMenu && (
|
||||||
|
<div className="ribbon-dropdown" role="menu">
|
||||||
|
<button className="ribbon-dropdown-item" onClick={() => triggerImport('json')} role="menuitem">
|
||||||
|
JSON
|
||||||
|
</button>
|
||||||
|
<button className="ribbon-dropdown-item" onClick={() => triggerImport('dxf')} role="menuitem">
|
||||||
|
DXF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{importStatus && (
|
||||||
|
<span className="ribbon-status" role="status" aria-live="polite">
|
||||||
|
{importStatus}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileSelected}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user