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;
|
label: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
neutralize: boolean;
|
neutralize: boolean;
|
||||||
|
recordFilter?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TreeNode {
|
interface TreeNode {
|
||||||
|
|
@ -69,6 +70,7 @@ interface FeatureConnectionNode {
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
tables: FeatureTableNode[] | null;
|
tables: FeatureTableNode[] | null;
|
||||||
|
parentRecords: Record<string, ParentRecordNode[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MandateGroupNode {
|
interface MandateGroupNode {
|
||||||
|
|
@ -83,6 +85,18 @@ interface FeatureTableNode {
|
||||||
tableName: string;
|
tableName: string;
|
||||||
label: Record<string, string>;
|
label: Record<string, string>;
|
||||||
fields: 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 ──────────────────────────────────────────────────────────── */
|
/* ─── Props ──────────────────────────────────────────────────────────── */
|
||||||
|
|
@ -386,6 +400,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
label: d.label,
|
label: d.label,
|
||||||
scope: d.scope || 'personal',
|
scope: d.scope || 'personal',
|
||||||
neutralize: d.neutralize ?? false,
|
neutralize: d.neutralize ?? false,
|
||||||
|
recordFilter: d.recordFilter || undefined,
|
||||||
}));
|
}));
|
||||||
setFeatureDataSources(list);
|
setFeatureDataSources(list);
|
||||||
})
|
})
|
||||||
|
|
@ -576,6 +591,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
expanded: false,
|
expanded: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
tables: null,
|
tables: null,
|
||||||
|
parentRecords: {},
|
||||||
})),
|
})),
|
||||||
})));
|
})));
|
||||||
})
|
})
|
||||||
|
|
@ -615,6 +631,10 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
tableName: t.tableName,
|
tableName: t.tableName,
|
||||||
label: t.label || {},
|
label: t.label || {},
|
||||||
fields: t.fields || [],
|
fields: t.fields || [],
|
||||||
|
isParent: t.isParent || false,
|
||||||
|
parentTable: t.parentTable || undefined,
|
||||||
|
parentKey: t.parentKey || undefined,
|
||||||
|
displayFields: t.displayFields || undefined,
|
||||||
}));
|
}));
|
||||||
if (mountedRef.current) {
|
if (mountedRef.current) {
|
||||||
setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
|
setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
|
||||||
|
|
@ -669,6 +689,119 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
);
|
);
|
||||||
}, [featureDataSources]);
|
}, [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 ── */
|
/* ── Render ── */
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -777,60 +910,139 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
{/* ── Divider ── */}
|
{/* ── Divider ── */}
|
||||||
<div style={{ height: 1, background: 'var(--border-color, #e0e0e0)', margin: '12px 0 8px' }} />
|
<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 && (
|
{featureDataSources.length > 0 && (
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
|
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
|
||||||
Active Feature Sources
|
Active Feature Sources
|
||||||
</div>
|
</div>
|
||||||
{[...featureDataSources].sort((a, b) => (a.label || a.tableName || '').localeCompare(b.label || b.tableName || '')).map(fds => {
|
{(() => {
|
||||||
const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId);
|
const sorted = [...featureDataSources].sort((a, b) => (a.label || a.tableName || '').localeCompare(b.label || b.tableName || ''));
|
||||||
const fdsConnLabel = meta?.instanceLabel || fds.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 (
|
return (
|
||||||
<div key={fds.id} style={{
|
<>
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
{grouped.map(group => (
|
||||||
padding: '4px 6px', borderRadius: 4, marginBottom: 2,
|
<div key={group.key} style={{ marginBottom: 4 }}>
|
||||||
background: '#7b1fa218',
|
<div style={{
|
||||||
borderLeft: '3px solid #7b1fa2',
|
fontSize: 11, fontWeight: 600, color: '#7b1fa2',
|
||||||
fontSize: 12,
|
padding: '2px 6px', marginBottom: 1,
|
||||||
}} title={_featureDataSourceHoverTitle(meta, fds)}>
|
display: 'flex', alignItems: 'center', gap: 4,
|
||||||
<span style={{ fontSize: 12, flexShrink: 0, display: 'flex', alignItems: 'center', color: '#7b1fa2' }}>
|
}}>
|
||||||
{getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'}
|
<span>{'\uD83D\uDCCB'}</span>
|
||||||
</span>
|
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
{group.label}
|
||||||
{fdsConnLabel} – {fds.tableName}
|
</span>
|
||||||
</span>
|
<button
|
||||||
<button
|
onClick={() => { group.items.forEach(fds => _removeFeatureDataSource(fds.id)); }}
|
||||||
onClick={() => _cycleFeatureScope(fds)}
|
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }}
|
||||||
style={{
|
title="Remove all tables for this record"
|
||||||
background: 'none', border: 'none', cursor: 'pointer',
|
>
|
||||||
fontSize: 13, padding: '0 2px', lineHeight: 1,
|
{'\u2715'}
|
||||||
}}
|
</button>
|
||||||
title={`Scope: ${_SCOPE_LABELS[fds.scope] || fds.scope} → ${_SCOPE_LABELS[_nextScope(fds.scope)]}`}
|
</div>
|
||||||
>
|
{group.items.map(fds => {
|
||||||
{_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
|
const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId);
|
||||||
</button>
|
return (
|
||||||
<button
|
<div key={fds.id} style={{
|
||||||
onClick={() => _toggleFeatureNeutralize(fds)}
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
style={{
|
padding: '3px 6px 3px 22px', borderRadius: 4, marginBottom: 1,
|
||||||
background: 'none', border: 'none', cursor: 'pointer',
|
background: '#7b1fa210',
|
||||||
fontSize: 13, padding: '0 2px', lineHeight: 1,
|
fontSize: 11,
|
||||||
opacity: fds.neutralize ? 1 : 0.35,
|
}} title={_featureDataSourceHoverTitle(meta, fds)}>
|
||||||
}}
|
<span style={{ fontSize: 11, flexShrink: 0, color: '#7b1fa2' }}>
|
||||||
title={fds.neutralize ? 'Neutralize: ON (click to deactivate)' : 'Neutralize: OFF (click to activate)'}
|
{getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDCC4'}
|
||||||
>
|
</span>
|
||||||
{'\uD83D\uDD12'}
|
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
</button>
|
{fds.tableName}
|
||||||
<button
|
</span>
|
||||||
onClick={() => _removeFeatureDataSource(fds.id)}
|
<button
|
||||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
|
onClick={() => _cycleFeatureScope(fds)}
|
||||||
title="Entfernen"
|
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)]}`}
|
||||||
{'\u2715'}
|
>
|
||||||
</button>
|
{_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
|
||||||
</div>
|
</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 style={{ height: 1, background: 'var(--border-color, #e0e0e0)', margin: '8px 0' }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -871,6 +1083,13 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
||||||
onAddTable={_addFeatureTable}
|
onAddTable={_addFeatureTable}
|
||||||
isTableAdded={_isFeatureTableAdded}
|
isTableAdded={_isFeatureTableAdded}
|
||||||
addingKey={addingFeatureKey}
|
addingKey={addingFeatureKey}
|
||||||
|
onToggleParentGroup={_toggleParentGroup}
|
||||||
|
onToggleParentRecord={_toggleParentRecord}
|
||||||
|
onAddParentRecord={_addParentRecord}
|
||||||
|
isParentRecordAdded={_isParentRecordAdded}
|
||||||
|
expandedParentGroups={expandedParentGroups}
|
||||||
|
loadingParentGroup={loadingParentGroup}
|
||||||
|
addingParentKey={addingParentKey}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -990,10 +1209,19 @@ interface _MandateGroupViewProps {
|
||||||
onAddTable: (node: FeatureConnectionNode, table: FeatureTableNode) => void;
|
onAddTable: (node: FeatureConnectionNode, table: FeatureTableNode) => void;
|
||||||
isTableAdded: (featureInstanceId: string, tableName: string) => boolean;
|
isTableAdded: (featureInstanceId: string, tableName: string) => boolean;
|
||||||
addingKey: string | null;
|
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> = ({
|
const _MandateGroupView: React.FC<_MandateGroupViewProps> = ({
|
||||||
group, onToggleGroup, onToggleFeature, onAddTable, isTableAdded, addingKey,
|
group, onToggleGroup, onToggleFeature, onAddTable, isTableAdded, addingKey,
|
||||||
|
onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded,
|
||||||
|
expandedParentGroups, loadingParentGroup, addingParentKey,
|
||||||
}) => {
|
}) => {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const chevron = group.expanded ? '\u25BE' : '\u25B8';
|
const chevron = group.expanded ? '\u25BE' : '\u25B8';
|
||||||
|
|
@ -1030,6 +1258,13 @@ const _MandateGroupView: React.FC<_MandateGroupViewProps> = ({
|
||||||
onAddTable={onAddTable}
|
onAddTable={onAddTable}
|
||||||
isTableAdded={isTableAdded}
|
isTableAdded={isTableAdded}
|
||||||
addingKey={addingKey}
|
addingKey={addingKey}
|
||||||
|
onToggleParentGroup={onToggleParentGroup}
|
||||||
|
onToggleParentRecord={onToggleParentRecord}
|
||||||
|
onAddParentRecord={onAddParentRecord}
|
||||||
|
isParentRecordAdded={isParentRecordAdded}
|
||||||
|
expandedParentGroups={expandedParentGroups}
|
||||||
|
loadingParentGroup={loadingParentGroup}
|
||||||
|
addingParentKey={addingParentKey}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1046,14 +1281,26 @@ interface _FeatureNodeViewProps {
|
||||||
onAddTable: (node: FeatureConnectionNode, table: FeatureTableNode) => void;
|
onAddTable: (node: FeatureConnectionNode, table: FeatureTableNode) => void;
|
||||||
isTableAdded: (featureInstanceId: string, tableName: string) => boolean;
|
isTableAdded: (featureInstanceId: string, tableName: string) => boolean;
|
||||||
addingKey: string | null;
|
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> = ({
|
const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
|
||||||
node, onToggle, onAddTable, isTableAdded, addingKey,
|
node, onToggle, onAddTable, isTableAdded, addingKey,
|
||||||
|
onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded,
|
||||||
|
expandedParentGroups, loadingParentGroup, addingParentKey,
|
||||||
}) => {
|
}) => {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const chevron = node.expanded ? '\u25BE' : '\u25B8';
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -1084,7 +1331,37 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
|
||||||
|
|
||||||
{node.expanded && node.tables && node.tables.length > 0 && (
|
{node.expanded && node.tables && node.tables.length > 0 && (
|
||||||
<div>
|
<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
|
<_FeatureTableRow
|
||||||
key={table.objectKey}
|
key={table.objectKey}
|
||||||
featureNode={node}
|
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;
|
export default SourcesTab;
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ export interface FeatureDataSource {
|
||||||
label: string;
|
label: string;
|
||||||
mandateId: string;
|
mandateId: string;
|
||||||
workspaceInstanceId: string;
|
workspaceInstanceId: string;
|
||||||
|
recordFilter?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileEditProposal {
|
export interface FileEditProposal {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue