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 {
context: UdbContext;
onSourcesChanged?: () => void;
}
/* ─── Icons ──────────────────────────────────────────────────────────── */
@ -115,27 +116,46 @@ const _SERVICE_ICONS: Record<string, string> = {
const _SOURCE_COLORS: Record<string, string> = {
sharepointFolder: '#0078d4',
sharepoint: '#0078d4',
onedriveFolder: '#0078d4',
onedrive: '#0078d4',
outlookFolder: '#0078d4',
outlook: '#0078d4',
googleDriveFolder: '#34a853',
drive: '#34a853',
gmailFolder: '#ea4335',
gmail: '#ea4335',
ftpFolder: '#795548',
files: '#795548',
'local:ftp': '#795548',
'local:jira': '#1976d2',
clickup: '#7b68ee',
};
function _getSourceColor(sourceType: string): string {
return _SOURCE_COLORS[sourceType] || '#1976d2';
}
function _getSourceIcon(sourceType: string): string {
const map: Record<string, string> = {
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',
};
return map[sourceType] || '\uD83D\uDCC1';
function _getSourceIcon(sourceType: string): string {
return _SOURCE_ICONS[sourceType] || '\uD83D\uDCC1';
}
/* ─── Scope / Neutralize constants ───────────────────────────────────── */
@ -162,6 +182,15 @@ function _nextScope(current: string): string {
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 ───────────────────────────────────────────────────── */
function _mapTree(nodes: TreeNode[], key: string, updater: (n: TreeNode) => TreeNode): TreeNode[] {
@ -301,7 +330,7 @@ function _Spinner(): React.ReactElement {
/* ─── Component ──────────────────────────────────────────────────────── */
const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => {
const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) => {
const instanceId = context.instanceId;
/* ── Active sources (fetched internally) ── */
@ -444,22 +473,15 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => {
if (!node.service || !node.connectionId) return;
setAddingPath(node.key);
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`, {
connectionId: node.connectionId,
sourceType: sourceTypeMap[node.service] || node.service,
sourceType: _SERVICE_TO_SOURCE_TYPE[node.service] || node.service,
path: node.path || '/',
label: node.label,
displayPath: node.displayPath || node.label,
});
_fetchDataSources();
onSourcesChanged?.();
} catch (err) {
console.error('Failed to add data source:', err);
} finally {
@ -472,15 +494,19 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => {
try {
await api.delete(`/api/workspace/${instanceId}/datasources/${dsId}`);
_fetchDataSources();
onSourcesChanged?.();
} catch (err) {
console.error('Failed to remove data source:', err);
}
}, [instanceId, _fetchDataSources]);
/* ── 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 =>
ds.connectionId === connectionId && ds.path === (path || '/'),
ds.connectionId === connectionId &&
ds.path === (path || '/') &&
(!expectedSourceType || ds.sourceType === expectedSourceType),
);
}, [dataSources]);
@ -617,6 +643,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => {
label: table.label?.en || table.label?.de || table.tableName,
});
_fetchFeatureDataSources();
onSourcesChanged?.();
} catch (err) {
console.error('Failed to add feature data source:', err);
} finally {
@ -629,6 +656,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context }) => {
try {
await api.delete(`/api/workspace/${instanceId}/feature-datasources/${fdsId}`);
_fetchFeatureDataSources();
onSourcesChanged?.();
} catch (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 }}>
Active Personal Sources
</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 connNode = tree.find(n => n.connectionId === 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 }}>
Active Feature Sources
</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 fdsConnLabel = meta?.instanceLabel || fds.tableName;
return (

View file

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

View file

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

View file

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