teamsbot auth fixes

This commit is contained in:
ValueOn AG 2026-05-12 21:31:27 +02:00
parent 2ee08c314b
commit 0d8e6501d3
4 changed files with 95 additions and 57 deletions

View file

@ -58,6 +58,13 @@ function _buildChildMap<T>(nodes: TreeNode<T>[]): Map<string | '__root__', TreeN
if (list) list.push(n); if (list) list.push(n);
else map.set(key, [n]); else map.set(key, [n]);
} }
for (const [, children] of map) {
children.sort((a, b) => {
if (a.type === 'folder' && b.type !== 'folder') return -1;
if (a.type !== 'folder' && b.type === 'folder') return 1;
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
});
}
return map; return map;
} }
@ -651,10 +658,14 @@ export function FormGeneratorTree<T = any>({
const _handleCycleScope = useCallback( const _handleCycleScope = useCallback(
async (node: TreeNode<T>) => { async (node: TreeNode<T>) => {
const newScope = _nextScope(node.scope); const newScope = _nextScope(node.scope);
await provider.patchScope?.([node.id], newScope); const isFolder = node.type === 'folder';
setNodes((prev) => await provider.patchScope?.([node.id], newScope, isFolder);
prev.map((n) => (n.id === node.id ? { ...n, scope: newScope } : n)), setNodes((prev) => {
); if (!isFolder) return prev.map((n) => (n.id === node.id ? { ...n, scope: newScope } : n));
const descendantIds = new Set(_collectDescendantIds(node.id, prev));
descendantIds.add(node.id);
return prev.map((n) => descendantIds.has(n.id) ? { ...n, scope: newScope } : n);
});
}, },
[provider], [provider],
); );
@ -663,9 +674,12 @@ export function FormGeneratorTree<T = any>({
async (node: TreeNode<T>) => { async (node: TreeNode<T>) => {
const newValue = !node.neutralize; const newValue = !node.neutralize;
await provider.patchNeutralize?.([node.id], newValue); await provider.patchNeutralize?.([node.id], newValue);
setNodes((prev) => setNodes((prev) => {
prev.map((n) => (n.id === node.id ? { ...n, neutralize: newValue } : n)), if (node.type !== 'folder') return prev.map((n) => (n.id === node.id ? { ...n, neutralize: newValue } : n));
); const descendantIds = new Set(_collectDescendantIds(node.id, prev));
descendantIds.add(node.id);
return prev.map((n) => descendantIds.has(n.id) ? { ...n, neutralize: newValue } : n);
});
}, },
[provider], [provider],
); );

View file

@ -343,7 +343,7 @@ async function _loadServices(instanceId: string, connectionId: string): Promise<
service: s.service, service: s.service,
path: '/', path: '/',
displayPath: s.label || s.service, displayPath: s.label || s.service,
})); })).sort((a: TreeNode, b: TreeNode) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }));
} }
async function _browseService( async function _browseService(
@ -375,6 +375,10 @@ async function _browseService(
path: entry.path, path: entry.path,
displayPath, displayPath,
}; };
}).sort((a: TreeNode, b: TreeNode) => {
if (a.type === 'folder' && b.type !== 'folder') return -1;
if (a.type !== 'folder' && b.type === 'folder') return 1;
return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' });
}); });
} }
@ -496,6 +500,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
scope: d.scope || 'personal', scope: d.scope || 'personal',
neutralize: d.neutralize ?? false, neutralize: d.neutralize ?? false,
})); }));
list.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }));
setDataSources(list); setDataSources(list);
}) })
.catch(() => { if (mountedRef.current) setDataSources([]); }); .catch(() => { if (mountedRef.current) setDataSources([]); });
@ -519,6 +524,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
neutralizeFields: d.neutralizeFields || undefined, neutralizeFields: d.neutralizeFields || undefined,
recordFilter: d.recordFilter || undefined, recordFilter: d.recordFilter || undefined,
})); }));
list.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }));
setFeatureDataSources(list); setFeatureDataSources(list);
}) })
.catch(() => { if (mountedRef.current) setFeatureDataSources([]); }); .catch(() => { if (mountedRef.current) setFeatureDataSources([]); });
@ -547,7 +553,8 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
children: null, children: null,
connectionId: c.id, connectionId: c.id,
authority: c.authority, authority: c.authority,
})); }))
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }));
setTree(nodes); setTree(nodes);
}) })
.catch(() => { if (mountedRef.current) setTree([]); }) .catch(() => { if (mountedRef.current) setTree([]); })
@ -760,7 +767,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
expanded: false, expanded: false,
loading: false, loading: false,
tables: null, tables: null,
})), })).sort((a: FeatureConnectionNode, b: FeatureConnectionNode) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })),
}))); })));
}) })
.catch(() => { if (mountedRef.current) setFeatureTree([]); }) .catch(() => { if (mountedRef.current) setFeatureTree([]); })
@ -798,14 +805,14 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
objectKey: t.objectKey ?? '', objectKey: t.objectKey ?? '',
tableName: t.tableName ?? '', tableName: t.tableName ?? '',
label: t.label ?? '', label: t.label ?? '',
fields: t.fields ?? [], fields: (t.fields ?? []).slice().sort((a: string, b: string) => a.localeCompare(b, undefined, { sensitivity: 'base' })),
isParent: Boolean(t.isParent), isParent: Boolean(t.isParent),
parentTable: t.parentTable ?? null, parentTable: t.parentTable ?? null,
parentKey: t.parentKey ?? null, parentKey: t.parentKey ?? null,
displayFields: t.displayFields ?? [], displayFields: t.displayFields ?? [],
isGroup: Boolean(t.isGroup), isGroup: Boolean(t.isGroup),
group: t.group ?? null, group: t.group ?? null,
})); })).sort((a: FeatureTableNode, b: FeatureTableNode) => (a.label || a.tableName).localeCompare(b.label || b.tableName, undefined, { sensitivity: 'base' }));
// Default-expand all categorical groups so users immediately see their content. // Default-expand all categorical groups so users immediately see their content.
const defaultExpansions: string[] = tables const defaultExpansions: string[] = tables
@ -912,7 +919,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
displayLabel: r.displayLabel || r.id, displayLabel: r.displayLabel || r.id,
fields: r.fields || {}, fields: r.fields || {},
tableName: table.tableName, tableName: table.tableName,
})); })).sort((a: ParentRecordNode, b: ParentRecordNode) => a.displayLabel.localeCompare(b.displayLabel, undefined, { sensitivity: 'base' }));
if (mountedRef.current) { if (mountedRef.current) {
setFeatureRecordsByPath(prev => ({ ...prev, [pathKey]: records })); setFeatureRecordsByPath(prev => ({ ...prev, [pathKey]: records }));
} }
@ -1229,11 +1236,24 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
{node.label} {node.label}
</span> </span>
{/* Stable trio: chat | scope | neutralize (always in this order). <button
* No "remove from workspace" button here by design: the UDB row only onClick={async (e) => {
* exposes the catalog state. Detach from the *current chat* happens e.stopPropagation();
* via the chip "x" in WorkspaceInput; that chip is the single source if (ds) { onToggleRagIndex(ds); return; }
* of truth for chat-scoped attachment lifecycle. */} const newId = await onEnsureDs(node);
if (newId) {
try { await api.patch(`/api/datasources/${newId}/rag-index`, { ragIndexEnabled: !effectiveRagIndex }); } catch {}
}
}}
style={{
background: 'none', border: 'none', cursor: 'pointer',
fontSize: 12, padding: '0 2px', lineHeight: 1, flexShrink: 0, width: 22, textAlign: 'center',
opacity: (ds?.ragIndexEnabled ?? effectiveRagIndex) ? 1 : 0.35,
}}
title={(ds?.ragIndexEnabled ?? effectiveRagIndex) ? t('RAG-Indexierung an') : t('RAG-Indexierung aus')}
>
{'\uD83E\uDDE0'}
</button>
<button <button
onClick={e => { e.stopPropagation(); onSendToChat?.(_chatPayload); }} onClick={e => { e.stopPropagation(); onSendToChat?.(_chatPayload); }}
style={{ style={{
@ -1278,24 +1298,6 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
> >
{'\uD83D\uDD12'} {'\uD83D\uDD12'}
</button> </button>
<button
onClick={async (e) => {
e.stopPropagation();
if (ds) { onToggleRagIndex(ds); return; }
const newId = await onEnsureDs(node);
if (newId) {
try { await api.patch(`/api/datasources/${newId}/rag-index`, { ragIndexEnabled: !effectiveRagIndex }); } catch {}
}
}}
style={{
background: 'none', border: 'none', cursor: 'pointer',
fontSize: 12, padding: '0 2px', lineHeight: 1, flexShrink: 0, width: 22, textAlign: 'center',
opacity: (ds?.ragIndexEnabled ?? effectiveRagIndex) ? 1 : 0.35,
}}
title={(ds?.ragIndexEnabled ?? effectiveRagIndex) ? t('RAG-Indexierung an') : t('RAG-Indexierung aus')}
>
{'\uD83E\uDDE0'}
</button>
</div> </div>

View file

@ -415,6 +415,17 @@
padding: 1rem; padding: 1rem;
} }
.sessionSwitcherSelect {
width: 100%;
max-width: 420px;
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
border: 1px solid var(--border-color, #ccc);
border-radius: 8px;
background: var(--bg-primary, #fff);
color: var(--text-primary, #333);
}
/* ----- Session Layout (UDB Sidebar + Main) ------------------------------- */ /* ----- Session Layout (UDB Sidebar + Main) ------------------------------- */
.sessionLayout { .sessionLayout {

View file

@ -771,28 +771,39 @@ export const TeamsbotSessionView: React.FC = () => {
</div> </div>
)} )}
{/* Session Switcher (if multiple sessions exist) */} {/* Session Switcher */}
{allSessions.length > 1 && ( {allSessions.length > 1 && (
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px', flexWrap: 'wrap' }}> <div style={{ marginBottom: '12px' }}>
{allSessions.map((s) => ( <select
<button value={sessionId}
key={s.id} onChange={(e) => _switchSession(e.target.value)}
onClick={() => _switchSession(s.id)} className={styles.sessionSwitcherSelect}
style={{ >
padding: '6px 12px', {[...allSessions]
borderRadius: '6px', .sort((a, b) => {
border: s.id === sessionId ? '2px solid #4A90D9' : '1px solid #ddd', const ta = a.startedAt ? new Date(a.startedAt).getTime() : 0;
background: s.id === sessionId ? '#EBF3FC' : '#fff', const tb = b.startedAt ? new Date(b.startedAt).getTime() : 0;
cursor: 'pointer', return tb - ta;
fontSize: '13px', })
fontWeight: s.id === sessionId ? 600 : 400, .map((s) => {
}} const dt = s.startedAt ? new Date(s.startedAt) : null;
> const ts = dt && !Number.isNaN(dt.getTime())
{s.botName} ? `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')} ${String(dt.getHours()).padStart(2, '0')}:${String(dt.getMinutes()).padStart(2, '0')}`
{['active', 'joining', 'pending'].includes(s.status) && ' (aktiv)'} : '';
{s.status === 'ended' && ' (beendet)'} const statusTag = ['active', 'joining', 'pending'].includes(s.status)
</button> ? ` (${t('aktiv')})`
))} : s.status === 'ended'
? ` (${t('beendet')})`
: s.status === 'error'
? ` (${t('Fehler')})`
: '';
return (
<option key={s.id} value={s.id}>
{ts ? `${ts}` : ''}{s.botName}{statusTag}
</option>
);
})}
</select>
</div> </div>
)} )}