From 66708d674341c8e0ce8f7e7fbb6b965fc80103ea Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Tue, 31 Mar 2026 23:41:02 +0200 Subject: [PATCH] fixed data source --- src/components/UnifiedDataBar/SourcesTab.tsx | 538 +++++++++++++++++-- src/pages/views/workspace/useWorkspace.ts | 1 + 2 files changed, 491 insertions(+), 48 deletions(-) diff --git a/src/components/UnifiedDataBar/SourcesTab.tsx b/src/components/UnifiedDataBar/SourcesTab.tsx index 2370209..57d6485 100644 --- a/src/components/UnifiedDataBar/SourcesTab.tsx +++ b/src/components/UnifiedDataBar/SourcesTab.tsx @@ -42,6 +42,7 @@ interface UdbFeatureDataSource { label: string; scope: string; neutralize: boolean; + recordFilter?: Record; } interface TreeNode { @@ -69,6 +70,7 @@ interface FeatureConnectionNode { expanded: boolean; loading: boolean; tables: FeatureTableNode[] | null; + parentRecords: Record; } interface MandateGroupNode { @@ -83,6 +85,18 @@ interface FeatureTableNode { tableName: string; label: Record; fields: string[]; + isParent?: boolean; + parentTable?: string; + parentKey?: string; + displayFields?: string[]; +} + +interface ParentRecordNode { + id: string; + displayLabel: string; + fields: Record; + tableName: string; + expanded: boolean; } /* ─── Props ──────────────────────────────────────────────────────────── */ @@ -386,6 +400,7 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => label: d.label, scope: d.scope || 'personal', neutralize: d.neutralize ?? false, + recordFilter: d.recordFilter || undefined, })); setFeatureDataSources(list); }) @@ -576,6 +591,7 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => expanded: false, loading: false, tables: null, + parentRecords: {}, })), }))); }) @@ -615,6 +631,10 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => tableName: t.tableName, label: t.label || {}, fields: t.fields || [], + isParent: t.isParent || false, + parentTable: t.parentTable || undefined, + parentKey: t.parentKey || undefined, + displayFields: t.displayFields || undefined, })); if (mountedRef.current) { setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({ @@ -669,6 +689,119 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => ); }, [featureDataSources]); + /* ── Parent groups: expand/collapse + load records ── */ + const [expandedParentGroups, setExpandedParentGroups] = useState>(new Set()); + const [loadingParentGroup, setLoadingParentGroup] = useState(null); + const [addingParentKey, setAddingParentKey] = useState(null); + + const _toggleParentGroup = useCallback(async (node: FeatureConnectionNode, parentTableName: string) => { + const groupKey = `${node.featureInstanceId}-${parentTableName}`; + + if (expandedParentGroups.has(groupKey)) { + setExpandedParentGroups(prev => { const next = new Set(prev); next.delete(groupKey); return next; }); + return; + } + + setExpandedParentGroups(prev => new Set(prev).add(groupKey)); + + if (node.parentRecords[parentTableName]) return; + + setLoadingParentGroup(groupKey); + try { + const res = await api.get( + `/api/workspace/${instanceId}/feature-connections/${node.featureInstanceId}/parent-objects/${parentTableName}`, + ); + const records: ParentRecordNode[] = (res.data.parentObjects || []).map((r: any) => ({ + id: r.id, + displayLabel: r.displayLabel || r.id, + fields: r.fields || {}, + tableName: parentTableName, + expanded: false, + })); + if (mountedRef.current) { + setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({ + ...n, + parentRecords: { ...n.parentRecords, [parentTableName]: records }, + }))); + } + } catch { + if (mountedRef.current) { + setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({ + ...n, + parentRecords: { ...n.parentRecords, [parentTableName]: [] }, + }))); + } + } finally { + if (mountedRef.current) setLoadingParentGroup(null); + } + }, [instanceId, expandedParentGroups]); + + const _toggleParentRecord = useCallback((featureInstanceId: string, parentTableName: string, recordId: string) => { + setFeatureTree(prev => _mapFeatureTreeUpdate(prev, featureInstanceId, n => ({ + ...n, + parentRecords: { + ...n.parentRecords, + [parentTableName]: (n.parentRecords[parentTableName] || []).map(r => + r.id === recordId ? { ...r, expanded: !r.expanded } : r, + ), + }, + }))); + }, []); + + /* ── Parent record: add parent + all children with recordFilter ── */ + const _addParentRecord = useCallback(async ( + node: FeatureConnectionNode, + parentRecord: ParentRecordNode, + allTables: FeatureTableNode[], + ) => { + const addKey = `${node.featureInstanceId}-parent-${parentRecord.id}`; + setAddingParentKey(addKey); + try { + const parentTable = allTables.find(t => t.tableName === parentRecord.tableName && t.isParent); + const childTables = allTables.filter(t => t.parentTable === parentRecord.tableName); + + if (parentTable) { + const parentLabel = `${parentTable.label?.en || parentTable.label?.de || parentTable.tableName}: ${parentRecord.displayLabel}`; + await api.post(`/api/workspace/${instanceId}/feature-datasources`, { + featureInstanceId: node.featureInstanceId, + featureCode: node.featureCode, + tableName: parentTable.tableName, + objectKey: parentTable.objectKey, + label: parentLabel, + recordFilter: { id: parentRecord.id }, + }); + } + + for (const child of childTables) { + const childLabel = `${child.label?.en || child.label?.de || child.tableName}: ${parentRecord.displayLabel}`; + await api.post(`/api/workspace/${instanceId}/feature-datasources`, { + featureInstanceId: node.featureInstanceId, + featureCode: node.featureCode, + tableName: child.tableName, + objectKey: child.objectKey, + label: childLabel, + recordFilter: { [child.parentKey!]: parentRecord.id }, + }); + } + + _fetchFeatureDataSources(); + onSourcesChanged?.(); + } catch (err) { + console.error('Failed to add parent record sources:', err); + } finally { + if (mountedRef.current) setAddingParentKey(null); + } + }, [instanceId, _fetchFeatureDataSources]); + + /* ── Check if a parent record is already added ── */ + const _isParentRecordAdded = useCallback((featureInstanceId: string, parentTableName: string, recordId: string): boolean => { + return featureDataSources.some(fds => + fds.featureInstanceId === featureInstanceId && + fds.tableName === parentTableName && + fds.recordFilter?.id === recordId, + ); + }, [featureDataSources]); + /* ── Render ── */ return ( @@ -777,60 +910,139 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => {/* ── Divider ── */}
- {/* ── Active Feature Sources ── */} + {/* ── Active Feature Sources (grouped by parent record) ── */} {featureDataSources.length > 0 && (
Active Feature Sources
- {[...featureDataSources].sort((a, b) => (a.label || a.tableName || '').localeCompare(b.label || b.tableName || '')).map(fds => { - const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId); - const fdsConnLabel = meta?.instanceLabel || fds.tableName; + {(() => { + 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 ( -
- - {getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'} - - - {fdsConnLabel} – {fds.tableName} - - - - -
+ <> + {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} + + + + +
+ ); + })} + ); - })} + })()}
)} @@ -871,6 +1083,13 @@ const SourcesTab: React.FC = ({ context, onSourcesChanged }) => onAddTable={_addFeatureTable} isTableAdded={_isFeatureTableAdded} addingKey={addingFeatureKey} + onToggleParentGroup={_toggleParentGroup} + onToggleParentRecord={_toggleParentRecord} + onAddParentRecord={_addParentRecord} + isParentRecordAdded={_isParentRecordAdded} + expandedParentGroups={expandedParentGroups} + loadingParentGroup={loadingParentGroup} + addingParentKey={addingParentKey} /> ))}
@@ -990,10 +1209,19 @@ interface _MandateGroupViewProps { onAddTable: (node: FeatureConnectionNode, table: FeatureTableNode) => void; isTableAdded: (featureInstanceId: string, tableName: string) => boolean; addingKey: string | null; + onToggleParentGroup: (node: FeatureConnectionNode, parentTableName: string) => void; + onToggleParentRecord: (featureInstanceId: string, parentTableName: string, recordId: string) => void; + onAddParentRecord: (node: FeatureConnectionNode, record: ParentRecordNode, allTables: FeatureTableNode[]) => void; + isParentRecordAdded: (featureInstanceId: string, parentTableName: string, recordId: string) => boolean; + expandedParentGroups: Set; + loadingParentGroup: string | null; + addingParentKey: string | null; } const _MandateGroupView: React.FC<_MandateGroupViewProps> = ({ group, onToggleGroup, onToggleFeature, onAddTable, isTableAdded, addingKey, + onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded, + expandedParentGroups, loadingParentGroup, addingParentKey, }) => { const [hovered, setHovered] = useState(false); const chevron = group.expanded ? '\u25BE' : '\u25B8'; @@ -1030,6 +1258,13 @@ const _MandateGroupView: React.FC<_MandateGroupViewProps> = ({ onAddTable={onAddTable} isTableAdded={isTableAdded} addingKey={addingKey} + onToggleParentGroup={onToggleParentGroup} + onToggleParentRecord={onToggleParentRecord} + onAddParentRecord={onAddParentRecord} + isParentRecordAdded={isParentRecordAdded} + expandedParentGroups={expandedParentGroups} + loadingParentGroup={loadingParentGroup} + addingParentKey={addingParentKey} /> ))}
@@ -1046,14 +1281,26 @@ interface _FeatureNodeViewProps { onAddTable: (node: FeatureConnectionNode, table: FeatureTableNode) => void; isTableAdded: (featureInstanceId: string, tableName: string) => boolean; addingKey: string | null; + onToggleParentGroup: (node: FeatureConnectionNode, parentTableName: string) => void; + onToggleParentRecord: (featureInstanceId: string, parentTableName: string, recordId: string) => void; + onAddParentRecord: (node: FeatureConnectionNode, record: ParentRecordNode, allTables: FeatureTableNode[]) => void; + isParentRecordAdded: (featureInstanceId: string, parentTableName: string, recordId: string) => boolean; + expandedParentGroups: Set; + loadingParentGroup: string | null; + addingParentKey: string | null; } const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({ node, onToggle, onAddTable, isTableAdded, addingKey, + onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded, + expandedParentGroups, loadingParentGroup, addingParentKey, }) => { const [hovered, setHovered] = useState(false); const chevron = node.expanded ? '\u25BE' : '\u25B8'; + const parentTables = (node.tables || []).filter(t => t.isParent); + const standaloneTables = (node.tables || []).filter(t => !t.isParent && !t.parentTable); + return (
= ({ {node.expanded && node.tables && node.tables.length > 0 && (
- {node.tables.map(table => ( + {/* Parent table groups (hierarchical) */} + {parentTables.map(pt => { + const groupKey = `${node.featureInstanceId}-${pt.tableName}`; + const isGroupExpanded = expandedParentGroups.has(groupKey); + const isGroupLoading = loadingParentGroup === groupKey; + const records = node.parentRecords[pt.tableName]; + const childTables = (node.tables || []).filter(t => t.parentTable === pt.tableName); + const ptLabel = pt.label?.en || pt.label?.de || pt.tableName; + + return ( + <_ParentGroupView + key={groupKey} + featureNode={node} + parentTable={pt} + label={ptLabel} + expanded={isGroupExpanded} + loading={isGroupLoading} + records={records || null} + childTables={childTables} + allTables={node.tables!} + onToggleGroup={() => onToggleParentGroup(node, pt.tableName)} + onToggleRecord={(recordId) => onToggleParentRecord(node.featureInstanceId, pt.tableName, recordId)} + onAddRecord={(record) => onAddParentRecord(node, record, node.tables!)} + isRecordAdded={(recordId) => isParentRecordAdded(node.featureInstanceId, pt.tableName, recordId)} + addingParentKey={addingParentKey} + /> + ); + })} + + {/* Standalone tables (not part of any hierarchy) */} + {standaloneTables.map(table => ( <_FeatureTableRow key={table.objectKey} featureNode={node} @@ -1163,4 +1440,169 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({ ); }; +/* ─── ParentGroupView (parent table → parent records) ────────────────── */ + +interface _ParentGroupViewProps { + featureNode: FeatureConnectionNode; + parentTable: FeatureTableNode; + label: string; + expanded: boolean; + loading: boolean; + records: ParentRecordNode[] | null; + childTables: FeatureTableNode[]; + allTables: FeatureTableNode[]; + onToggleGroup: () => void; + onToggleRecord: (recordId: string) => void; + onAddRecord: (record: ParentRecordNode) => void; + isRecordAdded: (recordId: string) => boolean; + addingParentKey: string | null; +} + +const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({ + featureNode, parentTable, label, expanded, loading, records, childTables, allTables, + onToggleGroup, onToggleRecord, onAddRecord, isRecordAdded, addingParentKey, +}) => { + const [hovered, setHovered] = useState(false); + const chevron = expanded ? '\u25BE' : '\u25B8'; + + return ( +
+
setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ + display: 'flex', alignItems: 'center', gap: 4, + paddingLeft: 24, paddingRight: 4, paddingTop: 3, paddingBottom: 3, + cursor: 'pointer', borderRadius: 3, + background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent', + transition: 'background 0.1s', userSelect: 'none', + }} + > + + {loading ? _Spinner() : chevron} + + {'\uD83D\uDCC2'} + + {label} + + {childTables.length > 0 && ( + + +{childTables.length} tables + + )} +
+ + {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}`} + /> + ))} +
+ )} + + {expanded && records && records.length === 0 && !loading && ( +
+ (no records) +
+ )} +
+ ); +}; + +/* ─── ParentRecordRow (single parent record + child tables info) ─────── */ + +interface _ParentRecordRowProps { + featureNode: FeatureConnectionNode; + record: ParentRecordNode; + childTables: FeatureTableNode[]; + allTables: FeatureTableNode[]; + onToggle: () => void; + onAdd: () => void; + isAdded: boolean; + isAdding: boolean; +} + +const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({ + featureNode, record, childTables, allTables, + onToggle, onAdd, isAdded, isAdding, +}) => { + const [hovered, setHovered] = useState(false); + const chevron = record.expanded ? '\u25BE' : '\u25B8'; + + return ( +
+
setHovered(true)} + onMouseLeave={() => setHovered(false)} + 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', + transition: 'background 0.1s', userSelect: 'none', + }} + title={Object.entries(record.fields).map(([k, v]) => `${k}: ${v}`).join(', ')} + > + + {chevron} + + {'\uD83D\uDCCB'} + + {record.displayLabel} + + {hovered && !isAdded && ( + + )} + {isAdded && ( + + {'\u2713'} + + )} +
+ + {record.expanded && ( +
+ {childTables.map(ct => { + const ctLabel = ct.label?.en || ct.label?.de || ct.tableName; + return ( +
+ {'\uD83D\uDCC4'} + {ctLabel} + ({ct.parentKey}) +
+ ); + })} +
+ )} +
+ ); +}; + export default SourcesTab; diff --git a/src/pages/views/workspace/useWorkspace.ts b/src/pages/views/workspace/useWorkspace.ts index c5dc589..0fcff28 100644 --- a/src/pages/views/workspace/useWorkspace.ts +++ b/src/pages/views/workspace/useWorkspace.ts @@ -71,6 +71,7 @@ export interface FeatureDataSource { label: string; mandateId: string; workspaceInstanceId: string; + recordFilter?: Record; } export interface FileEditProposal {