diff --git a/src/components/FolderTree/FolderTree.tsx b/src/components/FolderTree/FolderTree.tsx index fc102f4..2e2194c 100644 --- a/src/components/FolderTree/FolderTree.tsx +++ b/src/components/FolderTree/FolderTree.tsx @@ -620,7 +620,7 @@ export default function FolderTree({ expandedIds: externalExpandedIds, onToggleExpand, onCreateFolder, onRenameFolder, onDeleteFolder, onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles, onRenameFile, onDeleteFile, onDeleteFiles, onDeleteFolders, onRefresh, onDownloadFolder, - onScopeChange, onNeutralizeToggle, + onScopeChange, onNeutralizeToggle, onFolderNeutralizeToggle, onSendToChat, }: FolderTreeProps) { const { t } = useLanguage(); diff --git a/src/components/Navigation/UserSection.tsx b/src/components/Navigation/UserSection.tsx index 384f930..842a353 100644 --- a/src/components/Navigation/UserSection.tsx +++ b/src/components/Navigation/UserSection.tsx @@ -140,8 +140,8 @@ export const UserSection: React.FC = () => { {/* Legal Modal */} {showLegalModal && ( -
setShowLegalModal(false)}> -
e.stopPropagation()}> +
+

{t('Legal notices')}

+ ); +} + +export default LanguageSelector; diff --git a/src/components/UiComponents/LanguageSelector/index.ts b/src/components/UiComponents/LanguageSelector/index.ts new file mode 100644 index 0000000..9952ab3 --- /dev/null +++ b/src/components/UiComponents/LanguageSelector/index.ts @@ -0,0 +1,2 @@ +export { LanguageSelector } from './LanguageSelector'; +export { default } from './LanguageSelector'; diff --git a/src/components/UiComponents/Popup/Popup.tsx b/src/components/UiComponents/Popup/Popup.tsx index fbdbe7f..c19894b 100644 --- a/src/components/UiComponents/Popup/Popup.tsx +++ b/src/components/UiComponents/Popup/Popup.tsx @@ -23,6 +23,8 @@ export interface PopupProps { className?: string; size?: 'small' | 'medium' | 'large' | 'fullscreen'; closable?: boolean; + closeOnBackdropClick?: boolean; + closeOnEscape?: boolean; actions?: PopupAction[]; } @@ -36,6 +38,8 @@ export function Popup({ className = '', size = 'medium', closable = true, + closeOnBackdropClick = false, + closeOnEscape = true, actions = [] }: PopupProps) { const { t } = useLanguage(); @@ -43,7 +47,7 @@ export function Popup({ // Handle escape key React.useEffect(() => { const handleEscape = (e: KeyboardEvent) => { - if (e.key === 'Escape' && closable) { + if (e.key === 'Escape' && closable && closeOnEscape) { onClose(); } }; @@ -58,13 +62,13 @@ export function Popup({ document.removeEventListener('keydown', handleEscape); document.body.style.overflow = 'unset'; }; - }, [isOpen, closable, onClose]); + }, [isOpen, closable, closeOnEscape, onClose]); if (!isOpen) return null; // Handle backdrop click const handleBackdropClick = (e: React.MouseEvent) => { - if (e.target === e.currentTarget && closable) { + if (e.target === e.currentTarget && closable && closeOnBackdropClick) { onClose(); } }; diff --git a/src/components/UnifiedDataBar/SourcesTab.tsx b/src/components/UnifiedDataBar/SourcesTab.tsx index 8e9da49..7580ec0 100644 --- a/src/components/UnifiedDataBar/SourcesTab.tsx +++ b/src/components/UnifiedDataBar/SourcesTab.tsx @@ -44,6 +44,7 @@ interface UdbFeatureDataSource { label: string; scope: string; neutralize: boolean; + neutralizeFields?: string[]; recordFilter?: Record; } @@ -106,7 +107,8 @@ interface ParentRecordNode { interface SourcesTabProps { context: UdbContext; onSourcesChanged?: () => void; - onSendToChat_FeatureSource?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string }) => void; + onSendToChat_FeatureSource?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string; fieldName?: string }) => void; + onAttachDataSource?: (dsId: string) => void; } /* ─── Icons ──────────────────────────────────────────────────────────── */ @@ -153,28 +155,6 @@ function _getSourceColor(sourceType: string): string { return _SOURCE_COLORS[sourceType] || '#F25843'; } -const _SOURCE_ICONS: Record = { - sharepointFolder: '\uD83D\uDCC1', - sharepoint: '\uD83D\uDCC1', - onedriveFolder: '\u2601\uFE0F', - onedrive: '\u2601\uFE0F', - outlookFolder: '\uD83D\uDCE7', - outlook: '\uD83D\uDCE7', - googleDriveFolder: '\uD83D\uDCC2', - drive: '\uD83D\uDCC2', - gmailFolder: '\uD83D\uDCE8', - gmail: '\uD83D\uDCE8', - ftpFolder: '\uD83D\uDD17', - files: '\uD83D\uDD17', - 'local:ftp': '\uD83D\uDD17', - 'local:jira': '\uD83D\uDD27', - clickup: '\uD83D\uDCCB', -}; - -function _getSourceIcon(sourceType: string): string { - return _SOURCE_ICONS[sourceType] || '\uD83D\uDCC1'; -} - /* ─── Scope / Neutralize constants ───────────────────────────────────── */ const _SCOPE_ORDER: string[] = ['personal', 'featureInstance', 'mandate']; @@ -224,38 +204,19 @@ function _mapFeatureTreeUpdate( })); } -function _findFeatureInstanceMeta( +function _findTableFields( groups: MandateGroupNode[], featureInstanceId: string, -): { mandateLabel: string; instanceLabel: string } | null { + tableName: string, +): string[] { for (const g of groups) { const fc = g.featureConnections.find(f => f.featureInstanceId === featureInstanceId); - if (fc) return { mandateLabel: g.mandateLabel, instanceLabel: fc.label }; + if (fc?.tables) { + const tbl = fc.tables.find(t => t.tableName === tableName); + if (tbl) return tbl.fields; + } } - return null; -} - -function _personalDataSourceHoverTitle(connLabel: string, ds: UdbDataSource): string { - const pathPart = (ds.displayPath && ds.displayPath.trim()) || ds.label || ds.path || ''; - return pathPart ? `${connLabel} / ${pathPart}` : connLabel; -} - -function _featureDataSourceHoverTitle( - meta: { mandateLabel: string; instanceLabel: string } | null, - fds: UdbFeatureDataSource, -): string { - const parts: string[] = []; - if (meta) { - parts.push(meta.mandateLabel, meta.instanceLabel); - } - const labelPart = fds.label && fds.tableName && fds.label !== fds.tableName - ? `${fds.label} (${fds.tableName})` - : (fds.label || fds.tableName); - parts.push(labelPart); - if (fds.objectKey && fds.objectKey !== labelPart && !labelPart.includes(fds.objectKey)) { - parts.push(fds.objectKey); - } - return parts.join(' / '); + return []; } /* ─── Data fetching (module-level) ───────────────────────────────────── */ @@ -340,7 +301,7 @@ function _Spinner(): React.ReactElement { /* ─── Component ──────────────────────────────────────────────────────── */ -const SourcesTab: React.FC = ({ context, onSourcesChanged, onSendToChat_FeatureSource }) => { +const SourcesTab: React.FC = ({ context, onSourcesChanged, onSendToChat_FeatureSource, onAttachDataSource }) => { const { t } = useLanguage(); const _scopeLabel = (scope: string) => ({ personal: t('Persönlich'), @@ -366,6 +327,43 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged, onSe const [loadingFeatures, setLoadingFeatures] = useState(false); const [addingFeatureKey, setAddingFeatureKey] = useState(null); + /* ── Multi-selection state for Browse-Tree ── */ + const [selectedKeys, setSelectedKeys] = useState>(new Set()); + const lastClickedKeyRef = useRef(null); + + const _flattenVisibleKeys = useCallback((nodes: TreeNode[]): string[] => { + const result: string[] = []; + for (const n of nodes) { + result.push(n.key); + if (n.expanded && n.children) { + result.push(..._flattenVisibleKeys(n.children)); + } + } + return result; + }, []); + + const _handleNodeSelect = useCallback((node: TreeNode, e: React.MouseEvent) => { + if (e.ctrlKey || e.metaKey) { + setSelectedKeys(prev => { + const next = new Set(prev); + if (next.has(node.key)) next.delete(node.key); else next.add(node.key); + return next; + }); + lastClickedKeyRef.current = node.key; + } else if (e.shiftKey && lastClickedKeyRef.current) { + const visible = _flattenVisibleKeys(tree); + const a = visible.indexOf(lastClickedKeyRef.current); + const b = visible.indexOf(node.key); + if (a !== -1 && b !== -1) { + const [start, end] = a < b ? [a, b] : [b, a]; + setSelectedKeys(new Set(visible.slice(start, end + 1))); + } + } else { + setSelectedKeys(new Set([node.key])); + lastClickedKeyRef.current = node.key; + } + }, [tree, _flattenVisibleKeys]); + const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); @@ -405,6 +403,7 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged, onSe label: d.label, scope: d.scope || 'personal', neutralize: d.neutralize ?? false, + neutralizeFields: d.neutralizeFields || undefined, recordFilter: d.recordFilter || undefined, })); setFeatureDataSources(list); @@ -489,21 +488,26 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged, onSe }, [instanceId, _updateNode]); /* ── Add as DataSource ── */ - const _addAsDataSource = useCallback(async (node: TreeNode) => { - if (!node.service || !node.connectionId) return; + const _addAsDataSource = useCallback(async (node: TreeNode): Promise => { + if (!node.connectionId) return null; + const sourceType = node.service + ? (_SERVICE_TO_SOURCE_TYPE[node.service] || node.service) + : (node.authority || node.type); setAddingPath(node.key); try { - await api.post(`/api/workspace/${instanceId}/datasources`, { + const res = await api.post(`/api/workspace/${instanceId}/datasources`, { connectionId: node.connectionId, - sourceType: _SERVICE_TO_SOURCE_TYPE[node.service] || node.service, + sourceType, path: node.path || '/', label: node.label, displayPath: node.displayPath || node.label, }); _fetchDataSources(); onSourcesChanged?.(); + return res.data?.id || res.data?.dataSource?.id || null; } catch (err) { console.error('Failed to add data source:', err); + return null; } finally { if (mountedRef.current) setAddingPath(null); } @@ -530,6 +534,38 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged, onSe ); }, [dataSources]); + /* ── Send node to chat: ensure DataSource exists, then attach ── */ + const _sendNodeToChat = useCallback(async (params: { connectionId: string; sourceType: string; path: string; label: string; displayPath?: string }) => { + if (!onAttachDataSource) return; + const expectedSourceType = params.sourceType; + let ds = dataSources.find(d => + d.connectionId === params.connectionId && + d.path === (params.path || '/') && + d.sourceType === expectedSourceType, + ); + if (ds) { + onAttachDataSource(ds.id); + return; + } + try { + const res = await api.post(`/api/workspace/${instanceId}/datasources`, { + connectionId: params.connectionId, + sourceType: params.sourceType, + path: params.path || '/', + label: params.label, + displayPath: params.displayPath || params.label, + }); + const newId = res.data?.id || res.data?.dataSource?.id; + if (newId) { + onAttachDataSource(newId); + _fetchDataSources(); + onSourcesChanged?.(); + } + } catch (err) { + console.error('Failed to send data source to chat:', err); + } + }, [instanceId, dataSources, onAttachDataSource, _fetchDataSources, onSourcesChanged]); + /* ── Scope change (personal data source, optimistic) ── */ const _cyclePersonalScope = useCallback(async (ds: UdbDataSource) => { const newScope = _nextScope(ds.scope); @@ -574,6 +610,21 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged, onSe } }, []); + /* ── Neutralize fields toggle (field-level, optimistic) ── */ + const _toggleNeutralizeField = useCallback(async (fds: UdbFeatureDataSource, fieldName: string) => { + const current = fds.neutralizeFields || []; + const updated = current.includes(fieldName) + ? current.filter(f => f !== fieldName) + : [...current, fieldName]; + const newFields = updated.length > 0 ? updated : undefined; + setFeatureDataSources(prev => prev.map(d => d.id === fds.id ? { ...d, neutralizeFields: newFields } : d)); + try { + await api.patch(`/api/datasources/${fds.id}/neutralize-fields`, { neutralizeFields: newFields || [] }); + } catch { + setFeatureDataSources(prev => prev.map(d => d.id === fds.id ? { ...d, neutralizeFields: fds.neutralizeFields } : d)); + } + }, []); + /* ── Feature Connections: Load Level 1 ── */ const _loadFeatureConnections = useCallback(() => { if (!instanceId) return; @@ -811,68 +862,6 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged, onSe return (
- {/* ── Active Personal Sources ── */} - {dataSources.length > 0 && ( -
-
- {t('Aktive persönliche Quellen')} -
- {[...dataSources].sort((a, b) => { - const aKey = `${a.sourceType}|${a.label || a.path || ''}`; - const bKey = `${b.sourceType}|${b.label || b.path || ''}`; - return aKey.localeCompare(bKey); - }).map(ds => { - const connColor = _getSourceColor(ds.sourceType); - const connNode = tree.find(n => n.connectionId === ds.connectionId); - const connLabel = connNode?.label || ds.connectionId; - const folder = ds.label || ds.path || ds.id; - return ( -
- {_getSourceIcon(ds.sourceType)} - - {connLabel} – {folder} - - - - -
- ); - })} -
-
- )} - {/* ── Browse Sources header ── */}
@@ -909,149 +898,20 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged, onSe onAdd={_addAsDataSource} isAdded={_isAdded} addingPath={addingPath} + dataSources={dataSources} + onCycleScope={_cyclePersonalScope} + onToggleNeutralize={_togglePersonalNeutralize} + onRemoveDs={_removeDatasource} + onSendToChat={_sendNodeToChat} + scopeCycleTitle={_scopeCycleTitle} + selectedKeys={selectedKeys} + onSelect={_handleNodeSelect} /> ))} {/* ── Divider ── */}
- {/* ── Active Feature Sources (grouped by parent record) ── */} - {featureDataSources.length > 0 && ( -
-
- {t('Aktive Feature-Quellen')} -
- {(() => { - const sorted = [...featureDataSources].sort((a, b) => (a.label || a.tableName || '').localeCompare(b.label || b.tableName || '')); - const grouped: { key: string; label: string; items: UdbFeatureDataSource[] }[] = []; - const standalone: UdbFeatureDataSource[] = []; - - for (const fds of sorted) { - if (fds.recordFilter && Object.keys(fds.recordFilter).length > 0) { - const filterKey = `${fds.featureInstanceId}|${JSON.stringify(fds.recordFilter)}`; - let group = grouped.find(g => g.key === filterKey); - if (!group) { - const parentLabel = fds.label.includes(':') ? fds.label.split(':')[1]?.trim() : fds.label; - const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId); - group = { key: filterKey, label: `${meta?.instanceLabel || fds.featureCode} – ${parentLabel}`, items: [] }; - grouped.push(group); - } - group.items.push(fds); - } else { - standalone.push(fds); - } - } - - return ( - <> - {grouped.map(group => ( -
-
- {'\uD83D\uDCCB'} - - {group.label} - - -
- {group.items.map(fds => { - const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId); - return ( -
- - {getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDCC4'} - - - {fds.tableName} - - - - -
- ); - })} -
- ))} - {standalone.map(fds => { - const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId); - const fdsConnLabel = meta?.instanceLabel || fds.tableName; - return ( -
- - {getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'} - - - {fdsConnLabel} – {fds.tableName} - - - - -
- ); - })} - - ); - })()} -
-
- )} - {/* ── Feature Data header ── */}
@@ -1096,6 +956,12 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged, onSe loadingParentGroup={loadingParentGroup} addingParentKey={addingParentKey} onSendToChat={onSendToChat_FeatureSource} + featureDataSources={featureDataSources} + onCycleScope={_cycleFeatureScope} + onToggleNeutralize={_toggleFeatureNeutralize} + onToggleNeutralizeField={_toggleNeutralizeField} + onRemoveFds={_removeFeatureDataSource} + featureTree={featureTree} /> ))}
@@ -1104,6 +970,15 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged, onSe /* ─── TreeNodeView (recursive) ───────────────────────────────────────── */ +function _findDs(dataSources: UdbDataSource[], node: TreeNode): UdbDataSource | undefined { + const expectedSourceType = node.service ? (_SERVICE_TO_SOURCE_TYPE[node.service] || node.service) : undefined; + return dataSources.find(ds => + ds.connectionId === node.connectionId && + ds.path === (node.path || '/') && + (!expectedSourceType || ds.sourceType === expectedSourceType), + ); +} + interface _TreeNodeViewProps { node: TreeNode; depth: number; @@ -1111,10 +986,22 @@ interface _TreeNodeViewProps { onAdd: (node: TreeNode) => void; isAdded: (connectionId: string, service: string | undefined, path: string | undefined) => boolean; addingPath: string | null; + dataSources: UdbDataSource[]; + onCycleScope: (ds: UdbDataSource) => void; + onToggleNeutralize: (ds: UdbDataSource) => void; + onRemoveDs: (dsId: string) => void; + onSendToChat?: (params: { connectionId: string; sourceType: string; path: string; label: string; displayPath?: string }) => void; + scopeCycleTitle: (scope: string) => string; + selectedKeys: Set; + onSelect: (node: TreeNode, e: React.MouseEvent) => void; + inheritedScope?: string; + inheritedNeutralize?: boolean; } const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({ node, depth, onToggle, onAdd, isAdded, addingPath, + dataSources, onCycleScope, onToggleNeutralize, onRemoveDs, onSendToChat, scopeCycleTitle, + selectedKeys, onSelect, inheritedScope, inheritedNeutralize, }) => { const { t } = useLanguage(); const [hovered, setHovered] = useState(false); @@ -1122,16 +1009,60 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({ const chevron = hasChildren ? (node.expanded ? '\u25BE' : '\u25B8') : '\u00A0\u00A0'; - const canAdd = node.type === 'folder' || node.type === 'service'; - const alreadyAdded = canAdd && isAdded(node.connectionId, node.service, node.path); + const ds = _findDs(dataSources, node); + const alreadyAdded = isAdded(node.connectionId, node.service, node.path); const isAdding = addingPath === node.key; + const effectiveScope = ds?.scope ?? inheritedScope; + const effectiveNeutralize = ds?.neutralize ?? inheritedNeutralize ?? false; + const childInheritedScope = ds?.scope ?? inheritedScope; + const childInheritedNeutralize = ds?.neutralize ?? inheritedNeutralize; + + const _dragPayload = { + connectionId: node.connectionId, + sourceType: node.service ? (_SERVICE_TO_SOURCE_TYPE[node.service] || node.service) : node.authority || '', + path: node.path || '/', + label: node.label, + displayPath: node.displayPath || node.label, + nodeType: node.type, + }; + + const _chatPayload = { + connectionId: node.connectionId, + sourceType: node.service ? (_SERVICE_TO_SOURCE_TYPE[node.service] || node.service) : node.authority || '', + path: node.path || '/', + label: node.label, + displayPath: node.displayPath || node.label, + }; + + const connColor = ds ? _getSourceColor(ds.sourceType) : undefined; + const isSelected = selectedKeys.has(node.key); + return (
{ if (hasChildren) onToggle(node); }} + onClick={(e) => { + if (e.ctrlKey || e.metaKey || e.shiftKey) { + e.stopPropagation(); + onSelect(node, e); + } else if (hasChildren) { + onToggle(node); + } + }} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} + draggable + onDragStart={(e) => { + e.stopPropagation(); + if (selectedKeys.size > 1 && isSelected) { + const items = Array.from(selectedKeys).map(k => ({ key: k, ...(_dragPayload) })); + e.dataTransfer.setData('application/datasource', JSON.stringify(items)); + } else { + e.dataTransfer.setData('application/datasource', JSON.stringify(_dragPayload)); + } + e.dataTransfer.setData('text/plain', node.label); + e.dataTransfer.effectAllowed = 'copy'; + }} style={{ display: 'flex', alignItems: 'center', @@ -1142,7 +1073,13 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({ paddingBottom: 3, cursor: hasChildren ? 'pointer' : 'default', borderRadius: 3, - background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent', + background: ds + ? (hovered ? `${connColor}28` : `${connColor}10`) + : isSelected + ? 'var(--selection-bg, rgba(242, 88, 67, 0.12))' + : (hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent'), + borderLeft: ds ? `3px solid ${connColor}` : undefined, + outline: isSelected && !ds ? '1px solid var(--primary-color, #F25843)' : undefined, transition: 'background 0.1s', userSelect: 'none', }} @@ -1155,11 +1092,78 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12, - fontWeight: node.type === 'connection' ? 600 : 400, + fontWeight: (node.type === 'connection' || ds) ? 600 : 400, }}> {node.label} - {canAdd && hovered && !alreadyAdded && ( + + {/* Chat-Senden: always visible */} + + + {/* Scope: own DS → clickable, inherited → dimmed static */} + {ds ? ( + + ) : ( + + {_SCOPE_ICONS[effectiveScope || 'personal']} + + )} + + {/* Neutralize: own DS → clickable, inherited → dimmed static */} + {ds ? ( + + ) : ( + + {'\uD83D\uDD12'} + + )} + + {/* Remove: only when DS exists */} + {ds && ( + + )} + + {/* Add button: on hover when not yet added */} + {hovered && !alreadyAdded && !ds && ( )} - {canAdd && alreadyAdded && ( - - {'\u2713'} - - )}
{node.expanded && node.children && node.children.length > 0 && ( @@ -1193,6 +1192,16 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({ onAdd={onAdd} isAdded={isAdded} addingPath={addingPath} + dataSources={dataSources} + onCycleScope={onCycleScope} + onToggleNeutralize={onToggleNeutralize} + onRemoveDs={onRemoveDs} + onSendToChat={onSendToChat} + scopeCycleTitle={scopeCycleTitle} + selectedKeys={selectedKeys} + onSelect={onSelect} + inheritedScope={childInheritedScope} + inheritedNeutralize={childInheritedNeutralize} /> ))}
@@ -1209,7 +1218,16 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({ /* ─── MandateGroupView (mandate + feature instances) ─────────────────── */ -interface _MandateGroupViewProps { +interface _FdsActionProps { + featureDataSources: UdbFeatureDataSource[]; + onCycleScope: (fds: UdbFeatureDataSource) => void; + onToggleNeutralize: (fds: UdbFeatureDataSource) => void; + onToggleNeutralizeField: (fds: UdbFeatureDataSource, fieldName: string) => void; + onRemoveFds: (fdsId: string) => void; + featureTree: MandateGroupNode[]; +} + +interface _MandateGroupViewProps extends _FdsActionProps { group: MandateGroupNode; onToggleGroup: (mandateId: string) => void; onToggleFeature: (node: FeatureConnectionNode) => void; @@ -1223,13 +1241,15 @@ interface _MandateGroupViewProps { expandedParentGroups: Set; loadingParentGroup: string | null; addingParentKey: string | null; - onSendToChat?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string }) => void; + onSendToChat?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string; fieldName?: string }) => void; } const _MandateGroupView: React.FC<_MandateGroupViewProps> = ({ group, onToggleGroup, onToggleFeature, onAddTable, isTableAdded, addingKey, onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded, expandedParentGroups, loadingParentGroup, addingParentKey, onSendToChat, + featureDataSources, onCycleScope, onToggleNeutralize, onToggleNeutralizeField, + onRemoveFds, featureTree, }) => { const [hovered, setHovered] = useState(false); const chevron = group.expanded ? '\u25BE' : '\u25B8'; @@ -1274,6 +1294,12 @@ const _MandateGroupView: React.FC<_MandateGroupViewProps> = ({ loadingParentGroup={loadingParentGroup} addingParentKey={addingParentKey} onSendToChat={onSendToChat} + featureDataSources={featureDataSources} + onCycleScope={onCycleScope} + onToggleNeutralize={onToggleNeutralize} + onToggleNeutralizeField={onToggleNeutralizeField} + onRemoveFds={onRemoveFds} + featureTree={featureTree} /> ))}
@@ -1284,7 +1310,7 @@ const _MandateGroupView: React.FC<_MandateGroupViewProps> = ({ /* ─── FeatureNodeView (feature instance + tables) ────────────────────── */ -interface _FeatureNodeViewProps { +interface _FeatureNodeViewProps extends _FdsActionProps { node: FeatureConnectionNode; onToggle: (node: FeatureConnectionNode) => void; onAddTable: (node: FeatureConnectionNode, table: FeatureTableNode) => void; @@ -1297,18 +1323,24 @@ interface _FeatureNodeViewProps { expandedParentGroups: Set; loadingParentGroup: string | null; addingParentKey: string | null; - onSendToChat?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string }) => void; + onSendToChat?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string; fieldName?: string }) => void; } const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({ node, onToggle, onAddTable, isTableAdded, addingKey, onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded, expandedParentGroups, loadingParentGroup, addingParentKey, onSendToChat, + featureDataSources, onCycleScope, onToggleNeutralize, onToggleNeutralizeField, + onRemoveFds, featureTree, }) => { const { t } = useLanguage(); const [hovered, setHovered] = useState(false); const chevron = node.expanded ? '\u25BE' : '\u25B8'; + const wildcardFds = featureDataSources.find( + f => f.featureInstanceId === node.featureInstanceId && f.tableName === '*' && !f.recordFilter, + ); + const parentTables = (node.tables || []).filter(t => t.isParent); const standaloneTables = (node.tables || []).filter(t => !t.isParent && !t.parentTable); @@ -1318,11 +1350,27 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({ onClick={() => onToggle(node)} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} + draggable + onDragStart={(e) => { + e.stopPropagation(); + const payload = JSON.stringify({ + featureInstanceId: node.featureInstanceId, + featureCode: node.featureCode, + objectKey: `data.feature.${node.featureCode}.*`, + label: node.label, + }); + e.dataTransfer.setData('application/feature-source', payload); + e.dataTransfer.setData('text/plain', node.label); + e.dataTransfer.effectAllowed = 'copy'; + }} style={{ display: 'flex', alignItems: 'center', gap: 4, paddingLeft: 4, paddingRight: 4, paddingTop: 3, paddingBottom: 3, cursor: 'pointer', borderRadius: 3, - background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent', + background: wildcardFds + ? (hovered ? '#ede7f6' : '#7b1fa208') + : (hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent'), + borderLeft: wildcardFds ? '3px solid #7b1fa2' : undefined, transition: 'background 0.1s', userSelect: 'none', }} > @@ -1338,11 +1386,12 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({ {node.tableCount} {t('Tabellen')} - {hovered && onSendToChat && ( + + {(wildcardFds || hovered) && ( )} + + {wildcardFds && ( + + )} + {wildcardFds && ( + + )} + {wildcardFds && ( + + )} + + {!wildcardFds && hovered && ( + + )}
{node.expanded && node.tables && node.tables.length > 0 && ( @@ -1388,22 +1488,44 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({ onAddRecord={(record) => onAddParentRecord(node, record, node.tables!)} isRecordAdded={(recordId) => isParentRecordAdded(node.featureInstanceId, pt.tableName, recordId)} addingParentKey={addingParentKey} + onSendToChat={onSendToChat} + featureDataSources={featureDataSources} + onCycleScope={onCycleScope} + onToggleNeutralize={onToggleNeutralize} + onToggleNeutralizeField={onToggleNeutralizeField} + onRemoveFds={onRemoveFds} + featureTree={featureTree} + inheritedScope={wildcardFds?.scope} + inheritedNeutralize={wildcardFds?.neutralize} /> ); })} {/* Standalone tables (not part of any hierarchy) */} - {standaloneTables.map(table => ( - <_FeatureTableRow - key={table.objectKey} - featureNode={node} - table={table} - onAdd={onAddTable} - isAdded={isTableAdded(node.featureInstanceId, table.tableName)} - isAdding={addingKey === `${node.featureInstanceId}-${table.tableName}`} - onSendToChat={onSendToChat} - /> - ))} + {standaloneTables.map(table => { + const fds = featureDataSources.find( + f => f.featureInstanceId === node.featureInstanceId && f.tableName === table.tableName && !f.recordFilter, + ); + return ( + <_FeatureTableRow + key={table.objectKey} + featureNode={node} + table={table} + onAdd={onAddTable} + isAdded={isTableAdded(node.featureInstanceId, table.tableName)} + isAdding={addingKey === `${node.featureInstanceId}-${table.tableName}`} + onSendToChat={onSendToChat} + fds={fds} + onCycleScope={onCycleScope} + onToggleNeutralize={onToggleNeutralize} + onToggleNeutralizeField={onToggleNeutralizeField} + onRemoveFds={onRemoveFds} + featureTree={featureTree} + inheritedScope={wildcardFds?.scope} + inheritedNeutralize={wildcardFds?.neutralize} + /> + ); + })}
)} @@ -1424,70 +1546,274 @@ interface _FeatureTableRowProps { onAdd: (node: FeatureConnectionNode, table: FeatureTableNode) => void; isAdded: boolean; isAdding: boolean; - onSendToChat?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string }) => void; + onSendToChat?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string; fieldName?: string }) => void; + fds?: UdbFeatureDataSource; + onCycleScope?: (fds: UdbFeatureDataSource) => void; + onToggleNeutralize?: (fds: UdbFeatureDataSource) => void; + onToggleNeutralizeField?: (fds: UdbFeatureDataSource, fieldName: string) => void; + onRemoveFds?: (fdsId: string) => void; + featureTree?: MandateGroupNode[]; + inheritedScope?: string; + inheritedNeutralize?: boolean; } const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({ featureNode, table, onAdd, isAdded, isAdding, onSendToChat, + fds, onCycleScope, onToggleNeutralize, onToggleNeutralizeField, + onRemoveFds, featureTree, inheritedScope, inheritedNeutralize, }) => { const { t } = useLanguage(); const [hovered, setHovered] = useState(false); + const [fieldsExpanded, setFieldsExpanded] = useState(false); const tableLabel = table.label || table.tableName; + const effectiveScope = fds?.scope ?? inheritedScope; + const effectiveNeutralize = fds?.neutralize ?? inheritedNeutralize ?? false; + const _chatPayload = { + featureInstanceId: featureNode.featureInstanceId, + featureCode: featureNode.featureCode, + tableName: table.tableName, + objectKey: table.objectKey, + label: table.label || table.tableName, + }; + + const resolvedFields = featureTree ? _findTableFields(featureTree, featureNode.featureInstanceId, table.tableName) : table.fields; + const neutralizedCount = fds?.neutralizeFields?.length ?? 0; + + return ( +
+
setHovered(true)} + onMouseLeave={() => setHovered(false)} + draggable + onDragStart={(e) => { + e.stopPropagation(); + e.dataTransfer.setData('application/feature-source', JSON.stringify(_chatPayload)); + e.dataTransfer.setData('text/plain', tableLabel); + e.dataTransfer.effectAllowed = 'copy'; + }} + style={{ + display: 'flex', alignItems: 'center', gap: 4, + paddingLeft: 36, paddingRight: 4, paddingTop: 3, paddingBottom: 3, + borderRadius: 3, + background: fds + ? (hovered ? '#ede7f6' : '#7b1fa208') + : (hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent'), + transition: 'background 0.1s', userSelect: 'none', + }} + title={`${table.tableName}: ${table.fields.join(', ')}`} + > + { e.stopPropagation(); if (resolvedFields.length > 0) setFieldsExpanded(prev => !prev); }} + style={{ fontSize: 10, color: '#888', width: 12, textAlign: 'center', flexShrink: 0, cursor: resolvedFields.length > 0 ? 'pointer' : 'default' }} + > + {resolvedFields.length > 0 ? (fieldsExpanded ? '\u25BE' : '\u25B8') : '\u00A0\u00A0'} + + {'\uD83D\uDCC1'} + + {tableLabel} + {neutralizedCount > 0 && ( + ({neutralizedCount} {t('Felder')}) + )} + + + {(fds || hovered) && ( + + )} + + {fds && onCycleScope && ( + + )} + {fds && onToggleNeutralize && ( + + )} + {fds && onRemoveFds && ( + + )} + + {/* Inherited scope/neutralize indicators (no own FDS) */} + {!fds && effectiveScope && ( + + {_SCOPE_ICONS[effectiveScope] || _SCOPE_ICONS.personal} + + )} + {!fds && effectiveNeutralize && ( + + {'\uD83D\uDD12'} + + )} + + {!fds && hovered && !isAdded && ( + + )} +
+ + {/* Expandable field sub-nodes */} + {fieldsExpanded && resolvedFields.length > 0 && ( +
+ {resolvedFields.map(field => { + const isNeutralized = (fds?.neutralizeFields || []).includes(field); + return ( + <_FeatureFieldRow + key={field} + featureNode={featureNode} + table={table} + fieldName={field} + isNeutralized={isNeutralized || effectiveNeutralize} + fds={fds} + onToggleNeutralizeField={onToggleNeutralizeField} + onSendToChat={onSendToChat} + inheritedScope={fds?.scope ?? inheritedScope} + /> + ); + })} +
+ )} +
+ ); +}; + +/* ─── FeatureFieldRow (single field under a table) ────────────────────── */ + +interface _FeatureFieldRowProps { + featureNode: FeatureConnectionNode; + table: FeatureTableNode; + fieldName: string; + isNeutralized: boolean; + fds?: UdbFeatureDataSource; + onToggleNeutralizeField?: (fds: UdbFeatureDataSource, fieldName: string) => void; + onSendToChat?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string; fieldName?: string }) => void; + inheritedScope?: string; +} + +const _FeatureFieldRow: React.FC<_FeatureFieldRowProps> = ({ + featureNode, table, fieldName, isNeutralized, fds, onToggleNeutralizeField, onSendToChat, inheritedScope, +}) => { + const { t } = useLanguage(); + const [hovered, setHovered] = useState(false); + const _chatPayload = { + featureInstanceId: featureNode.featureInstanceId, + featureCode: featureNode.featureCode, + tableName: table.tableName, + objectKey: table.objectKey, + label: `${table.label || table.tableName}.${fieldName}`, + fieldName, + }; + return (
setHovered(true)} onMouseLeave={() => setHovered(false)} + draggable + onDragStart={(e) => { + e.stopPropagation(); + e.dataTransfer.setData('application/feature-source', JSON.stringify(_chatPayload)); + e.dataTransfer.setData('text/plain', `${table.tableName}.${fieldName}`); + e.dataTransfer.effectAllowed = 'copy'; + }} style={{ display: 'flex', alignItems: 'center', gap: 4, - paddingLeft: 36, paddingRight: 4, paddingTop: 3, paddingBottom: 3, + paddingLeft: 56, paddingRight: 4, paddingTop: 2, paddingBottom: 2, borderRadius: 3, - background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent', + background: isNeutralized + ? (hovered ? '#f3e5f5' : '#f3e5f508') + : (hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent'), transition: 'background 0.1s', userSelect: 'none', + fontSize: 11, }} - title={`${table.tableName}: ${table.fields.join(', ')}`} > - {'\uD83D\uDCC1'} - - {tableLabel} + {'\u2514'} + + {fieldName} - {hovered && onSendToChat && ( + + {(fds || hovered) && ( )} - {hovered && !isAdded && ( + + {fds && onToggleNeutralizeField && ( )} - {isAdded && ( - - {'\u2713'} + + {inheritedScope && ( + + {_SCOPE_ICONS[inheritedScope] || _SCOPE_ICONS.personal} )}
@@ -1496,7 +1822,7 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({ /* ─── ParentGroupView (parent table → parent records) ────────────────── */ -interface _ParentGroupViewProps { +interface _ParentGroupViewProps extends _FdsActionProps { featureNode: FeatureConnectionNode; parentTable: FeatureTableNode; label: string; @@ -1510,11 +1836,17 @@ interface _ParentGroupViewProps { onAddRecord: (record: ParentRecordNode) => void; isRecordAdded: (recordId: string) => boolean; addingParentKey: string | null; + onSendToChat?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string; fieldName?: string }) => void; + inheritedScope?: string; + inheritedNeutralize?: boolean; } const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({ featureNode, parentTable: _parentTable, label, expanded, loading, records, childTables, allTables, onToggleGroup, onToggleRecord, onAddRecord, isRecordAdded, addingParentKey, + onSendToChat, featureDataSources, onCycleScope, onToggleNeutralize, + onToggleNeutralizeField: _onToggleNeutralizeField, onRemoveFds, + featureTree: _featureTreeRef, inheritedScope, inheritedNeutralize, }) => { const { t } = useLanguage(); const [hovered, setHovered] = useState(false); @@ -1550,19 +1882,32 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({ {expanded && records && records.length > 0 && (
- {records.map(record => ( - <_ParentRecordRow - key={record.id} - featureNode={featureNode} - record={record} - childTables={childTables} - allTables={allTables} - onToggle={() => onToggleRecord(record.id)} - onAdd={() => onAddRecord(record)} - isAdded={isRecordAdded(record.id)} - isAdding={addingParentKey === `${featureNode.featureInstanceId}-parent-${record.id}`} - /> - ))} + {records.map(record => { + const recordFds = featureDataSources.find( + f => f.featureInstanceId === featureNode.featureInstanceId + && f.recordFilter?.id === record.id, + ); + return ( + <_ParentRecordRow + key={record.id} + featureNode={featureNode} + record={record} + childTables={childTables} + allTables={allTables} + onToggle={() => onToggleRecord(record.id)} + onAdd={() => onAddRecord(record)} + isAdded={isRecordAdded(record.id)} + isAdding={addingParentKey === `${featureNode.featureInstanceId}-parent-${record.id}`} + onSendToChat={onSendToChat} + fds={recordFds} + onCycleScope={onCycleScope} + onToggleNeutralize={onToggleNeutralize} + onRemoveFds={onRemoveFds} + inheritedScope={inheritedScope} + inheritedNeutralize={inheritedNeutralize} + /> + ); + })}
)} @@ -1586,11 +1931,20 @@ interface _ParentRecordRowProps { onAdd: () => void; isAdded: boolean; isAdding: boolean; + onSendToChat?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string; fieldName?: string }) => void; + fds?: UdbFeatureDataSource; + onCycleScope?: (fds: UdbFeatureDataSource) => void; + onToggleNeutralize?: (fds: UdbFeatureDataSource) => void; + onRemoveFds?: (fdsId: string) => void; + inheritedScope?: string; + inheritedNeutralize?: boolean; } const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({ - featureNode: _featureNode, record, childTables, allTables: _allTables, + featureNode, record, childTables, allTables: _allTables, onToggle, onAdd, isAdded, isAdding, + onSendToChat, fds, onCycleScope, onToggleNeutralize, onRemoveFds, + inheritedScope, inheritedNeutralize, }) => { const { t } = useLanguage(); const [hovered, setHovered] = useState(false); @@ -1602,11 +1956,26 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({ onClick={onToggle} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} + draggable + onDragStart={(e) => { + e.stopPropagation(); + const payload = JSON.stringify({ + featureInstanceId: featureNode.featureInstanceId, + featureCode: featureNode.featureCode, + objectKey: `data.feature.${featureNode.featureCode}.${record.tableName || '*'}`, + label: record.displayLabel, + }); + e.dataTransfer.setData('application/feature-source', payload); + e.dataTransfer.setData('text/plain', record.displayLabel); + e.dataTransfer.effectAllowed = 'copy'; + }} style={{ display: 'flex', alignItems: 'center', gap: 4, paddingLeft: 44, paddingRight: 4, paddingTop: 3, paddingBottom: 3, cursor: 'pointer', borderRadius: 3, - background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent', + background: fds + ? (hovered ? '#ede7f6' : '#7b1fa208') + : (hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent'), transition: 'background 0.1s', userSelect: 'none', }} title={Object.entries(record.fields).map(([k, v]) => `${k}: ${v}`).join(', ')} @@ -1615,10 +1984,85 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({ {chevron} {'\uD83D\uDCCB'} - + {record.displayLabel} - {hovered && !isAdded && ( + + {/* Chat-Senden: always visible when fds, hover-only otherwise */} + {(fds || hovered) && ( + + )} + + {/* FDS inline actions */} + {fds && onCycleScope && ( + + )} + {fds && onToggleNeutralize && ( + + )} + {fds && onRemoveFds && ( + + )} + + {/* Inherited scope/neutralize indicators */} + {!fds && inheritedScope && ( + + {_SCOPE_ICONS[inheritedScope] || _SCOPE_ICONS.personal} + + )} + {!fds && (inheritedNeutralize ?? false) && ( + + {'\uD83D\uDD12'} + + )} + + {/* Add button (only when not yet added) */} + {!fds && hovered && !isAdded && ( )} - {isAdded && ( - - {'\u2713'} - - )}
{record.expanded && ( diff --git a/src/components/UnifiedDataBar/UnifiedDataBar.tsx b/src/components/UnifiedDataBar/UnifiedDataBar.tsx index 0e94c98..b872405 100644 --- a/src/components/UnifiedDataBar/UnifiedDataBar.tsx +++ b/src/components/UnifiedDataBar/UnifiedDataBar.tsx @@ -43,6 +43,7 @@ interface UnifiedDataBarProps { onSourcesChanged?: () => void; onSendToChat_Files?: (items: AddToChat_FileItem[]) => void; onSendToChat_FeatureSource?: (params: AddToChat_FeatureSource) => void; + onAttachDataSource?: (dsId: string) => void; className?: string; } @@ -70,6 +71,7 @@ const UnifiedDataBar: React.FC = ({ onSourcesChanged, onSendToChat_Files, onSendToChat_FeatureSource, + onAttachDataSource, className, }) => { const { t } = useLanguage(); @@ -121,6 +123,7 @@ const UnifiedDataBar: React.FC = ({ context={context} onSourcesChanged={onSourcesChanged} onSendToChat_FeatureSource={onSendToChat_FeatureSource} + onAttachDataSource={onAttachDataSource} /> )}
diff --git a/src/components/UnifiedDataBar/index.ts b/src/components/UnifiedDataBar/index.ts index 83b7dfc..5789264 100644 --- a/src/components/UnifiedDataBar/index.ts +++ b/src/components/UnifiedDataBar/index.ts @@ -1,3 +1,3 @@ export { default as UnifiedDataBar } from './UnifiedDataBar'; -export type { UdbContext, UdbTab } from './UnifiedDataBar'; +export type { UdbContext, UdbTab, AddToChat_FileItem, AddToChat_FeatureSource } from './UnifiedDataBar'; export { useUdlContext } from './useUdlContext'; diff --git a/src/pages/ComplianceAuditPage.tsx b/src/pages/ComplianceAuditPage.tsx index 10a6a52..d38f190 100644 --- a/src/pages/ComplianceAuditPage.tsx +++ b/src/pages/ComplianceAuditPage.tsx @@ -815,8 +815,8 @@ export const ComplianceAuditPage: React.FC = () => { {/* ── Content View Modal ── */} {contentModal && ( -
setContentModal(null)}> -
e.stopPropagation()}> +
+

{t('AI-Audit Inhalt')}

diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index c4feb60..7795ba4 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -8,7 +8,7 @@ import { PENDING_INVITATION_KEY } from './InvitePage'; import OnboardingWizard from '../components/OnboardingWizard'; import styles from './Login.module.css'; - +import { LanguageSelector } from '../components/UiComponents/LanguageSelector'; import { useLanguage } from '../providers/language/LanguageContext'; @@ -131,6 +131,9 @@ function Login() { return (
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
= { teamsbot: , workspace: , commcoach: , + trustee: , }; /** Fallback when GET /store/features omits description (German i18n keys). */ @@ -27,6 +28,7 @@ const STORE_FEATURE_DESCRIPTION_FALLBACK: Record = { teamsbot: 'Integriere einen AI-Bot in deine Microsoft Teams Meetings und Channels.', workspace: 'Nutze den gemeinsamen AI Workspace: Chats, Tools und Kontext pro Instanz.', commcoach: 'CommCoach: Kommunikation trainieren mit KI-gestütztem Coaching und Feedback.', + trustee: 'Trustee: Intelligentes Dokumentenmanagement mit KI-gestützter Analyse und Verarbeitung.', }; function _storeCardDescription(feature: StoreFeature): string { diff --git a/src/pages/admin/AdminFeatureAccessPage.tsx b/src/pages/admin/AdminFeatureAccessPage.tsx index 0f18c62..f4343ca 100644 --- a/src/pages/admin/AdminFeatureAccessPage.tsx +++ b/src/pages/admin/AdminFeatureAccessPage.tsx @@ -15,7 +15,6 @@ import { FaPlus, FaSync, FaCube, FaBuilding, FaCogs, FaEdit } from 'react-icons/ import { useToast } from '../../contexts/ToastContext'; import api from '../../api'; import { ChatbotConfigSection } from './ChatbotConfigSection'; -import { DropdownSelect } from '../../components/UiComponents/DropdownSelect'; import { TextField } from '../../components/UiComponents/TextField'; import styles from './Admin.module.css'; @@ -512,8 +511,8 @@ export const AdminFeatureAccessPage: React.FC = () => { {/* Create Instance Modal */} {showCreateModal && ( -
setShowCreateModal(false)}> -
e.stopPropagation()}> +
+

{t('Neue Feature-Instanz erstellen')}

) : (
- {/* Feature Code Selector - Required for chatbot config */} + {/* Feature Code Selector — buttons instead of dropdown */}
- ({ - id: f.code, - label: f.label || f.code, - value: f.code - }))} - selectedItemId={createFeatureCode} - onSelect={(item) => { - const selectedCode = item?.value || ''; - setCreateFeatureCode(selectedCode); - // Reset chatbot config when switching - setChatbotConnectors(['preprocessor']); - setChatbotSystemPrompt(''); - setChatbotEnableWebResearch(true); - setChatbotAllowedProviders([]); - }} - placeholder={t('Feature-Auswahl erforderlich')} - className={styles.configSelect} - /> - {!createFeatureCode && ( -

- {t('Bitte wählen Sie ein Feature aus, um fortzufahren.')} -

- )} +
+ {features.map(f => ( + + ))} +
{/* Chatbot Configuration Title - Show when chatbot is selected */} @@ -634,8 +636,8 @@ export const AdminFeatureAccessPage: React.FC = () => { {/* Edit Instance Modal */} {showEditModal && editingInstance && ( -
{ setShowEditModal(false); setEditingInstance(null); }}> -
e.stopPropagation()}> +
+

{t('Feature-Instanz bearbeiten')}

) : ( { {/* Add User Modal */} {showAddModal && ( -
setShowAddModal(false)}> -
e.stopPropagation()}> +
+

{t('Benutzer zum Mandanten hinzufügen')}

{showAddModal && ( -
setShowAddModal(false)}> -
e.stopPropagation()}> +
+

{t('Benutzer hinzufügen')}

{editingFile && ( -
setEditingFile(null)}> -
e.stopPropagation()}> +
+

{t('Datei bearbeiten')}

diff --git a/src/pages/basedata/PromptsPage.tsx b/src/pages/basedata/PromptsPage.tsx index 2fa7740..c733f11 100644 --- a/src/pages/basedata/PromptsPage.tsx +++ b/src/pages/basedata/PromptsPage.tsx @@ -230,8 +230,8 @@ export const PromptsPage: React.FC = () => { {/* Create Modal */} {showCreateModal && ( -
setShowCreateModal(false)}> -
e.stopPropagation()}> +
+

{t('Neuer Prompt')}