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);
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;
}
@ -651,10 +658,14 @@ export function FormGeneratorTree<T = any>({
const _handleCycleScope = useCallback(
async (node: TreeNode<T>) => {
const newScope = _nextScope(node.scope);
await provider.patchScope?.([node.id], newScope);
setNodes((prev) =>
prev.map((n) => (n.id === node.id ? { ...n, scope: newScope } : n)),
);
const isFolder = node.type === 'folder';
await provider.patchScope?.([node.id], newScope, isFolder);
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],
);
@ -663,9 +674,12 @@ export function FormGeneratorTree<T = any>({
async (node: TreeNode<T>) => {
const newValue = !node.neutralize;
await provider.patchNeutralize?.([node.id], newValue);
setNodes((prev) =>
prev.map((n) => (n.id === node.id ? { ...n, neutralize: newValue } : n)),
);
setNodes((prev) => {
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],
);

View file

@ -343,7 +343,7 @@ async function _loadServices(instanceId: string, connectionId: string): Promise<
service: s.service,
path: '/',
displayPath: s.label || s.service,
}));
})).sort((a: TreeNode, b: TreeNode) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }));
}
async function _browseService(
@ -375,6 +375,10 @@ async function _browseService(
path: entry.path,
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',
neutralize: d.neutralize ?? false,
}));
list.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }));
setDataSources(list);
})
.catch(() => { if (mountedRef.current) setDataSources([]); });
@ -519,6 +524,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
neutralizeFields: d.neutralizeFields || undefined,
recordFilter: d.recordFilter || undefined,
}));
list.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }));
setFeatureDataSources(list);
})
.catch(() => { if (mountedRef.current) setFeatureDataSources([]); });
@ -547,7 +553,8 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
children: null,
connectionId: c.id,
authority: c.authority,
}));
}))
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }));
setTree(nodes);
})
.catch(() => { if (mountedRef.current) setTree([]); })
@ -760,7 +767,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
expanded: false,
loading: false,
tables: null,
})),
})).sort((a: FeatureConnectionNode, b: FeatureConnectionNode) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })),
})));
})
.catch(() => { if (mountedRef.current) setFeatureTree([]); })
@ -798,14 +805,14 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
objectKey: t.objectKey ?? '',
tableName: t.tableName ?? '',
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),
parentTable: t.parentTable ?? null,
parentKey: t.parentKey ?? null,
displayFields: t.displayFields ?? [],
isGroup: Boolean(t.isGroup),
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.
const defaultExpansions: string[] = tables
@ -912,7 +919,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged, onSe
displayLabel: r.displayLabel || r.id,
fields: r.fields || {},
tableName: table.tableName,
}));
})).sort((a: ParentRecordNode, b: ParentRecordNode) => a.displayLabel.localeCompare(b.displayLabel, undefined, { sensitivity: 'base' }));
if (mountedRef.current) {
setFeatureRecordsByPath(prev => ({ ...prev, [pathKey]: records }));
}
@ -1229,11 +1236,24 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
{node.label}
</span>
{/* Stable trio: chat | scope | neutralize (always in this order).
* No "remove from workspace" button here by design: the UDB row only
* exposes the catalog state. Detach from the *current chat* happens
* via the chip "x" in WorkspaceInput; that chip is the single source
* of truth for chat-scoped attachment lifecycle. */}
<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>
<button
onClick={e => { e.stopPropagation(); onSendToChat?.(_chatPayload); }}
style={{
@ -1278,24 +1298,6 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
>
{'\uD83D\uDD12'}
</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>

View file

@ -415,6 +415,17 @@
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) ------------------------------- */
.sessionLayout {

View file

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