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, expandedIds: externalExpandedIds, onToggleExpand,
onCreateFolder, onRenameFolder, onDeleteFolder, onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles, onCreateFolder, onRenameFolder, onDeleteFolder, onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles,
onRenameFile, onDeleteFile, onDeleteFiles, onDeleteFolders, onRefresh, onDownloadFolder, onRenameFile, onDeleteFile, onDeleteFiles, onDeleteFolders, onRefresh, onDownloadFolder,
onScopeChange, onNeutralizeToggle, onScopeChange, onNeutralizeToggle, onFolderNeutralizeToggle, onSendToChat,
}: FolderTreeProps) { }: FolderTreeProps) {
const { t } = useLanguage(); const { t } = useLanguage();

View file

@ -140,8 +140,8 @@ export const UserSection: React.FC = () => {
{/* Legal Modal */} {/* Legal Modal */}
{showLegalModal && ( {showLegalModal && (
<div className={styles.modalOverlay} onClick={() => setShowLegalModal(false)}> <div className={styles.modalOverlay}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}> <div className={styles.modal}>
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<h2>{t('Legal notices')}</h2> <h2>{t('Legal notices')}</h2>
<button <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; className?: string;
size?: 'small' | 'medium' | 'large' | 'fullscreen'; size?: 'small' | 'medium' | 'large' | 'fullscreen';
closable?: boolean; closable?: boolean;
closeOnBackdropClick?: boolean;
closeOnEscape?: boolean;
actions?: PopupAction[]; actions?: PopupAction[];
} }
@ -36,6 +38,8 @@ export function Popup({
className = '', className = '',
size = 'medium', size = 'medium',
closable = true, closable = true,
closeOnBackdropClick = false,
closeOnEscape = true,
actions = [] actions = []
}: PopupProps) { }: PopupProps) {
const { t } = useLanguage(); const { t } = useLanguage();
@ -43,7 +47,7 @@ export function Popup({
// Handle escape key // Handle escape key
React.useEffect(() => { React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => { const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && closable) { if (e.key === 'Escape' && closable && closeOnEscape) {
onClose(); onClose();
} }
}; };
@ -58,13 +62,13 @@ export function Popup({
document.removeEventListener('keydown', handleEscape); document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset'; document.body.style.overflow = 'unset';
}; };
}, [isOpen, closable, onClose]); }, [isOpen, closable, closeOnEscape, onClose]);
if (!isOpen) return null; if (!isOpen) return null;
// Handle backdrop click // Handle backdrop click
const handleBackdropClick = (e: React.MouseEvent) => { const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget && closable) { if (e.target === e.currentTarget && closable && closeOnBackdropClick) {
onClose(); onClose();
} }
}; };

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,3 +1,3 @@
export { default as UnifiedDataBar } from './UnifiedDataBar'; 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'; export { useUdlContext } from './useUdlContext';

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
import styles from './PasswordResetRequest.module.css'; import styles from './PasswordResetRequest.module.css';
import { usePasswordResetRequest } from '../hooks/useAuthentication'; import { usePasswordResetRequest } from '../hooks/useAuthentication';
import { generateAndStoreCSRFToken } from '../utils/csrfUtils'; import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
import { LanguageSelector } from '../components/UiComponents/LanguageSelector';
import { useLanguage } from '../providers/language/LanguageContext'; import { useLanguage } from '../providers/language/LanguageContext';
@ -57,6 +58,9 @@ function PasswordResetRequest() {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.mainContent}> <div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}> <div className={styles.logo}>
<img <img
src="/logos/poweron-logo.png" 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 { useRegister, useMsalRegister, useUsernameAvailability } from '../hooks/useAuthentication';
import { generateAndStoreCSRFToken } from '../utils/csrfUtils'; import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
import { PENDING_INVITATION_KEY } from './InvitePage'; import { PENDING_INVITATION_KEY } from './InvitePage';
import { LanguageSelector } from '../components/UiComponents/LanguageSelector';
import { useLanguage } from '../providers/language/LanguageContext'; import { useLanguage } from '../providers/language/LanguageContext';
@ -16,7 +17,7 @@ interface RegisterFormData {
} }
function Register() { function Register() {
const { t } = useLanguage(); const { t, currentLanguage } = useLanguage();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { register, error: registerError, isLoading } = useRegister(); const { register, error: registerError, isLoading } = useRegister();
@ -91,7 +92,7 @@ function Register() {
return; 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.'); 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) { if (hasPendingInvitation) {
@ -125,6 +126,9 @@ function Register() {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.mainContent}> <div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}> <div className={styles.logo}>
<img <img
src="/logos/poweron-logo.png" 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 styles from './Reset.module.css';
import { usePasswordReset } from '../hooks/useAuthentication'; import { usePasswordReset } from '../hooks/useAuthentication';
import { generateAndStoreCSRFToken } from '../utils/csrfUtils'; import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
import { LanguageSelector } from '../components/UiComponents/LanguageSelector';
import { useLanguage } from '../providers/language/LanguageContext'; import { useLanguage } from '../providers/language/LanguageContext';
@ -98,6 +99,9 @@ function Reset() {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.mainContent}> <div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}> <div className={styles.logo}>
<img <img
src="/logos/poweron-logo.png" src="/logos/poweron-logo.png"
@ -138,6 +142,9 @@ function Reset() {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.mainContent}> <div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}> <div className={styles.logo}>
<img <img
src="/logos/poweron-logo.png" src="/logos/poweron-logo.png"

View file

@ -5,7 +5,7 @@
*/ */
import React from 'react'; 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 { useLanguage } from '../providers/language/LanguageContext';
import { useStore } from '../hooks/useStore'; import { useStore } from '../hooks/useStore';
import type { StoreFeature, UserMandate } from '../api/storeApi'; import type { StoreFeature, UserMandate } from '../api/storeApi';
@ -18,6 +18,7 @@ const FEATURE_ICONS: Record<string, React.ReactNode> = {
teamsbot: <FaHeadset />, teamsbot: <FaHeadset />,
workspace: <FaComments />, workspace: <FaComments />,
commcoach: <FaComments />, commcoach: <FaComments />,
trustee: <FaShieldAlt />,
}; };
/** Fallback when GET /store/features omits description (German i18n keys). */ /** 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.', teamsbot: 'Integriere einen AI-Bot in deine Microsoft Teams Meetings und Channels.',
workspace: 'Nutze den gemeinsamen AI Workspace: Chats, Tools und Kontext pro Instanz.', workspace: 'Nutze den gemeinsamen AI Workspace: Chats, Tools und Kontext pro Instanz.',
commcoach: 'CommCoach: Kommunikation trainieren mit KI-gestütztem Coaching und Feedback.', 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 { 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 { useToast } from '../../contexts/ToastContext';
import api from '../../api'; import api from '../../api';
import { ChatbotConfigSection } from './ChatbotConfigSection'; import { ChatbotConfigSection } from './ChatbotConfigSection';
import { DropdownSelect } from '../../components/UiComponents/DropdownSelect';
import { TextField } from '../../components/UiComponents/TextField'; import { TextField } from '../../components/UiComponents/TextField';
import styles from './Admin.module.css'; import styles from './Admin.module.css';
@ -512,8 +511,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
{/* Create Instance Modal */} {/* Create Instance Modal */}
{showCreateModal && ( {showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}> <div className={styles.modalOverlay}>
<div className={styles.modal} onClick={e => e.stopPropagation()}> <div className={styles.modal}>
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neue Feature-Instanz erstellen')}</h2> <h2 className={styles.modalTitle}>{t('Neue Feature-Instanz erstellen')}</h2>
<button <button
@ -533,35 +532,38 @@ export const AdminFeatureAccessPage: React.FC = () => {
</div> </div>
) : ( ) : (
<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)' }}> <div className={styles.configField} style={{ marginBottom: '1.5rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border-color)' }}>
<label className={styles.configLabel} style={{ fontWeight: 600 }}> <label className={styles.configLabel} style={{ fontWeight: 600 }}>
{t('Feature auswählen')}: <span style={{ color: 'var(--error-color)' }}>*</span> {t('Feature auswählen')}: <span style={{ color: 'var(--error-color)' }}>*</span>
</label> </label>
<DropdownSelect <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginTop: '0.5rem' }}>
items={features.map(f => ({ {features.map(f => (
id: f.code, <button
label: f.label || f.code, key={f.code}
value: f.code type="button"
}))} className={styles.secondaryButton}
selectedItemId={createFeatureCode} style={{
onSelect={(item) => { padding: '0.5rem 1rem',
const selectedCode = item?.value || ''; borderRadius: '6px',
setCreateFeatureCode(selectedCode); cursor: 'pointer',
// Reset chatbot config when switching fontWeight: createFeatureCode === f.code ? 600 : 400,
setChatbotConnectors(['preprocessor']); background: createFeatureCode === f.code ? 'var(--primary-color)' : undefined,
setChatbotSystemPrompt(''); color: createFeatureCode === f.code ? '#fff' : undefined,
setChatbotEnableWebResearch(true); borderColor: createFeatureCode === f.code ? 'var(--primary-color)' : undefined,
setChatbotAllowedProviders([]); }}
}} onClick={() => {
placeholder={t('Feature-Auswahl erforderlich')} setCreateFeatureCode(f.code);
className={styles.configSelect} setChatbotConnectors(['preprocessor']);
/> setChatbotSystemPrompt('');
{!createFeatureCode && ( setChatbotEnableWebResearch(true);
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '0.5rem' }}> setChatbotAllowedProviders([]);
{t('Bitte wählen Sie ein Feature aus, um fortzufahren.')} }}
</p> >
)} {f.label || f.code}
</button>
))}
</div>
</div> </div>
{/* Chatbot Configuration Title - Show when chatbot is selected */} {/* Chatbot Configuration Title - Show when chatbot is selected */}
@ -634,8 +636,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
{/* Edit Instance Modal */} {/* Edit Instance Modal */}
{showEditModal && editingInstance && ( {showEditModal && editingInstance && (
<div className={styles.modalOverlay} onClick={() => { setShowEditModal(false); setEditingInstance(null); }}> <div className={styles.modalOverlay}>
<div className={styles.modal} onClick={e => e.stopPropagation()}> <div className={styles.modal}>
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Feature-Instanz bearbeiten')}</h2> <h2 className={styles.modalTitle}>{t('Feature-Instanz bearbeiten')}</h2>
<button <button

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@
* Admin page for managing Mandates (tenants) using FormGeneratorTable. * 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 { useNavigate } from 'react-router-dom';
import { useAdminMandates, useMandateFormAttributes, type Mandate } from '../../hooks/useMandates'; import { useAdminMandates, useMandateFormAttributes, type Mandate } from '../../hooks/useMandates';
import { useApiRequest } from '../../hooks/useApi'; import { useApiRequest } from '../../hooks/useApi';
@ -16,8 +16,9 @@ import {
import { useToast } from '../../contexts/ToastContext'; import { useToast } from '../../contexts/ToastContext';
import { usePrompt } from '../../hooks/usePrompt'; import { usePrompt } from '../../hooks/usePrompt';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; 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 { FaPlus, FaSync, FaUsers, FaLock, FaSkullCrossbones } from 'react-icons/fa';
import { getUserDataCache } from '../../utils/userCache';
import styles from './Admin.module.css'; import styles from './Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext'; 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 [editingFormData, setEditingFormData] = useState<Record<string, unknown> | null>(null);
const [editingBillingWarning, setEditingBillingWarning] = useState<string | 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 // Check if user can create
const canCreate = permissions?.create !== 'n'; const canCreate = permissions?.create !== 'n';
const canUpdate = permissions?.update !== 'n'; const canUpdate = permissions?.update !== 'n';
@ -106,7 +118,10 @@ export const AdminMandatesPage: React.FC = () => {
const mandateId = String(editingFormData.id); const mandateId = String(editingFormData.id);
const { mandatePayload, billingUpdate } = splitMandateAndBillingFromForm(data); const { mandatePayload, billingUpdate } = splitMandateAndBillingFromForm(data);
const mandateOk = await handleUpdate(mandateId, mandatePayload as Partial<Mandate>); 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 { try {
await updateSettingsAdmin(request, mandateId, billingUpdate); await updateSettingsAdmin(request, mandateId, billingUpdate);
showSuccess(t('Gespeichert'), t('Mandant und Abrechnung aktualisiert.')); showSuccess(t('Gespeichert'), t('Mandant und Abrechnung aktualisiert.'));
@ -253,8 +268,8 @@ export const AdminMandatesPage: React.FC = () => {
{/* Create Modal */} {/* Create Modal */}
{showCreateModal && ( {showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}> <div className={styles.modalOverlay}>
<div className={styles.modal} onClick={e => e.stopPropagation()}> <div className={styles.modal}>
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neuer Mandant')}</h2> <h2 className={styles.modalTitle}>{t('Neuer Mandant')}</h2>
<button <button
@ -293,14 +308,8 @@ export const AdminMandatesPage: React.FC = () => {
{/* Edit Modal */} {/* Edit Modal */}
{editingFormData && ( {editingFormData && (
<div <div className={styles.modalOverlay}>
className={styles.modalOverlay} <div className={styles.modal}>
onClick={() => {
setEditingFormData(null);
setEditingBillingWarning(null);
}}
>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Mandant bearbeiten')}</h2> <h2 className={styles.modalTitle}>{t('Mandant bearbeiten')}</h2>
<button <button
@ -338,7 +347,7 @@ export const AdminMandatesPage: React.FC = () => {
</div> </div>
) : ( ) : (
<FormGeneratorForm <FormGeneratorForm
attributes={formAttributesWithBilling} attributes={editFormAttrs}
data={editingFormData} data={editingFormData}
mode="edit" mode="edit"
onSubmit={handleEditSubmit} onSubmit={handleEditSubmit}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,6 +55,10 @@ interface WorkspaceInputProps {
onProviderSelectionChange?: (selection: ProviderSelection) => void; onProviderSelectionChange?: (selection: ProviderSelection) => void;
isMobile?: boolean; isMobile?: boolean;
onTreeItemsDrop?: (items: TreeItemDrop[]) => void; 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; onPasteAsFile?: (file: File) => void;
draftAppend?: string; draftAppend?: string;
onDraftAppendConsumed?: () => void; onDraftAppendConsumed?: () => void;
@ -75,6 +79,10 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
onProviderSelectionChange, onProviderSelectionChange,
isMobile = false, isMobile = false,
onTreeItemsDrop, onTreeItemsDrop,
onFeatureSourceDrop,
onDataSourceDrop,
pendingAttachDsId,
onPendingAttachDsConsumed,
onPasteAsFile, onPasteAsFile,
draftAppend, draftAppend,
onDraftAppendConsumed, onDraftAppendConsumed,
@ -101,6 +109,15 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
} }
}, [draftAppend, onDraftAppendConsumed]); }, [draftAppend, onDraftAppendConsumed]);
useEffect(() => {
if (pendingAttachDsId) {
setAttachedDataSourceIds(prev =>
prev.includes(pendingAttachDsId) ? prev : [...prev, pendingAttachDsId],
);
onPendingAttachDsConsumed?.();
}
}, [pendingAttachDsId, onPendingAttachDsConsumed]);
const promptBeforeVoiceRef = useRef(''); const promptBeforeVoiceRef = useRef('');
const finalizedTextRef = useRef(''); const finalizedTextRef = useRef('');
const currentInterimRef = useRef(''); const currentInterimRef = useRef('');
@ -142,7 +159,6 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
onSend(trimmed, allFileIds, attachedDataSourceIds, attachedFeatureDataSourceIds, options); onSend(trimmed, allFileIds, attachedDataSourceIds, attachedFeatureDataSourceIds, options);
setPrompt(''); setPrompt('');
setShowAutocomplete(false); setShowAutocomplete(false);
setShowSourcePicker(false);
setAttachedFileIds([]); setAttachedFileIds([]);
}, [prompt, isProcessing, _extractFileRefs, attachedFileIds, attachedDataSourceIds, attachedFeatureDataSourceIds, neutralizeActive, onSend]); }, [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)); 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) => { const _toggleFeatureDataSource = useCallback((fdsId: string) => {
setAttachedFeatureDataSourceIds(prev => setAttachedFeatureDataSourceIds(prev =>
prev.includes(fdsId) ? prev.filter(id => id !== fdsId) : [...prev, fdsId], 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) => { const _handlePromptDragOver = useCallback((e: React.DragEvent) => {
if ( if (
e.dataTransfer.types.includes('application/tree-items') || 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.preventDefault();
e.dataTransfer.dropEffect = 'copy'; e.dataTransfer.dropEffect = 'copy';
@ -311,6 +321,24 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
return; 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'); const treeItemsJson = e.dataTransfer.getData('application/tree-items');
if (treeItemsJson && onTreeItemsDrop) { if (treeItemsJson && onTreeItemsDrop) {
e.preventDefault(); e.preventDefault();
@ -318,7 +346,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
const items: TreeItemDrop[] = JSON.parse(treeItemsJson); const items: TreeItemDrop[] = JSON.parse(treeItemsJson);
onTreeItemsDrop(items); onTreeItemsDrop(items);
} }
}, [onTreeItemsDrop]); }, [onTreeItemsDrop, onFeatureSourceDrop, onDataSourceDrop]);
return ( return (
<div <div

View file

@ -308,6 +308,30 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
} }
}, [instanceId, workspace]); }, [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 = ( const _leftPanelBody = (
<UnifiedDataBar <UnifiedDataBar
context={_udbContext} context={_udbContext}
@ -322,6 +346,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
onSourcesChanged={_handleSourcesChanged} onSourcesChanged={_handleSourcesChanged}
onSendToChat_Files={_handleSendToChat_Files} onSendToChat_Files={_handleSendToChat_Files}
onSendToChat_FeatureSource={_handleSendToChat_FeatureSource} onSendToChat_FeatureSource={_handleSendToChat_FeatureSource}
onAttachDataSource={_handleAttachDataSource}
/> />
); );
@ -492,6 +517,10 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
onProviderSelectionChange={setProviderSelection} onProviderSelectionChange={setProviderSelection}
isMobile={isMobile} isMobile={isMobile}
onTreeItemsDrop={_handleTreeItemsDrop} onTreeItemsDrop={_handleTreeItemsDrop}
onFeatureSourceDrop={_handleSendToChat_FeatureSource}
onDataSourceDrop={_handleDataSourceDrop}
pendingAttachDsId={pendingAttachDsId}
onPendingAttachDsConsumed={() => setPendingAttachDsId('')}
onPasteAsFile={_uploadAndAttach} onPasteAsFile={_uploadAndAttach}
draftAppend={draftAppend} draftAppend={draftAppend}
onDraftAppendConsumed={() => setDraftAppend('')} onDraftAppendConsumed={() => setDraftAppend('')}