190 lines
7.4 KiB
TypeScript
190 lines
7.4 KiB
TypeScript
/**
|
|
* GraphicalEditorPage
|
|
*
|
|
* Layout: [UDB sidebar (collapsible)] [FlowEditor (flex)] [Chat/Tracing (inside FlowEditor)]
|
|
* UDB provides access to Files & Sources while configuring nodes.
|
|
* AI Chat and Tracing panels are managed by the FlowEditor's CanvasHeader.
|
|
*
|
|
* File/Source attachment UX mirrors the Workspace:
|
|
* - Files: click in UDB FilesTab → added as pendingFile chip in chat input
|
|
* - Data Sources: 🔗 picker button in chat input (loaded from UDB API)
|
|
*/
|
|
import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react';
|
|
import { useSearchParams } from 'react-router-dom';
|
|
import { FaDatabase, FaChevronLeft } from 'react-icons/fa';
|
|
import { useInstanceId, useMandateId } from '../../../hooks/useCurrentInstance';
|
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
|
import { Automation2FlowEditor as FlowEditor } from '../../../components/FlowEditor';
|
|
import type { PendingFile, EditorDataSource, EditorFeatureDataSource } from '../../../components/FlowEditor';
|
|
import { UnifiedDataBar } from '../../../components/UnifiedDataBar';
|
|
import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar';
|
|
import api from '../../../api';
|
|
import styles from '../../FeatureView.module.css';
|
|
|
|
interface GraphicalEditorPageProps {
|
|
persistentInstanceId?: string;
|
|
persistentMandateId?: string;
|
|
}
|
|
|
|
export const GraphicalEditorPage: React.FC<GraphicalEditorPageProps> = ({
|
|
persistentInstanceId,
|
|
persistentMandateId,
|
|
}) => {
|
|
const urlInstanceId = useInstanceId();
|
|
const urlMandateId = useMandateId();
|
|
const instanceId = persistentInstanceId || urlInstanceId;
|
|
const mandateId = persistentMandateId || urlMandateId;
|
|
const [searchParams] = useSearchParams();
|
|
const initialWorkflowIdRef = useRef(searchParams.get('workflowId'));
|
|
const { currentLanguage } = useLanguage();
|
|
const language = (currentLanguage?.slice(0, 2) || 'de') as string;
|
|
const [udbTab, setUdbTab] = useState<UdbTab>('files');
|
|
const [udbOpen, setUdbOpen] = useState(true);
|
|
|
|
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
|
const [dataSources, setDataSources] = useState<EditorDataSource[]>([]);
|
|
const [featureDataSources, setFeatureDataSources] = useState<EditorFeatureDataSource[]>([]);
|
|
|
|
const udbContext: UdbContext = useMemo(() => ({
|
|
instanceId: instanceId || '',
|
|
mandateId: mandateId || '',
|
|
featureInstanceId: instanceId || '',
|
|
}), [instanceId, mandateId]);
|
|
|
|
useEffect(() => {
|
|
if (!instanceId) return;
|
|
api.get(`/api/workspace/${instanceId}/datasources`)
|
|
.then(res => {
|
|
const list = (res.data.dataSources || res.data || []).map((d: any) => ({
|
|
id: d.id, label: d.label || d.path || d.id, path: d.path, sourceType: d.sourceType,
|
|
}));
|
|
setDataSources(list);
|
|
})
|
|
.catch(() => setDataSources([]));
|
|
}, [instanceId]);
|
|
|
|
useEffect(() => {
|
|
if (!instanceId) return;
|
|
api.get(`/api/workspace/${instanceId}/feature-datasources`)
|
|
.then(res => {
|
|
const list = (res.data.featureDataSources || res.data || []).map((d: any) => ({
|
|
id: d.id, featureInstanceId: d.featureInstanceId, featureCode: d.featureCode,
|
|
tableName: d.tableName, label: d.label || d.tableName,
|
|
}));
|
|
setFeatureDataSources(list);
|
|
})
|
|
.catch(() => setFeatureDataSources([]));
|
|
}, [instanceId]);
|
|
|
|
const _handleFileSelect = useCallback((fileId: string, fileName?: string) => {
|
|
setPendingFiles(prev => {
|
|
if (prev.some(f => f.fileId === fileId)) return prev;
|
|
return [...prev, { fileId, fileName: fileName || fileId.slice(0, 12) }];
|
|
});
|
|
}, []);
|
|
|
|
const _handleRemovePendingFile = useCallback((fileId: string) => {
|
|
setPendingFiles(prev => prev.filter(f => f.fileId !== fileId));
|
|
}, []);
|
|
|
|
const _handleSourcesChanged = useCallback(() => {
|
|
if (!instanceId) return;
|
|
api.get(`/api/workspace/${instanceId}/datasources`)
|
|
.then(res => {
|
|
setDataSources((res.data.dataSources || res.data || []).map((d: any) => ({
|
|
id: d.id, label: d.label || d.path || d.id, path: d.path, sourceType: d.sourceType,
|
|
})));
|
|
})
|
|
.catch(() => {});
|
|
api.get(`/api/workspace/${instanceId}/feature-datasources`)
|
|
.then(res => {
|
|
setFeatureDataSources((res.data.featureDataSources || res.data || []).map((d: any) => ({
|
|
id: d.id, featureInstanceId: d.featureInstanceId, featureCode: d.featureCode,
|
|
tableName: d.tableName, label: d.label || d.tableName,
|
|
})));
|
|
})
|
|
.catch(() => {});
|
|
}, [instanceId]);
|
|
|
|
if (!instanceId) {
|
|
return (
|
|
<div className={styles.placeholder}>
|
|
<h2>Graphical Editor</h2>
|
|
<p>Keine Feature-Instanz gefunden.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ flex: 1, minHeight: 0, display: 'flex', position: 'relative' }}>
|
|
{/* UDB Sidebar */}
|
|
{udbOpen ? (
|
|
<div style={{
|
|
width: 280, minWidth: 280,
|
|
borderRight: '1px solid var(--border-color, #e0e0e0)',
|
|
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
|
background: 'var(--bg-primary, #fff)',
|
|
}}>
|
|
<div style={{
|
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
padding: '6px 12px',
|
|
borderBottom: '1px solid var(--border-color, #e0e0e0)',
|
|
background: 'var(--bg-secondary, #f8f9fa)',
|
|
}}>
|
|
<span style={{ fontWeight: 600, fontSize: '13px', display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
<FaDatabase style={{ fontSize: 12, opacity: 0.6 }} /> Daten
|
|
</span>
|
|
<button
|
|
onClick={() => setUdbOpen(false)}
|
|
title="Sidebar schliessen"
|
|
style={{
|
|
border: 'none', background: 'transparent', cursor: 'pointer',
|
|
fontSize: '14px', padding: '2px 4px', borderRadius: 4,
|
|
display: 'flex', alignItems: 'center',
|
|
}}
|
|
>
|
|
<FaChevronLeft style={{ fontSize: 12 }} />
|
|
</button>
|
|
</div>
|
|
<UnifiedDataBar
|
|
context={udbContext}
|
|
activeTab={udbTab}
|
|
onTabChange={setUdbTab}
|
|
hideTabs={['chats']}
|
|
onFileSelect={_handleFileSelect}
|
|
onSourcesChanged={_handleSourcesChanged}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<button
|
|
onClick={() => setUdbOpen(true)}
|
|
title="Daten-Sidebar öffnen"
|
|
style={{
|
|
position: 'absolute', left: 0, top: '50%', transform: 'translateY(-50%)',
|
|
zIndex: 20, width: 28, height: 80,
|
|
border: '1px solid var(--border-color, #ddd)', borderLeft: 'none',
|
|
borderRadius: '0 8px 8px 0',
|
|
background: 'var(--bg-primary, #fff)', cursor: 'pointer',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
boxShadow: '2px 0 6px rgba(0,0,0,0.06)',
|
|
}}
|
|
>
|
|
<FaDatabase style={{ fontSize: 13, opacity: 0.5 }} />
|
|
</button>
|
|
)}
|
|
|
|
{/* FlowEditor */}
|
|
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
|
|
<FlowEditor
|
|
instanceId={instanceId}
|
|
language={language}
|
|
initialWorkflowId={initialWorkflowIdRef.current}
|
|
pendingFiles={pendingFiles}
|
|
onRemovePendingFile={_handleRemovePendingFile}
|
|
dataSources={dataSources}
|
|
featureDataSources={featureDataSources}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|