fixed data source
This commit is contained in:
parent
b96d3dad4a
commit
66708d6743
2 changed files with 491 additions and 48 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export interface FeatureDataSource {
|
|||
label: string;
|
||||
mandateId: string;
|
||||
workspaceInstanceId: string;
|
||||
recordFilter?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface FileEditProposal {
|
||||
|
|
|
|||
Loading…
Reference in a new issue