+
{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