testing fixes, udb source handling fixes

This commit is contained in:
ValueOn AG 2026-04-17 11:50:25 +02:00
parent 74d0ce429a
commit 4c959538ac
30 changed files with 1039 additions and 443 deletions

View file

@ -620,7 +620,7 @@ export default function FolderTree({
expandedIds: externalExpandedIds, onToggleExpand,
onCreateFolder, onRenameFolder, onDeleteFolder, onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles,
onRenameFile, onDeleteFile, onDeleteFiles, onDeleteFolders, onRefresh, onDownloadFolder,
onScopeChange, onNeutralizeToggle,
onScopeChange, onNeutralizeToggle, onFolderNeutralizeToggle, onSendToChat,
}: FolderTreeProps) {
const { t } = useLanguage();

View file

@ -140,8 +140,8 @@ export const UserSection: React.FC = () => {
{/* Legal Modal */}
{showLegalModal && (
<div className={styles.modalOverlay} onClick={() => setShowLegalModal(false)}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2>{t('Legal notices')}</h2>
<button

View file

@ -0,0 +1,31 @@
.wrapper {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.icon {
font-size: 0.85rem;
opacity: 0.6;
flex-shrink: 0;
}
.select {
appearance: none;
background: transparent;
border: none;
color: inherit;
font-size: 0.8rem;
font-family: inherit;
cursor: pointer;
padding: 0.15rem 0.3rem;
border-radius: 4px;
opacity: 0.7;
transition: opacity 0.15s;
}
.select:hover,
.select:focus {
opacity: 1;
outline: none;
}

View file

@ -0,0 +1,29 @@
import React from 'react';
import { FaGlobe } from 'react-icons/fa';
import { useLanguage } from '../../../providers/language/LanguageContext';
import styles from './LanguageSelector.module.css';
export function LanguageSelector() {
const { currentLanguage, setLanguage, availableLanguages } = useLanguage();
if (availableLanguages.length <= 1) return null;
return (
<div className={styles.wrapper}>
<FaGlobe className={styles.icon} />
<select
className={styles.select}
value={currentLanguage}
onChange={(e) => setLanguage(e.target.value as typeof currentLanguage)}
>
{availableLanguages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.label || lang.code.toUpperCase()}
</option>
))}
</select>
</div>
);
}
export default LanguageSelector;

View file

@ -0,0 +1,2 @@
export { LanguageSelector } from './LanguageSelector';
export { default } from './LanguageSelector';

View file

@ -23,6 +23,8 @@ export interface PopupProps {
className?: string;
size?: 'small' | 'medium' | 'large' | 'fullscreen';
closable?: boolean;
closeOnBackdropClick?: boolean;
closeOnEscape?: boolean;
actions?: PopupAction[];
}
@ -36,6 +38,8 @@ export function Popup({
className = '',
size = 'medium',
closable = true,
closeOnBackdropClick = false,
closeOnEscape = true,
actions = []
}: PopupProps) {
const { t } = useLanguage();
@ -43,7 +47,7 @@ export function Popup({
// Handle escape key
React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && closable) {
if (e.key === 'Escape' && closable && closeOnEscape) {
onClose();
}
};
@ -58,13 +62,13 @@ export function Popup({
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isOpen, closable, onClose]);
}, [isOpen, closable, closeOnEscape, onClose]);
if (!isOpen) return null;
// Handle backdrop click
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget && closable) {
if (e.target === e.currentTarget && closable && closeOnBackdropClick) {
onClose();
}
};

File diff suppressed because it is too large Load diff

View file

@ -43,6 +43,7 @@ interface UnifiedDataBarProps {
onSourcesChanged?: () => void;
onSendToChat_Files?: (items: AddToChat_FileItem[]) => void;
onSendToChat_FeatureSource?: (params: AddToChat_FeatureSource) => void;
onAttachDataSource?: (dsId: string) => void;
className?: string;
}
@ -70,6 +71,7 @@ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
onSourcesChanged,
onSendToChat_Files,
onSendToChat_FeatureSource,
onAttachDataSource,
className,
}) => {
const { t } = useLanguage();
@ -121,6 +123,7 @@ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
context={context}
onSourcesChanged={onSourcesChanged}
onSendToChat_FeatureSource={onSendToChat_FeatureSource}
onAttachDataSource={onAttachDataSource}
/>
)}
</div>

View file

@ -1,3 +1,3 @@
export { default as UnifiedDataBar } from './UnifiedDataBar';
export type { UdbContext, UdbTab } from './UnifiedDataBar';
export type { UdbContext, UdbTab, AddToChat_FileItem, AddToChat_FeatureSource } from './UnifiedDataBar';
export { useUdlContext } from './useUdlContext';

View file

@ -815,8 +815,8 @@ export const ComplianceAuditPage: React.FC = () => {
{/* ── Content View Modal ── */}
{contentModal && (
<div className={styles.modalOverlay} onClick={() => setContentModal(null)}>
<div className={styles.modalContainer} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modalContainer}>
<div className={styles.modalHeader}>
<h3 className={styles.modalTitle}>{t('AI-Audit Inhalt')}</h3>
<div className={styles.modalMeta}>

View file

@ -8,7 +8,7 @@ import { PENDING_INVITATION_KEY } from './InvitePage';
import OnboardingWizard from '../components/OnboardingWizard';
import styles from './Login.module.css';
import { LanguageSelector } from '../components/UiComponents/LanguageSelector';
import { useLanguage } from '../providers/language/LanguageContext';
@ -131,6 +131,9 @@ function Login() {
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}>
<img
src="/logos/poweron-logo.png"

View file

@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
import styles from './PasswordResetRequest.module.css';
import { usePasswordResetRequest } from '../hooks/useAuthentication';
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
import { LanguageSelector } from '../components/UiComponents/LanguageSelector';
import { useLanguage } from '../providers/language/LanguageContext';
@ -57,6 +58,9 @@ function PasswordResetRequest() {
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}>
<img
src="/logos/poweron-logo.png"

View file

@ -6,6 +6,7 @@ import styles from './Register.module.css';
import { useRegister, useMsalRegister, useUsernameAvailability } from '../hooks/useAuthentication';
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
import { PENDING_INVITATION_KEY } from './InvitePage';
import { LanguageSelector } from '../components/UiComponents/LanguageSelector';
import { useLanguage } from '../providers/language/LanguageContext';
@ -16,7 +17,7 @@ interface RegisterFormData {
}
function Register() {
const { t } = useLanguage();
const { t, currentLanguage } = useLanguage();
const navigate = useNavigate();
const location = useLocation();
const { register, error: registerError, isLoading } = useRegister();
@ -91,7 +92,7 @@ function Register() {
return;
}
await register({ ...formData, registrationType: 'personal' });
await register({ ...formData, language: currentLanguage, registrationType: 'personal' });
let message = t('Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail (auch den Spam-Ordner) für den Link zum Setzen Ihres Passworts.');
if (hasPendingInvitation) {
@ -125,6 +126,9 @@ function Register() {
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}>
<img
src="/logos/poweron-logo.png"

View file

@ -4,6 +4,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import styles from './Reset.module.css';
import { usePasswordReset } from '../hooks/useAuthentication';
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
import { LanguageSelector } from '../components/UiComponents/LanguageSelector';
import { useLanguage } from '../providers/language/LanguageContext';
@ -98,6 +99,9 @@ function Reset() {
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}>
<img
src="/logos/poweron-logo.png"
@ -138,6 +142,9 @@ function Reset() {
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}>
<img
src="/logos/poweron-logo.png"

View file

@ -5,7 +5,7 @@
*/
import React from 'react';
import { FaCogs, FaComments, FaHeadset, FaProjectDiagram } from 'react-icons/fa';
import { FaCogs, FaComments, FaHeadset, FaProjectDiagram, FaShieldAlt } from 'react-icons/fa';
import { useLanguage } from '../providers/language/LanguageContext';
import { useStore } from '../hooks/useStore';
import type { StoreFeature, UserMandate } from '../api/storeApi';
@ -18,6 +18,7 @@ const FEATURE_ICONS: Record<string, React.ReactNode> = {
teamsbot: <FaHeadset />,
workspace: <FaComments />,
commcoach: <FaComments />,
trustee: <FaShieldAlt />,
};
/** Fallback when GET /store/features omits description (German i18n keys). */
@ -27,6 +28,7 @@ const STORE_FEATURE_DESCRIPTION_FALLBACK: Record<string, string> = {
teamsbot: 'Integriere einen AI-Bot in deine Microsoft Teams Meetings und Channels.',
workspace: 'Nutze den gemeinsamen AI Workspace: Chats, Tools und Kontext pro Instanz.',
commcoach: 'CommCoach: Kommunikation trainieren mit KI-gestütztem Coaching und Feedback.',
trustee: 'Trustee: Intelligentes Dokumentenmanagement mit KI-gestützter Analyse und Verarbeitung.',
};
function _storeCardDescription(feature: StoreFeature): string {

View file

@ -15,7 +15,6 @@ import { FaPlus, FaSync, FaCube, FaBuilding, FaCogs, FaEdit } from 'react-icons/
import { useToast } from '../../contexts/ToastContext';
import api from '../../api';
import { ChatbotConfigSection } from './ChatbotConfigSection';
import { DropdownSelect } from '../../components/UiComponents/DropdownSelect';
import { TextField } from '../../components/UiComponents/TextField';
import styles from './Admin.module.css';
@ -512,8 +511,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
{/* Create Instance Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neue Feature-Instanz erstellen')}</h2>
<button
@ -533,35 +532,38 @@ export const AdminFeatureAccessPage: React.FC = () => {
</div>
) : (
<div>
{/* Feature Code Selector - Required for chatbot config */}
{/* Feature Code Selector — buttons instead of dropdown */}
<div className={styles.configField} style={{ marginBottom: '1.5rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border-color)' }}>
<label className={styles.configLabel} style={{ fontWeight: 600 }}>
{t('Feature auswählen')}: <span style={{ color: 'var(--error-color)' }}>*</span>
</label>
<DropdownSelect
items={features.map(f => ({
id: f.code,
label: f.label || f.code,
value: f.code
}))}
selectedItemId={createFeatureCode}
onSelect={(item) => {
const selectedCode = item?.value || '';
setCreateFeatureCode(selectedCode);
// Reset chatbot config when switching
setChatbotConnectors(['preprocessor']);
setChatbotSystemPrompt('');
setChatbotEnableWebResearch(true);
setChatbotAllowedProviders([]);
}}
placeholder={t('Feature-Auswahl erforderlich')}
className={styles.configSelect}
/>
{!createFeatureCode && (
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '0.5rem' }}>
{t('Bitte wählen Sie ein Feature aus, um fortzufahren.')}
</p>
)}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginTop: '0.5rem' }}>
{features.map(f => (
<button
key={f.code}
type="button"
className={styles.secondaryButton}
style={{
padding: '0.5rem 1rem',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: createFeatureCode === f.code ? 600 : 400,
background: createFeatureCode === f.code ? 'var(--primary-color)' : undefined,
color: createFeatureCode === f.code ? '#fff' : undefined,
borderColor: createFeatureCode === f.code ? 'var(--primary-color)' : undefined,
}}
onClick={() => {
setCreateFeatureCode(f.code);
setChatbotConnectors(['preprocessor']);
setChatbotSystemPrompt('');
setChatbotEnableWebResearch(true);
setChatbotAllowedProviders([]);
}}
>
{f.label || f.code}
</button>
))}
</div>
</div>
{/* Chatbot Configuration Title - Show when chatbot is selected */}
@ -634,8 +636,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
{/* Edit Instance Modal */}
{showEditModal && editingInstance && (
<div className={styles.modalOverlay} onClick={() => { setShowEditModal(false); setEditingInstance(null); }}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Feature-Instanz bearbeiten')}</h2>
<button

View file

@ -561,8 +561,8 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
{/* Add User Modal */}
{showAddModal && (
<div className={styles.modalOverlay} onClick={() => setShowAddModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Benutzer zur Feature-Instanz hinzufügen')}</h2>
<button
@ -594,8 +594,8 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
{/* Edit Roles Modal */}
{editingUser && (
<div className={styles.modalOverlay} onClick={() => setEditingUser(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>
{t('Rollen bearbeiten')}: {editingUser.username}

View file

@ -397,8 +397,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
{/* Create Role Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neue Feature-Rolle erstellen')}</h2>
<button
@ -430,8 +430,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
{/* Edit Role Modal */}
{editingRole && (
<div className={styles.modalOverlay} onClick={() => setEditingRole(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Feature-Rolle bearbeiten')}</h2>
<button
@ -462,8 +462,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
{/* Permissions Modal */}
{permissionsRole && (
<div className={styles.modalOverlay} onClick={() => setPermissionsRole(null)}>
<div className={styles.modal} style={{ maxWidth: '900px', width: '90%' }} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal} style={{ maxWidth: '900px', width: '90%' }}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>
<FaShieldAlt style={{ marginRight: 8 }} />

View file

@ -372,8 +372,8 @@ export const AdminInvitationsPage: React.FC = () => {
{/* Create Invitation Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neue Einladung erstellen')}</h2>
<button
@ -411,8 +411,8 @@ export const AdminInvitationsPage: React.FC = () => {
{/* URL Display Modal */}
{showUrlModal && (
<div className={styles.modalOverlay} onClick={() => setShowUrlModal(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Einladungs-Link')}</h2>
<button

View file

@ -434,8 +434,8 @@ export const AdminMandateRolesPage: React.FC = () => {
{/* Create Role Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neue Rolle erstellen')}</h2>
<button
@ -468,8 +468,8 @@ export const AdminMandateRolesPage: React.FC = () => {
{/* Edit Role Modal */}
{editingRole && (
<div className={styles.modalOverlay} onClick={() => setEditingRole(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>
{t('Rolle bearbeiten')}: {editingRole.roleLabel}

View file

@ -4,7 +4,7 @@
* Admin page for managing Mandates (tenants) using FormGeneratorTable.
*/
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAdminMandates, useMandateFormAttributes, type Mandate } from '../../hooks/useMandates';
import { useApiRequest } from '../../hooks/useApi';
@ -16,8 +16,9 @@ import {
import { useToast } from '../../contexts/ToastContext';
import { usePrompt } from '../../hooks/usePrompt';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaUsers, FaLock, FaSkullCrossbones } from 'react-icons/fa';
import { getUserDataCache } from '../../utils/userCache';
import styles from './Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
@ -59,6 +60,17 @@ export const AdminMandatesPage: React.FC = () => {
const [editingFormData, setEditingFormData] = useState<Record<string, unknown> | null>(null);
const [editingBillingWarning, setEditingBillingWarning] = useState<string | null>(null);
const isSysAdmin = getUserDataCache()?.isSysAdmin === true;
// MandateAdmin: only label + billing fields editable; rest readonly
const _MANDATE_ADMIN_EDITABLE = new Set(['label', 'warningThresholdPercent', 'notifyOnWarning', 'notifyEmails']);
const editFormAttrs: AttributeDefinition[] = useMemo(() => {
if (isSysAdmin) return formAttributesWithBilling;
return formAttributesWithBilling.map(attr =>
_MANDATE_ADMIN_EDITABLE.has(attr.name) ? attr : { ...attr, editable: false, readonly: true }
);
}, [formAttributesWithBilling, isSysAdmin]);
// Check if user can create
const canCreate = permissions?.create !== 'n';
const canUpdate = permissions?.update !== 'n';
@ -106,7 +118,10 @@ export const AdminMandatesPage: React.FC = () => {
const mandateId = String(editingFormData.id);
const { mandatePayload, billingUpdate } = splitMandateAndBillingFromForm(data);
const mandateOk = await handleUpdate(mandateId, mandatePayload as Partial<Mandate>);
if (!mandateOk) return;
if (!mandateOk) {
showWarning(t('Fehler'), t('Mandant konnte nicht gespeichert werden. Fehlende Berechtigung oder Serverfehler.'));
return;
}
try {
await updateSettingsAdmin(request, mandateId, billingUpdate);
showSuccess(t('Gespeichert'), t('Mandant und Abrechnung aktualisiert.'));
@ -253,8 +268,8 @@ export const AdminMandatesPage: React.FC = () => {
{/* Create Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neuer Mandant')}</h2>
<button
@ -293,14 +308,8 @@ export const AdminMandatesPage: React.FC = () => {
{/* Edit Modal */}
{editingFormData && (
<div
className={styles.modalOverlay}
onClick={() => {
setEditingFormData(null);
setEditingBillingWarning(null);
}}
>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Mandant bearbeiten')}</h2>
<button
@ -338,7 +347,7 @@ export const AdminMandatesPage: React.FC = () => {
</div>
) : (
<FormGeneratorForm
attributes={formAttributesWithBilling}
attributes={editFormAttrs}
data={editingFormData}
mode="edit"
onSubmit={handleEditSubmit}

View file

@ -375,8 +375,8 @@ export const AdminUserMandatesPage: React.FC = () => {
{/* Add User Modal */}
{showAddModal && (
<div className={styles.modalOverlay} onClick={() => setShowAddModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Benutzer zum Mandanten hinzufügen')}</h2>
<button
@ -411,8 +411,8 @@ export const AdminUserMandatesPage: React.FC = () => {
{/* Edit Roles Modal */}
{editingUser && (
<div className={styles.modalOverlay} onClick={() => setEditingUser(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>
{t('Rollen bearbeiten')}: {editingUser.username}

View file

@ -230,8 +230,8 @@ export const AdminUsersPage: React.FC = () => {
{/* Create Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neuer Benutzer')}</h2>
<button
@ -264,8 +264,8 @@ export const AdminUsersPage: React.FC = () => {
{/* Edit Modal */}
{editingUser && (
<div className={styles.modalOverlay} onClick={() => setEditingUser(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Benutzer bearbeiten')}</h2>
<button

View file

@ -311,8 +311,8 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instan
</div>
{showAddModal && (
<div className={styles.modalOverlay} onClick={() => setShowAddModal(false)}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Benutzer hinzufügen')}</h2>
<button type="button" className={styles.modalClose} onClick={() => setShowAddModal(false)}>
@ -340,8 +340,8 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instan
)}
{editingUser && (
<div className={styles.modalOverlay} onClick={() => setEditingUser(null)}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>
{t('Rollen')}: {editingUser.username}

View file

@ -365,8 +365,8 @@ export const ConnectionsPage: React.FC = () => {
{/* Edit Modal */}
{editingConnection && (
<div className={styles.modalOverlay} onClick={() => setEditingConnection(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Verbindung bearbeiten')}</h2>
<button

View file

@ -511,8 +511,8 @@ export const FilesPage: React.FC = () => {
</div>
{editingFile && (
<div className={styles.modalOverlay} onClick={() => setEditingFile(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Datei bearbeiten')}</h2>
<button className={styles.modalClose} onClick={() => setEditingFile(null)}></button>

View file

@ -230,8 +230,8 @@ export const PromptsPage: React.FC = () => {
{/* Create Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neuer Prompt')}</h2>
<button
@ -264,8 +264,8 @@ export const PromptsPage: React.FC = () => {
{/* Edit Modal */}
{editingPrompt && (
<div className={styles.modalOverlay} onClick={() => setEditingPrompt(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Prompt bearbeiten')}</h2>
<button

View file

@ -249,8 +249,8 @@ export const TrusteePositionDocumentsView: React.FC = () => {
{/* Edit Modal */}
{editingLink && (
<div className={styles.modalOverlay} onClick={() => setEditingLink(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Verknüpfung bearbeiten')}</h2>
<button

View file

@ -55,6 +55,10 @@ interface WorkspaceInputProps {
onProviderSelectionChange?: (selection: ProviderSelection) => void;
isMobile?: boolean;
onTreeItemsDrop?: (items: TreeItemDrop[]) => void;
onFeatureSourceDrop?: (params: { featureInstanceId: string; featureCode: string; tableName?: string; objectKey: string; label: string; fieldName?: string }) => void;
onDataSourceDrop?: (params: { connectionId: string; sourceType: string; path: string; label: string; displayPath?: string }) => void;
pendingAttachDsId?: string;
onPendingAttachDsConsumed?: () => void;
onPasteAsFile?: (file: File) => void;
draftAppend?: string;
onDraftAppendConsumed?: () => void;
@ -75,6 +79,10 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
onProviderSelectionChange,
isMobile = false,
onTreeItemsDrop,
onFeatureSourceDrop,
onDataSourceDrop,
pendingAttachDsId,
onPendingAttachDsConsumed,
onPasteAsFile,
draftAppend,
onDraftAppendConsumed,
@ -101,6 +109,15 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
}
}, [draftAppend, onDraftAppendConsumed]);
useEffect(() => {
if (pendingAttachDsId) {
setAttachedDataSourceIds(prev =>
prev.includes(pendingAttachDsId) ? prev : [...prev, pendingAttachDsId],
);
onPendingAttachDsConsumed?.();
}
}, [pendingAttachDsId, onPendingAttachDsConsumed]);
const promptBeforeVoiceRef = useRef('');
const finalizedTextRef = useRef('');
const currentInterimRef = useRef('');
@ -142,7 +159,6 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
onSend(trimmed, allFileIds, attachedDataSourceIds, attachedFeatureDataSourceIds, options);
setPrompt('');
setShowAutocomplete(false);
setShowSourcePicker(false);
setAttachedFileIds([]);
}, [prompt, isProcessing, _extractFileRefs, attachedFileIds, attachedDataSourceIds, attachedFeatureDataSourceIds, neutralizeActive, onSend]);
@ -197,14 +213,6 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
setAttachedDataSourceIds(prev => prev.filter(id => id !== dsId));
}, []);
const [showSourcePicker, setShowSourcePicker] = useState(false);
const _toggleDataSource = useCallback((dsId: string) => {
setAttachedDataSourceIds(prev =>
prev.includes(dsId) ? prev.filter(id => id !== dsId) : [...prev, dsId],
);
}, []);
const _toggleFeatureDataSource = useCallback((fdsId: string) => {
setAttachedFeatureDataSourceIds(prev =>
prev.includes(fdsId) ? prev.filter(id => id !== fdsId) : [...prev, fdsId],
@ -288,7 +296,9 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
const _handlePromptDragOver = useCallback((e: React.DragEvent) => {
if (
e.dataTransfer.types.includes('application/tree-items') ||
e.dataTransfer.types.includes('application/chat-id')
e.dataTransfer.types.includes('application/chat-id') ||
e.dataTransfer.types.includes('application/feature-source') ||
e.dataTransfer.types.includes('application/datasource')
) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
@ -311,6 +321,24 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
return;
}
const featureSourceJson = e.dataTransfer.getData('application/feature-source');
if (featureSourceJson && onFeatureSourceDrop) {
e.preventDefault();
e.stopPropagation();
const params = JSON.parse(featureSourceJson);
onFeatureSourceDrop(params);
return;
}
const dataSourceJson = e.dataTransfer.getData('application/datasource');
if (dataSourceJson && onDataSourceDrop) {
e.preventDefault();
e.stopPropagation();
const params = JSON.parse(dataSourceJson);
onDataSourceDrop(params);
return;
}
const treeItemsJson = e.dataTransfer.getData('application/tree-items');
if (treeItemsJson && onTreeItemsDrop) {
e.preventDefault();
@ -318,7 +346,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
const items: TreeItemDrop[] = JSON.parse(treeItemsJson);
onTreeItemsDrop(items);
}
}, [onTreeItemsDrop]);
}, [onTreeItemsDrop, onFeatureSourceDrop, onDataSourceDrop]);
return (
<div

View file

@ -308,6 +308,30 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
}
}, [instanceId, workspace]);
const [pendingAttachDsId, setPendingAttachDsId] = useState<string>('');
const _handleAttachDataSource = useCallback((dsId: string) => {
setPendingAttachDsId(dsId);
}, []);
const _handleDataSourceDrop = useCallback(async (params: { connectionId: string; sourceType: string; path: string; label: string; displayPath?: string }) => {
try {
const res = await api.post(`/api/workspace/${instanceId}/datasources`, {
connectionId: params.connectionId,
sourceType: params.sourceType,
path: params.path,
label: params.label,
displayPath: params.displayPath || params.label,
});
const newId = res.data?.id || res.data?.dataSource?.id;
if (newId) {
setPendingAttachDsId(newId);
workspace.refreshDataSources();
}
} catch (err) {
console.error('Failed to drop data source to chat:', err);
}
}, [instanceId, workspace]);
const _leftPanelBody = (
<UnifiedDataBar
context={_udbContext}
@ -322,6 +346,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
onSourcesChanged={_handleSourcesChanged}
onSendToChat_Files={_handleSendToChat_Files}
onSendToChat_FeatureSource={_handleSendToChat_FeatureSource}
onAttachDataSource={_handleAttachDataSource}
/>
);
@ -492,6 +517,10 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
onProviderSelectionChange={setProviderSelection}
isMobile={isMobile}
onTreeItemsDrop={_handleTreeItemsDrop}
onFeatureSourceDrop={_handleSendToChat_FeatureSource}
onDataSourceDrop={_handleDataSourceDrop}
pendingAttachDsId={pendingAttachDsId}
onPendingAttachDsConsumed={() => setPendingAttachDsId('')}
onPasteAsFile={_uploadAndAttach}
draftAppend={draftAppend}
onDraftAppendConsumed={() => setDraftAppend('')}