fix workspace sources: icon visibility, state sync, color mapping, sort order

Made-with: Cursor
This commit is contained in:
ValueOn AG 2026-03-31 22:47:30 +02:00
parent cba01a2d61
commit b96d3dad4a
4 changed files with 65 additions and 25 deletions

View file

@ -89,6 +89,7 @@ interface FeatureTableNode {
interface SourcesTabProps { interface SourcesTabProps {
context: UdbContext; context: UdbContext;
onSourcesChanged?: () => void;
} }
/* ─── Icons ──────────────────────────────────────────────────────────── */ /* ─── Icons ──────────────────────────────────────────────────────────── */
@ -115,27 +116,46 @@ const _SERVICE_ICONS: Record<string, string> = {
const _SOURCE_COLORS: Record<string, string> = { const _SOURCE_COLORS: Record<string, string> = {
sharepointFolder: '#0078d4', sharepointFolder: '#0078d4',
sharepoint: '#0078d4',
onedriveFolder: '#0078d4', onedriveFolder: '#0078d4',
onedrive: '#0078d4',
outlookFolder: '#0078d4', outlookFolder: '#0078d4',
outlook: '#0078d4',
googleDriveFolder: '#34a853', googleDriveFolder: '#34a853',
drive: '#34a853',
gmailFolder: '#ea4335', gmailFolder: '#ea4335',
gmail: '#ea4335',
ftpFolder: '#795548', ftpFolder: '#795548',
files: '#795548',
'local:ftp': '#795548',
'local:jira': '#1976d2',
clickup: '#7b68ee',
}; };
function _getSourceColor(sourceType: string): string { function _getSourceColor(sourceType: string): string {
return _SOURCE_COLORS[sourceType] || '#1976d2'; return _SOURCE_COLORS[sourceType] || '#1976d2';
} }
const _SOURCE_ICONS: Record<string, string> = {
sharepointFolder: '\uD83D\uDCC1',
sharepoint: '\uD83D\uDCC1',
onedriveFolder: '\u2601\uFE0F',
onedrive: '\u2601\uFE0F',
outlookFolder: '\uD83D\uDCE7',
outlook: '\uD83D\uDCE7',
googleDriveFolder: '\uD83D\uDCC2',
drive: '\uD83D\uDCC2',
gmailFolder: '\uD83D\uDCE8',
gmail: '\uD83D\uDCE8',
ftpFolder: '\uD83D\uDD17',
files: '\uD83D\uDD17',
'local:ftp': '\uD83D\uDD17',
'local:jira': '\uD83D\uDD27',
clickup: '\uD83D\uDCCB',
};
function _getSourceIcon(sourceType: string): string { function _getSourceIcon(sourceType: string): string {
const map: Record<string, string> = { return _SOURCE_ICONS[sourceType] || '\uD83D\uDCC1';
sharepointFolder: '\uD83D\uDCC1',
onedriveFolder: '\u2601\uFE0F',
outlookFolder: '\uD83D\uDCE7',
googleDriveFolder: '\uD83D\uDCC2',
gmailFolder: '\uD83D\uDCE8',
ftpFolder: '\uD83D\uDD17',
};
return map[sourceType] || '\uD83D\uDCC1';
} }
/* ─── Scope / Neutralize constants ───────────────────────────────────── */ /* ─── Scope / Neutralize constants ───────────────────────────────────── */
@ -162,6 +182,15 @@ function _nextScope(current: string): string {
return _SCOPE_ORDER[(idx + 1) % _SCOPE_ORDER.length]; return _SCOPE_ORDER[(idx + 1) % _SCOPE_ORDER.length];
} }
const _SERVICE_TO_SOURCE_TYPE: Record<string, string> = {
sharepoint: 'sharepointFolder',
onedrive: 'onedriveFolder',
outlook: 'outlookFolder',
drive: 'googleDriveFolder',
gmail: 'gmailFolder',
files: 'ftpFolder',
};
/* ─── Tree helpers ───────────────────────────────────────────────────── */ /* ─── Tree helpers ───────────────────────────────────────────────────── */
function _mapTree(nodes: TreeNode[], key: string, updater: (n: TreeNode) => TreeNode): TreeNode[] { function _mapTree(nodes: TreeNode[], key: string, updater: (n: TreeNode) => TreeNode): TreeNode[] {
@ -301,7 +330,7 @@ function _Spinner(): React.ReactElement {
/* ─── Component ──────────────────────────────────────────────────────── */ /* ─── Component ──────────────────────────────────────────────────────── */
const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => { const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) => {
const instanceId = context.instanceId; const instanceId = context.instanceId;
/* ── Active sources (fetched internally) ── */ /* ── Active sources (fetched internally) ── */
@ -444,22 +473,15 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => {
if (!node.service || !node.connectionId) return; if (!node.service || !node.connectionId) return;
setAddingPath(node.key); setAddingPath(node.key);
try { try {
const sourceTypeMap: Record<string, string> = {
sharepoint: 'sharepointFolder',
onedrive: 'onedriveFolder',
outlook: 'outlookFolder',
drive: 'googleDriveFolder',
gmail: 'gmailFolder',
files: 'ftpFolder',
};
await api.post(`/api/workspace/${instanceId}/datasources`, { await api.post(`/api/workspace/${instanceId}/datasources`, {
connectionId: node.connectionId, connectionId: node.connectionId,
sourceType: sourceTypeMap[node.service] || node.service, sourceType: _SERVICE_TO_SOURCE_TYPE[node.service] || node.service,
path: node.path || '/', path: node.path || '/',
label: node.label, label: node.label,
displayPath: node.displayPath || node.label, displayPath: node.displayPath || node.label,
}); });
_fetchDataSources(); _fetchDataSources();
onSourcesChanged?.();
} catch (err) { } catch (err) {
console.error('Failed to add data source:', err); console.error('Failed to add data source:', err);
} finally { } finally {
@ -472,15 +494,19 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => {
try { try {
await api.delete(`/api/workspace/${instanceId}/datasources/${dsId}`); await api.delete(`/api/workspace/${instanceId}/datasources/${dsId}`);
_fetchDataSources(); _fetchDataSources();
onSourcesChanged?.();
} catch (err) { } catch (err) {
console.error('Failed to remove data source:', err); console.error('Failed to remove data source:', err);
} }
}, [instanceId, _fetchDataSources]); }, [instanceId, _fetchDataSources]);
/* ── Check if a path is already added ── */ /* ── Check if a path is already added ── */
const _isAdded = useCallback((connectionId: string, _service: string | undefined, path: string | undefined): boolean => { const _isAdded = useCallback((connectionId: string, service: string | undefined, path: string | undefined): boolean => {
const expectedSourceType = service ? (_SERVICE_TO_SOURCE_TYPE[service] || service) : undefined;
return dataSources.some(ds => return dataSources.some(ds =>
ds.connectionId === connectionId && ds.path === (path || '/'), ds.connectionId === connectionId &&
ds.path === (path || '/') &&
(!expectedSourceType || ds.sourceType === expectedSourceType),
); );
}, [dataSources]); }, [dataSources]);
@ -617,6 +643,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => {
label: table.label?.en || table.label?.de || table.tableName, label: table.label?.en || table.label?.de || table.tableName,
}); });
_fetchFeatureDataSources(); _fetchFeatureDataSources();
onSourcesChanged?.();
} catch (err) { } catch (err) {
console.error('Failed to add feature data source:', err); console.error('Failed to add feature data source:', err);
} finally { } finally {
@ -629,6 +656,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => {
try { try {
await api.delete(`/api/workspace/${instanceId}/feature-datasources/${fdsId}`); await api.delete(`/api/workspace/${instanceId}/feature-datasources/${fdsId}`);
_fetchFeatureDataSources(); _fetchFeatureDataSources();
onSourcesChanged?.();
} catch (err) { } catch (err) {
console.error('Failed to remove feature data source:', err); console.error('Failed to remove feature data source:', err);
} }
@ -651,7 +679,11 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => {
<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 Personal Sources Active Personal Sources
</div> </div>
{dataSources.map(ds => { {[...dataSources].sort((a, b) => {
const aKey = `${a.sourceType}|${a.label || a.path || ''}`;
const bKey = `${b.sourceType}|${b.label || b.path || ''}`;
return aKey.localeCompare(bKey);
}).map(ds => {
const connColor = _getSourceColor(ds.sourceType); const connColor = _getSourceColor(ds.sourceType);
const connNode = tree.find(n => n.connectionId === ds.connectionId); const connNode = tree.find(n => n.connectionId === ds.connectionId);
const connLabel = connNode?.label || ds.connectionId; const connLabel = connNode?.label || ds.connectionId;
@ -751,7 +783,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => {
<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.map(fds => { {[...featureDataSources].sort((a, b) => (a.label || a.tableName || '').localeCompare(b.label || b.tableName || '')).map(fds => {
const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId); const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId);
const fdsConnLabel = meta?.instanceLabel || fds.tableName; const fdsConnLabel = meta?.instanceLabel || fds.tableName;
return ( return (

View file

@ -25,6 +25,7 @@ interface UnifiedDataBarProps {
onDeleteChat?: (chatId: string) => void; onDeleteChat?: (chatId: string) => void;
onChatDragStart?: (chatId: string, event: React.DragEvent) => void; onChatDragStart?: (chatId: string, event: React.DragEvent) => void;
onFileSelect?: (fileId: string) => void; onFileSelect?: (fileId: string) => void;
onSourcesChanged?: () => void;
className?: string; className?: string;
} }
@ -46,6 +47,7 @@ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
onDeleteChat, onDeleteChat,
onChatDragStart, onChatDragStart,
onFileSelect, onFileSelect,
onSourcesChanged,
className, className,
}) => { }) => {
const visibleTabs = (['chats', 'files', 'sources'] as UdbTab[]).filter( const visibleTabs = (['chats', 'files', 'sources'] as UdbTab[]).filter(
@ -91,7 +93,7 @@ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
/> />
)} )}
{currentTab === 'sources' && !hideTabs?.includes('sources') && ( {currentTab === 'sources' && !hideTabs?.includes('sources') && (
<SourcesTab context={context} /> <SourcesTab context={context} onSourcesChanged={onSourcesChanged} />
)} )}
</div> </div>
</div> </div>

View file

@ -543,7 +543,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
{uploading ? '...' : '+'} {uploading ? '...' : '+'}
</button> </button>
{dataSources.length > 0 && ( {(dataSources.length > 0 || featureDataSources.length > 0) && (
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<button <button
onClick={() => setShowSourcePicker(prev => !prev)} onClick={() => setShowSourcePicker(prev => !prev)}

View file

@ -254,6 +254,11 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
featureInstanceId: instanceId, featureInstanceId: instanceId,
}; };
const _handleSourcesChanged = useCallback(() => {
workspace.refreshDataSources();
workspace.refreshFeatureDataSources();
}, [workspace]);
const _leftPanelBody = ( const _leftPanelBody = (
<UnifiedDataBar <UnifiedDataBar
context={_udbContext} context={_udbContext}
@ -265,6 +270,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
onRenameChat={_handleRenameChat} onRenameChat={_handleRenameChat}
onDeleteChat={_handleDeleteChat} onDeleteChat={_handleDeleteChat}
onFileSelect={_handleFileSelect} onFileSelect={_handleFileSelect}
onSourcesChanged={_handleSourcesChanged}
/> />
); );