fixed data source

This commit is contained in:
ValueOn AG 2026-03-31 23:41:02 +02:00
parent b96d3dad4a
commit 66708d6743
2 changed files with 491 additions and 48 deletions

View file

@ -42,6 +42,7 @@ interface UdbFeatureDataSource {
label: string;
scope: string;
neutralize: boolean;
recordFilter?: Record<string, string>;
}
interface TreeNode {
@ -69,6 +70,7 @@ interface FeatureConnectionNode {
expanded: boolean;
loading: boolean;
tables: FeatureTableNode[] | null;
parentRecords: Record<string, ParentRecordNode[]>;
}
interface MandateGroupNode {
@ -83,6 +85,18 @@ interface FeatureTableNode {
tableName: string;
label: Record<string, string>;
fields: string[];
isParent?: boolean;
parentTable?: string;
parentKey?: string;
displayFields?: string[];
}
interface ParentRecordNode {
id: string;
displayLabel: string;
fields: Record<string, any>;
tableName: string;
expanded: boolean;
}
/* ─── Props ──────────────────────────────────────────────────────────── */
@ -386,6 +400,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ 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<SourcesTabProps> = ({ context, onSourcesChanged }) =>
expanded: false,
loading: false,
tables: null,
parentRecords: {},
})),
})));
})
@ -615,6 +631,10 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ 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<SourcesTabProps> = ({ context, onSourcesChanged }) =>
);
}, [featureDataSources]);
/* ── Parent groups: expand/collapse + load records ── */
const [expandedParentGroups, setExpandedParentGroups] = useState<Set<string>>(new Set());
const [loadingParentGroup, setLoadingParentGroup] = useState<string | null>(null);
const [addingParentKey, setAddingParentKey] = useState<string | null>(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<SourcesTabProps> = ({ context, onSourcesChanged }) =>
{/* ── Divider ── */}
<div style={{ height: 1, background: 'var(--border-color, #e0e0e0)', margin: '12px 0 8px' }} />
{/* ── Active Feature Sources ── */}
{/* ── Active Feature Sources (grouped by parent record) ── */}
{featureDataSources.length > 0 && (
<div style={{ marginBottom: 8 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
Active Feature Sources
</div>
{[...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 (
<div key={fds.id} style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '4px 6px', borderRadius: 4, marginBottom: 2,
background: '#7b1fa218',
borderLeft: '3px solid #7b1fa2',
fontSize: 12,
}} title={_featureDataSourceHoverTitle(meta, fds)}>
<span style={{ fontSize: 12, flexShrink: 0, display: 'flex', alignItems: 'center', color: '#7b1fa2' }}>
{getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'}
</span>
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{fdsConnLabel} {fds.tableName}
</span>
<button
onClick={() => _cycleFeatureScope(fds)}
style={{
background: 'none', border: 'none', cursor: 'pointer',
fontSize: 13, padding: '0 2px', lineHeight: 1,
}}
title={`Scope: ${_SCOPE_LABELS[fds.scope] || fds.scope}${_SCOPE_LABELS[_nextScope(fds.scope)]}`}
>
{_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
</button>
<button
onClick={() => _toggleFeatureNeutralize(fds)}
style={{
background: 'none', border: 'none', cursor: 'pointer',
fontSize: 13, padding: '0 2px', lineHeight: 1,
opacity: fds.neutralize ? 1 : 0.35,
}}
title={fds.neutralize ? 'Neutralize: ON (click to deactivate)' : 'Neutralize: OFF (click to activate)'}
>
{'\uD83D\uDD12'}
</button>
<button
onClick={() => _removeFeatureDataSource(fds.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
title="Entfernen"
>
{'\u2715'}
</button>
</div>
<>
{grouped.map(group => (
<div key={group.key} style={{ marginBottom: 4 }}>
<div style={{
fontSize: 11, fontWeight: 600, color: '#7b1fa2',
padding: '2px 6px', marginBottom: 1,
display: 'flex', alignItems: 'center', gap: 4,
}}>
<span>{'\uD83D\uDCCB'}</span>
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{group.label}
</span>
<button
onClick={() => { group.items.forEach(fds => _removeFeatureDataSource(fds.id)); }}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }}
title="Remove all tables for this record"
>
{'\u2715'}
</button>
</div>
{group.items.map(fds => {
const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId);
return (
<div key={fds.id} style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '3px 6px 3px 22px', borderRadius: 4, marginBottom: 1,
background: '#7b1fa210',
fontSize: 11,
}} title={_featureDataSourceHoverTitle(meta, fds)}>
<span style={{ fontSize: 11, flexShrink: 0, color: '#7b1fa2' }}>
{getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDCC4'}
</span>
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{fds.tableName}
</span>
<button
onClick={() => _cycleFeatureScope(fds)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, padding: '0 2px', lineHeight: 1 }}
title={`Scope: ${_SCOPE_LABELS[fds.scope] || fds.scope}${_SCOPE_LABELS[_nextScope(fds.scope)]}`}
>
{_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
</button>
<button
onClick={() => _toggleFeatureNeutralize(fds)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, padding: '0 2px', lineHeight: 1, opacity: fds.neutralize ? 1 : 0.35 }}
title={fds.neutralize ? 'Neutralize: ON' : 'Neutralize: OFF'}
>
{'\uD83D\uDD12'}
</button>
<button
onClick={() => _removeFeatureDataSource(fds.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }}
title="Remove"
>
{'\u2715'}
</button>
</div>
);
})}
</div>
))}
{standalone.map(fds => {
const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId);
const fdsConnLabel = meta?.instanceLabel || fds.tableName;
return (
<div key={fds.id} style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '4px 6px', borderRadius: 4, marginBottom: 2,
background: '#7b1fa218',
borderLeft: '3px solid #7b1fa2',
fontSize: 12,
}} title={_featureDataSourceHoverTitle(meta, fds)}>
<span style={{ fontSize: 12, flexShrink: 0, display: 'flex', alignItems: 'center', color: '#7b1fa2' }}>
{getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'}
</span>
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{fdsConnLabel} {fds.tableName}
</span>
<button
onClick={() => _cycleFeatureScope(fds)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1 }}
title={`Scope: ${_SCOPE_LABELS[fds.scope] || fds.scope}${_SCOPE_LABELS[_nextScope(fds.scope)]}`}
>
{_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
</button>
<button
onClick={() => _toggleFeatureNeutralize(fds)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1, opacity: fds.neutralize ? 1 : 0.35 }}
title={fds.neutralize ? 'Neutralize: ON' : 'Neutralize: OFF'}
>
{'\uD83D\uDD12'}
</button>
<button
onClick={() => _removeFeatureDataSource(fds.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
title="Entfernen"
>
{'\u2715'}
</button>
</div>
);
})}
</>
);
})}
})()}
<div style={{ height: 1, background: 'var(--border-color, #e0e0e0)', margin: '8px 0' }} />
</div>
)}
@ -871,6 +1083,13 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
onAddTable={_addFeatureTable}
isTableAdded={_isFeatureTableAdded}
addingKey={addingFeatureKey}
onToggleParentGroup={_toggleParentGroup}
onToggleParentRecord={_toggleParentRecord}
onAddParentRecord={_addParentRecord}
isParentRecordAdded={_isParentRecordAdded}
expandedParentGroups={expandedParentGroups}
loadingParentGroup={loadingParentGroup}
addingParentKey={addingParentKey}
/>
))}
</div>
@ -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<string>;
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}
/>
))}
</div>
@ -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<string>;
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 (
<div>
<div
@ -1084,7 +1331,37 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
{node.expanded && node.tables && node.tables.length > 0 && (
<div>
{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 (
<div>
<div
onClick={onToggleGroup}
onMouseEnter={() => 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',
}}
>
<span style={{ fontSize: 10, color: '#888', width: 12, textAlign: 'center', flexShrink: 0 }}>
{loading ? _Spinner() : chevron}
</span>
<span style={{ fontSize: 13, flexShrink: 0 }}>{'\uD83D\uDCC2'}</span>
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12, fontWeight: 600, color: '#555' }}>
{label}
</span>
{childTables.length > 0 && (
<span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
+{childTables.length} tables
</span>
)}
</div>
{expanded && records && records.length > 0 && (
<div>
{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}`}
/>
))}
</div>
)}
{expanded && records && records.length === 0 && !loading && (
<div style={{ paddingLeft: 52, fontSize: 11, color: '#bbb', padding: '2px 0 2px 52px' }}>
(no records)
</div>
)}
</div>
);
};
/* ─── 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 (
<div>
<div
onClick={onToggle}
onMouseEnter={() => 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(', ')}
>
<span style={{ fontSize: 10, color: '#888', width: 12, textAlign: 'center', flexShrink: 0 }}>
{chevron}
</span>
<span style={{ fontSize: 13, flexShrink: 0 }}>{'\uD83D\uDCCB'}</span>
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12 }}>
{record.displayLabel}
</span>
{hovered && !isAdded && (
<button
onClick={e => { e.stopPropagation(); onAdd(); }}
disabled={isAdding}
style={{
background: 'none', border: '1px solid #7b1fa2', borderRadius: 3,
cursor: isAdding ? 'not-allowed' : 'pointer',
fontSize: 10, color: '#7b1fa2', padding: '1px 5px',
opacity: isAdding ? 0.5 : 1, flexShrink: 0,
}}
title="Add all tables for this record"
>
{isAdding ? '...' : '+ Add'}
</button>
)}
{isAdded && (
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title="Already added">
{'\u2713'}
</span>
)}
</div>
{record.expanded && (
<div style={{ paddingLeft: 64 }}>
{childTables.map(ct => {
const ctLabel = ct.label?.en || ct.label?.de || ct.tableName;
return (
<div key={ct.objectKey} style={{
display: 'flex', alignItems: 'center', gap: 4,
paddingTop: 2, paddingBottom: 2, fontSize: 11, color: '#888',
}}>
<span style={{ fontSize: 12 }}>{'\uD83D\uDCC4'}</span>
<span>{ctLabel}</span>
<span style={{ fontSize: 10, color: '#bbb' }}>({ct.parentKey})</span>
</div>
);
})}
</div>
)}
</div>
);
};
export default SourcesTab;

View file

@ -71,6 +71,7 @@ export interface FeatureDataSource {
label: string;
mandateId: string;
workspaceInstanceId: string;
recordFilter?: Record<string, string>;
}
export interface FileEditProposal {