fixed ai call end to end with saas multimandate

This commit is contained in:
ValueOn AG 2026-01-26 23:26:34 +01:00
parent 28af4cb068
commit f41e6d0088
30 changed files with 371 additions and 233 deletions

View file

@ -24,6 +24,7 @@ import {
FaThList,
FaTh,
} from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import {
useAccessRules,
type AccessRule,
@ -529,6 +530,7 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
mandateId,
featureCode,
}) => {
const { showError } = useToast();
const {
rules,
loading,
@ -580,9 +582,8 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
}, [updateRuleLocally]);
const handleDelete = useCallback((ruleId: string) => {
if (window.confirm('Möchten Sie diese Regel wirklich löschen?')) {
removeRuleLocally(ruleId);
}
// Direct delete - rules are local until saved
removeRuleLocally(ruleId);
}, [removeRuleLocally]);
const handleAdd = useCallback((ruleData: AccessRuleCreate) => {
@ -607,16 +608,15 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
setHasChanges(false);
onSave?.();
} else {
alert(result.error || 'Fehler beim Speichern');
showError('Fehler', result.error || 'Fehler beim Speichern');
}
};
const handleReset = () => {
if (window.confirm('Alle Änderungen verwerfen?')) {
fetchRules().then(fetchedRules => {
setOriginalRules(fetchedRules);
});
}
// Direct reset - user clicked the reset button intentionally
fetchRules().then(fetchedRules => {
setOriginalRules(fetchedRules);
});
};
const handleJsonApply = (newRules: AccessRule[]) => {

View file

@ -0,0 +1,72 @@
import React, { useState } from 'react';
import { IoIosDownload } from 'react-icons/io';
import { useLanguage } from '../../../../providers/language/LanguageContext';
import styles from '../ActionButton.module.css';
export interface DownloadActionButtonProps<T = any> {
row: T;
onDownload: (row: T) => Promise<void> | void;
disabled?: boolean | { disabled: boolean; message?: string };
loading?: boolean;
className?: string;
title?: string;
hookData?: any;
idField?: string;
loadingStateName?: string;
}
export function DownloadActionButton<T = any>({
row,
onDownload,
disabled = false,
loading = false,
className = '',
title,
hookData,
idField = 'id',
loadingStateName = 'downloadingFiles'
}: DownloadActionButtonProps<T>) {
const { t } = useLanguage();
const [internalLoading, setInternalLoading] = useState(false);
// Extract disabled state and tooltip message
const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false;
const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined;
const handleClick = async (e: React.MouseEvent) => {
e.stopPropagation();
if (!isDisabled && !loading && !internalLoading) {
setInternalLoading(true);
try {
if (onDownload) {
await onDownload(row);
}
} finally {
setInternalLoading(false);
}
}
};
const buttonTitle = title || t('files.action.download', 'Download');
// Use hookData loading state if available
const loadingState = hookData?.[loadingStateName];
const actualIsLoading = loadingState?.has((row as any)[idField]) || loading || internalLoading;
// Determine the final button title (tooltip)
const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle;
return (
<button
onClick={handleClick}
className={`${styles.actionButton} ${styles.download} ${actualIsLoading ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`}
title={finalTitle}
disabled={isDisabled || actualIsLoading}
>
<span className={styles.actionIcon}>
{actualIsLoading ? '⏳' : <IoIosDownload />}
</span>
</button>
);
}
export default DownloadActionButton;

View file

@ -0,0 +1,2 @@
export { DownloadActionButton, type DownloadActionButtonProps } from './DownloadActionButton';
export { DownloadActionButton as default } from './DownloadActionButton';

View file

@ -4,6 +4,7 @@ export { DeleteActionButton } from './DeleteActionButton';
export { ViewActionButton } from './ViewActionButton';
export { CopyActionButton } from './CopyActionButton';
export { RemoveActionButton } from './RemoveActionButton';
export { DownloadActionButton } from './DownloadActionButton';
// Generic Custom Action Button (for entity-specific actions)
export { CustomActionButton } from './CustomActionButton';
@ -14,4 +15,5 @@ export type { DeleteActionButtonProps } from './DeleteActionButton';
export type { ViewActionButtonProps } from './ViewActionButton';
export type { CopyActionButtonProps } from './CopyActionButton';
export type { RemoveActionButtonProps } from './RemoveActionButton';
export type { DownloadActionButtonProps } from './DownloadActionButton';
export type { CustomActionButtonProps } from './CustomActionButton';

View file

@ -853,7 +853,23 @@ export function FormGeneratorForm<T extends Record<string, any>>({
const minHeight = minRows * 1.5 * 16;
const maxHeight = maxRows * 1.5 * 16;
const currentValue = value || '';
// Handle object/array values by converting to JSON string for display
const isObjectValue = typeof value === 'object' && value !== null;
// Check if string value looks like JSON (for fields that were originally objects but temporarily invalid)
const looksLikeJson = typeof value === 'string' && value.trim().match(/^[\[{]/);
const isJsonField = isObjectValue || looksLikeJson;
let currentValue = '';
if (isObjectValue) {
try {
currentValue = JSON.stringify(value, null, 2);
} catch {
currentValue = String(value);
}
} else {
currentValue = value || '';
}
const isContentField = attr.name === 'content' || attr.name.toLowerCase().includes('content');
const textareaClassName = isContentField
? `${styles.fieldTextarea} ${styles.contentTextarea} ${hasError ? styles.fieldError : ''}`
@ -865,7 +881,20 @@ export function FormGeneratorForm<T extends Record<string, any>>({
name={attr.name}
value={currentValue}
onChange={(e) => {
handleFieldChange(attr.name, e.target.value);
const newTextValue = e.target.value;
const trimmed = newTextValue.trim();
// Try to parse as JSON if it looks like JSON (starts with { or [)
if (trimmed.match(/^[\[{]/)) {
try {
const parsed = JSON.parse(newTextValue);
handleFieldChange(attr.name, parsed);
} catch {
// If parsing fails, store as string (user is still typing)
handleFieldChange(attr.name, newTextValue);
}
} else {
handleFieldChange(attr.name, newTextValue);
}
const textarea = e.target;
textarea.style.height = 'auto';
const scrollHeight = textarea.scrollHeight;
@ -879,6 +908,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
onBlur={() => handleFieldFocus(attr.name, false)}
className={textareaClassName}
rows={minRows}
style={isJsonField ? { fontFamily: 'monospace', fontSize: '0.85em' } : undefined}
ref={(textarea) => {
if (textarea) {
textarea.style.setProperty('min-height', `${minHeight}px`, 'important');
@ -888,7 +918,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
}
}}
/>
<label className={getLabelClass(attr.name, value)}>
<label className={getLabelClass(attr.name, currentValue)}>
{attr.label}
{attr.required && <span className={styles.required}>*</span>}
</label>

View file

@ -369,7 +369,6 @@ export function FormGeneratorList<T extends Record<string, any>>({
}
} else {
console.warn('No delete handler found in hookData or props');
alert('No delete handler configured');
return;
}
} else if (onDeleteMultiple) {
@ -388,7 +387,6 @@ export function FormGeneratorList<T extends Record<string, any>>({
}
} else {
console.warn('No delete handler provided');
alert('No delete handler configured');
return;
}
@ -398,7 +396,6 @@ export function FormGeneratorList<T extends Record<string, any>>({
console.log('Delete completed, selection cleared');
} catch (error) {
console.error('Delete failed:', error);
alert(`Delete failed: ${error}`);
} finally {
setIsDeleting(false);
}

View file

@ -135,14 +135,13 @@ function SpeechSettings({ onDataUpdate }: SpeechSettingsProps) {
};
const handleReset = () => {
if (window.confirm(t('speech.settings.reset_confirm'))) {
localStorage.removeItem('speechSignUpData');
localStorage.removeItem('speechSignUpTimestamp');
window.dispatchEvent(new CustomEvent('speechSignUpChanged'));
setFormData(null);
setSaveMessage({ type: 'success', text: t('speech.settings.reset_success') });
setTimeout(() => setSaveMessage(null), 3000);
}
// Direct reset - user clicked the reset button intentionally
localStorage.removeItem('speechSignUpData');
localStorage.removeItem('speechSignUpTimestamp');
window.dispatchEvent(new CustomEvent('speechSignUpChanged'));
setFormData(null);
setSaveMessage({ type: 'success', text: t('speech.settings.reset_success') });
setTimeout(() => setSaveMessage(null), 3000);
};
if (isLoading) {

View file

@ -14,9 +14,11 @@ export interface ChatMessageProps {
onFileDelete?: (file: WorkflowFile) => Promise<void>;
onFileRemove?: (file: WorkflowFile) => Promise<void>;
onFileView?: (file: WorkflowFile) => Promise<void>;
onFileDownload?: (file: WorkflowFile) => Promise<void>;
deletingFiles?: Set<string>;
previewingFiles?: Set<string>;
removingFiles?: Set<string>;
downloadingFiles?: Set<string>;
workflowId?: string;
}
@ -30,9 +32,11 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
onFileDelete,
onFileRemove,
onFileView,
onFileDownload,
deletingFiles,
previewingFiles,
removingFiles,
downloadingFiles,
workflowId
}) => {
const isUser = message.role?.toLowerCase() === 'user';
@ -131,9 +135,11 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
onFileDelete={onFileDelete}
onFileRemove={onFileRemove}
onFileView={onFileView}
onFileDownload={onFileDownload}
deletingFiles={deletingFiles}
previewingFiles={previewingFiles}
removingFiles={removingFiles}
downloadingFiles={downloadingFiles}
workflowId={workflowId}
/>
)}

View file

@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { MessageDocument, Message } from '../MessagesTypes';
import { formatFileSize } from '../MessageUtils';
import { ViewActionButton, DeleteActionButton, RemoveActionButton } from '../../../FormGenerator/ActionButtons';
import { ViewActionButton, DeleteActionButton, RemoveActionButton, DownloadActionButton } from '../../../FormGenerator/ActionButtons';
import { WorkflowFile } from '../../../../hooks/usePlayground';
import styles from '../Messages.module.css';
@ -12,9 +12,11 @@ export interface DocumentItemProps {
onFileDelete?: (file: WorkflowFile) => Promise<void>;
onFileRemove?: (file: WorkflowFile) => Promise<void>;
onFileView?: (file: WorkflowFile) => Promise<void>;
onFileDownload?: (file: WorkflowFile) => Promise<void>;
deletingFiles?: Set<string>;
previewingFiles?: Set<string>;
removingFiles?: Set<string>;
downloadingFiles?: Set<string>;
workflowId?: string;
}
@ -28,9 +30,11 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
onFileDelete,
onFileRemove,
onFileView,
onFileDownload,
deletingFiles = new Set(),
previewingFiles = new Set(),
removingFiles = new Set(),
downloadingFiles = new Set(),
workflowId: _workflowId
}) => {
// Convert MessageDocument to WorkflowFile format for compatibility with action buttons
@ -47,6 +51,7 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
const isDeleting = deletingFiles.has(document.fileId);
const isPreviewing = previewingFiles.has(document.fileId);
const isRemoving = removingFiles.has(document.fileId);
const isDownloading = downloadingFiles.has(document.fileId);
// Create hookData object for action buttons
const hookData = useMemo(() => ({
@ -65,8 +70,9 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
},
deletingItems: deletingFiles,
previewingFiles: previewingFiles,
removingItems: removingFiles
}), [onFileDelete, workflowFile, deletingFiles, previewingFiles, removingFiles]);
removingItems: removingFiles,
downloadingFiles: downloadingFiles
}), [onFileDelete, workflowFile, deletingFiles, previewingFiles, removingFiles, downloadingFiles]);
const handleView = async () => {
if (onFileView) {
@ -74,6 +80,12 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
}
};
const handleDownload = async () => {
if (onFileDownload) {
await onFileDownload(workflowFile);
}
};
const handleRemove = async () => {
if (onFileRemove) {
await onFileRemove(workflowFile);
@ -89,7 +101,7 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
{formatFileSize(document.fileSize)} {document.mimeType}
</div>
</div>
{(onFileView || onFileDelete || onFileRemove) && (
{(onFileView || onFileDownload || onFileDelete || onFileRemove) && (
<div style={{ display: 'flex', gap: '4px', flexShrink: 0 }}>
{onFileView && (
<ViewActionButton
@ -103,6 +115,17 @@ export const DocumentItem: React.FC<DocumentItemProps> = ({
typeField="mimeType"
/>
)}
{onFileDownload && (
<DownloadActionButton
row={workflowFile}
onDownload={handleDownload}
disabled={isDeleting || isRemoving}
loading={isDownloading}
hookData={hookData}
idField="fileId"
loadingStateName="downloadingFiles"
/>
)}
{onFileRemove && (
<RemoveActionButton
row={workflowFile}

View file

@ -19,9 +19,11 @@ const Messages: React.FC<MessagesProps> = ({
onFileDelete,
onFileRemove,
onFileView,
onFileDownload,
deletingFiles,
previewingFiles,
removingFiles,
downloadingFiles,
workflowId
}) => {
if (!messages || messages.length === 0) {
@ -61,9 +63,11 @@ const Messages: React.FC<MessagesProps> = ({
onFileDelete={onFileDelete}
onFileRemove={onFileRemove}
onFileView={onFileView}
onFileDownload={onFileDownload}
deletingFiles={deletingFiles}
previewingFiles={previewingFiles}
removingFiles={removingFiles}
downloadingFiles={downloadingFiles}
workflowId={workflowId}
/>
);
@ -79,9 +83,11 @@ const Messages: React.FC<MessagesProps> = ({
onFileDelete={onFileDelete}
onFileRemove={onFileRemove}
onFileView={onFileView}
onFileDownload={onFileDownload}
deletingFiles={deletingFiles}
previewingFiles={previewingFiles}
removingFiles={removingFiles}
downloadingFiles={downloadingFiles}
workflowId={workflowId}
/>
);

View file

@ -110,9 +110,11 @@ export interface MessagesProps {
onFileDelete?: (file: WorkflowFile) => Promise<void>;
onFileRemove?: (file: WorkflowFile) => Promise<void>;
onFileView?: (file: WorkflowFile) => Promise<void>;
onFileDownload?: (file: WorkflowFile) => Promise<void>;
deletingFiles?: Set<string>;
previewingFiles?: Set<string>;
removingFiles?: Set<string>;
downloadingFiles?: Set<string>;
workflowId?: string;
}

View file

@ -8,9 +8,12 @@ interface FileContextType {
refetch: () => Promise<void>;
handleFileUpload: (file: File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>;
handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>;
handleFilePreview: (fileId: string, fileName: string, mimeType?: string) => Promise<{ success: boolean; previewUrl?: string | null; blob?: Blob; isJsonContent?: boolean; decodedContent?: string; error?: string }>;
handleFileDownload: (fileId: string, fileName: string) => Promise<void>;
uploadingFile: boolean;
deletingFiles: Set<string>;
previewingFiles: Set<string>;
downloadingFiles: Set<string>;
}
const FileContext = createContext<FileContextType | undefined>(undefined);
@ -20,9 +23,12 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
const {
handleFileUpload: hookHandleFileUpload,
handleFileDelete: hookHandleFileDelete,
handleFilePreview,
handleFileDownload,
uploadingFile,
deletingFiles,
previewingFiles
previewingFiles,
downloadingFiles
} = useFileOperations();
// Centralized file upload that updates the shared state
@ -77,9 +83,12 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
refetch,
handleFileUpload,
handleFileDelete,
handleFilePreview,
handleFileDownload,
uploadingFile,
deletingFiles,
previewingFiles
previewingFiles,
downloadingFiles
}}
>
{children}

View file

@ -83,7 +83,7 @@ export function useDashboardInputForm() {
useEffect(() => {
const checkPermissions = async () => {
try {
const uiPerm = await canView('UI', 'playground');
const uiPerm = await canView('UI', 'ui.system.playground');
setPlaygroundUIPermission(uiPerm);
if (uiPerm) {
@ -476,6 +476,17 @@ export function useDashboardInputForm() {
}
}
}, [workflowId, messages, fileContext, request]);
// handleFileView is a no-op because ViewActionButton's ContentPreview handles the preview internally
const handleFileView = useCallback(async (_file: WorkflowFile) => {
// The ViewActionButton component handles the preview via ContentPreview
// No additional action needed here
}, []);
const handleFileDownload = useCallback(async (file: WorkflowFile) => {
if (!file.fileId) return;
await fileContext.handleFileDownload(file.fileId, file.fileName);
}, [fileContext]);
const onInputChange = useCallback((value: string) => {
setInputValue(value);
@ -798,9 +809,12 @@ export function useDashboardInputForm() {
handleFileUpload,
handleFileDelete,
handleFileRemove,
handleFileView,
uploadingFile: fileContext.uploadingFile,
deletingFiles: fileContext.deletingFiles,
previewingFiles: fileContext.previewingFiles,
downloadingFiles: fileContext.downloadingFiles,
handleFileDownload,
isFileAttachmentPopupOpen,
setIsFileAttachmentPopupOpen,
allUserFiles: fileContext.files || [],

View file

@ -515,13 +515,22 @@
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 6px;
padding: 1rem;
width: 100%;
box-sizing: border-box;
overflow-x: auto;
}
.logEntry {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
gap: 0.5rem;
padding: 0.25rem 0;
border-bottom: 1px solid var(--border-color, #e0e0e0);
width: 100%;
min-width: 100%;
box-sizing: border-box;
}
.logEntry:last-child {
@ -530,7 +539,8 @@
.logTime {
color: var(--text-secondary);
flex-shrink: 0;
flex: 0 0 auto;
white-space: nowrap;
}
.logStatus {
@ -538,13 +548,17 @@
}
.logMessage {
flex: 1;
word-break: break-word;
flex: 1 1 auto;
min-width: 0;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: pre-wrap;
}
.logProgress {
color: var(--text-secondary);
flex-shrink: 0;
flex: 0 0 auto;
white-space: nowrap;
}
/* Logs history */

View file

@ -327,17 +327,15 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
}
};
// Handle remove user
// Handle remove user (confirmation handled by DeleteActionButton)
const handleRemoveUser = async (user: FeatureAccessUser) => {
if (!selectedMandateId || !selectedInstanceId) return;
if (window.confirm(`Möchten Sie den Benutzer "${user.username}" wirklich aus dieser Feature-Instanz entfernen?`)) {
const result = await removeUserFromInstance(selectedMandateId, selectedInstanceId, user.userId);
if (result.success) {
refreshUsers();
showSuccess('Benutzer entfernt', `"${user.username}" wurde aus der Feature-Instanz entfernt.`);
} else {
showError('Fehler', result.error || 'Fehler beim Entfernen des Benutzers');
}
const result = await removeUserFromInstance(selectedMandateId, selectedInstanceId, user.userId);
if (result.success) {
refreshUsers();
showSuccess('Benutzer entfernt', `"${user.username}" wurde aus der Feature-Instanz entfernt.`);
} else {
showError('Fehler', result.error || 'Fehler beim Entfernen des Benutzers');
}
};

View file

@ -15,6 +15,7 @@ import { FormGeneratorTable } from '../../components/FormGenerator/FormGenerator
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { AccessRulesEditor } from '../../components/AccessRules';
import { FaPlus, FaSync, FaUserShield, FaCube, FaShieldAlt } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import api from '../../api';
import styles from './Admin.module.css';
@ -39,6 +40,7 @@ interface FeatureRole {
}
export const AdminFeatureRolesPage: React.FC = () => {
const { showError } = useToast();
// State
const [features, setFeatures] = useState<Feature[]>([]);
const [selectedFeatureCode, setSelectedFeatureCode] = useState<string>('');
@ -197,7 +199,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
await fetchRoles();
} catch (err: any) {
console.error('Error creating role:', err);
alert(err.response?.data?.detail || 'Fehler beim Erstellen der Rolle');
showError('Fehler', err.response?.data?.detail || 'Fehler beim Erstellen der Rolle');
} finally {
setIsSubmitting(false);
}
@ -216,22 +218,20 @@ export const AdminFeatureRolesPage: React.FC = () => {
await fetchRoles();
} catch (err: any) {
console.error('Error updating role:', err);
alert(err.response?.data?.detail || 'Fehler beim Aktualisieren der Rolle');
showError('Fehler', err.response?.data?.detail || 'Fehler beim Aktualisieren der Rolle');
} finally {
setIsSubmitting(false);
}
};
// Handle delete role
// Handle delete role (confirmation handled by DeleteActionButton)
const handleDeleteRole = async (role: FeatureRole) => {
if (window.confirm(`Möchten Sie die Rolle "${role.roleLabel}" wirklich löschen?`)) {
try {
await api.delete(`/api/rbac/roles/${role.id}`);
await fetchRoles();
} catch (err: any) {
console.error('Error deleting role:', err);
alert(err.response?.data?.detail || 'Fehler beim Löschen der Rolle');
}
try {
await api.delete(`/api/rbac/roles/${role.id}`);
await fetchRoles();
} catch (err: any) {
console.error('Error deleting role:', err);
showError('Fehler', err.response?.data?.detail || 'Fehler beim Löschen der Rolle');
}
};

View file

@ -11,10 +11,12 @@ import { useUserMandates, type Mandate, type Role } from '../../hooks/useUserMan
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaEnvelopeOpenText, FaBuilding, FaCopy, FaLink } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import api from '../../api';
import styles from './Admin.module.css';
export const AdminInvitationsPage: React.FC = () => {
const { showError } = useToast();
const {
invitations,
loading,
@ -188,7 +190,7 @@ export const AdminInvitationsPage: React.FC = () => {
setShowUrlModal(result.data);
fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed });
} else {
alert(result.error || 'Fehler beim Erstellen der Einladung');
showError('Fehler', result.error || 'Fehler beim Erstellen der Einladung');
}
} finally {
setIsSubmitting(false);
@ -201,7 +203,7 @@ export const AdminInvitationsPage: React.FC = () => {
if (!selectedMandateId) return false;
const result = await revokeInvitation(selectedMandateId, invitationId);
if (!result.success) {
alert(result.error || 'Fehler beim Widerrufen der Einladung');
showError('Fehler', result.error || 'Fehler beim Widerrufen der Einladung');
}
return result.success;
};

View file

@ -19,10 +19,12 @@ import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaUserShield, FaBuilding, FaGlobe } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import api from '../../api';
import styles from './Admin.module.css';
export const AdminMandateRolesPage: React.FC = () => {
const { showError, showWarning } = useToast();
const {
roles,
loading,
@ -209,11 +211,11 @@ export const AdminMandateRolesPage: React.FC = () => {
setShowCreateModal(false);
await fetchRoles(selectedMandateId, { scopeFilter });
} else {
alert(result.error || 'Fehler beim Erstellen der Rolle');
showError('Fehler', result.error || 'Fehler beim Erstellen der Rolle');
}
} catch (err: any) {
console.error('Create role error:', err);
alert(err.message || 'Fehler beim Erstellen der Rolle');
showError('Fehler', err.message || 'Fehler beim Erstellen der Rolle');
} finally {
setIsSubmitting(false);
}
@ -248,31 +250,29 @@ export const AdminMandateRolesPage: React.FC = () => {
await fetchRoles(selectedMandateId, { scopeFilter });
}
} else {
alert(result.error || 'Fehler beim Aktualisieren der Rolle');
showError('Fehler', result.error || 'Fehler beim Aktualisieren der Rolle');
}
} catch (err: any) {
console.error('Update role error:', err);
alert(err.message || 'Fehler beim Aktualisieren der Rolle');
showError('Fehler', err.message || 'Fehler beim Aktualisieren der Rolle');
} finally {
setIsSubmitting(false);
}
};
// Handle delete role
// Handle delete role (confirmation handled by DeleteActionButton)
const handleDeleteRole = async (role: Role) => {
if (role.isSystemRole) {
alert('System-Rollen können nicht gelöscht werden.');
showWarning('Nicht erlaubt', 'System-Rollen können nicht gelöscht werden.');
return;
}
if (window.confirm(`Möchten Sie die Rolle "${role.roleLabel}" wirklich löschen?`)) {
const result = await deleteRole(role.id);
if (result.success) {
// Refetch to update the list
await fetchRoles(selectedMandateId, { scopeFilter });
} else {
alert(result.error || 'Fehler beim Löschen der Rolle');
}
const result = await deleteRole(role.id);
if (result.success) {
// Refetch to update the list
await fetchRoles(selectedMandateId, { scopeFilter });
} else {
showError('Fehler', result.error || 'Fehler beim Löschen der Rolle');
}
};

View file

@ -73,11 +73,9 @@ export const AdminMandatesPage: React.FC = () => {
}
};
// Handle delete
// Handle delete (confirmation handled by DeleteActionButton)
const handleDeleteMandate = async (mandate: Mandate) => {
if (window.confirm(`Möchten Sie den Mandanten "${mandate.name || mandate.id}" wirklich löschen?`)) {
await handleDelete(mandate.id);
}
await handleDelete(mandate.id);
};
if (error) {

View file

@ -10,10 +10,12 @@ import { useUserMandates, type MandateUser, type Mandate, type Role, type Pagina
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaUsers, FaBuilding } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import api from '../../api';
import styles from './Admin.module.css';
export const AdminUserMandatesPage: React.FC = () => {
const { showError } = useToast();
const {
users,
loading,
@ -206,7 +208,7 @@ export const AdminUserMandatesPage: React.FC = () => {
setShowAddModal(false);
fetchMandateUsers(selectedMandateId);
} else {
alert(result.error || 'Fehler beim Hinzufügen des Benutzers');
showError('Fehler', result.error || 'Fehler beim Hinzufügen des Benutzers');
}
} finally {
setIsSubmitting(false);
@ -223,21 +225,19 @@ export const AdminUserMandatesPage: React.FC = () => {
setEditingUser(null);
fetchMandateUsers(selectedMandateId);
} else {
alert(result.error || 'Fehler beim Aktualisieren der Rollen');
showError('Fehler', result.error || 'Fehler beim Aktualisieren der Rollen');
}
} finally {
setIsSubmitting(false);
}
};
// Handle remove user
// Handle remove user (confirmation handled by DeleteActionButton)
const handleRemoveUser = async (user: MandateUser) => {
if (!selectedMandateId) return;
if (window.confirm(`Möchten Sie den Benutzer "${user.username}" wirklich aus diesem Mandanten entfernen?`)) {
const result = await removeUserFromMandate(selectedMandateId, user.userId);
if (!result.success) {
alert(result.error || 'Fehler beim Entfernen des Benutzers');
}
const result = await removeUserFromMandate(selectedMandateId, user.userId);
if (!result.success) {
showError('Fehler', result.error || 'Fehler beim Entfernen des Benutzers');
}
};

View file

@ -96,13 +96,11 @@ export const AdminUsersPage: React.FC = () => {
}
};
// Handle delete
// Handle delete (confirmation handled by DeleteActionButton)
const handleDeleteUser = async (user: User) => {
if (window.confirm(`Möchten Sie den Benutzer "${user.username}" wirklich löschen?`)) {
const success = await deleteUser(user.id);
if (success) {
refetch(); // Refresh the list
}
const success = await deleteUser(user.id);
if (success) {
refetch(); // Refresh the list
}
};

View file

@ -93,22 +93,20 @@ export const ConnectionsPage: React.FC = () => {
}
};
// Handle delete
// Handle delete (confirmation handled by DeleteActionButton)
const handleDelete = async (connection: Connection) => {
if (window.confirm(`Möchten Sie die Verbindung "${connection.name || connection.email || connection.id}" wirklich löschen?`)) {
setDeletingConnections(prev => new Set(prev).add(connection.id));
try {
await deleteConnection(connection.id);
refetch();
} catch (error) {
console.error('Error deleting connection:', error);
} finally {
setDeletingConnections(prev => {
const newSet = new Set(prev);
newSet.delete(connection.id);
return newSet;
});
}
setDeletingConnections(prev => new Set(prev).add(connection.id));
try {
await deleteConnection(connection.id);
refetch();
} catch (error) {
console.error('Error deleting connection:', error);
} finally {
setDeletingConnections(prev => {
const newSet = new Set(prev);
newSet.delete(connection.id);
return newSet;
});
}
};

View file

@ -100,25 +100,20 @@ export const FilesPage: React.FC = () => {
}
};
// Handle delete single file
// Handle delete single file (confirmation handled by DeleteActionButton)
const handleDelete = async (file: UserFile) => {
if (window.confirm(`Möchten Sie die Datei "${file.fileName}" wirklich löschen?`)) {
const success = await handleFileDelete(file.id);
if (success) {
refetch();
}
const success = await handleFileDelete(file.id);
if (success) {
refetch();
}
};
// Handle delete multiple files
// Handle delete multiple files (confirmation handled by FormGenerator)
const handleDeleteMultiple = async (filesToDelete: UserFile[]) => {
const count = filesToDelete.length;
if (window.confirm(`Möchten Sie ${count} Datei(en) wirklich löschen?`)) {
const ids = filesToDelete.map(f => f.id);
const success = await handleFileDeleteMultiple(ids);
if (success) {
refetch();
}
const ids = filesToDelete.map(f => f.id);
const success = await handleFileDeleteMultiple(ids);
if (success) {
refetch();
}
};

View file

@ -109,13 +109,11 @@ export const PromptsPage: React.FC = () => {
}
};
// Handle delete single prompt
// Handle delete single prompt (confirmation handled by DeleteActionButton)
const handleDelete = async (prompt: Prompt) => {
if (window.confirm(`Möchten Sie den Prompt "${prompt.name}" wirklich löschen?`)) {
const success = await handlePromptDelete(prompt.id);
if (success) {
refetch();
}
const success = await handlePromptDelete(prompt.id);
if (success) {
refetch();
}
};

View file

@ -11,10 +11,12 @@ import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaFileAlt, FaDownload } from 'react-icons/fa';
import { useToast } from '../../../contexts/ToastContext';
import api from '../../../api';
import styles from '../../admin/Admin.module.css';
export const TrusteeDocumentsView: React.FC = () => {
const { showError } = useToast();
const instanceId = useInstanceId();
// Entity hook
@ -103,14 +105,12 @@ export const TrusteeDocumentsView: React.FC = () => {
}
};
// Handle delete
// Handle delete (confirmation handled by DeleteActionButton)
const handleDeleteDoc = async (doc: TrusteeDocument) => {
if (window.confirm(`Dokument "${doc.documentName}" wirklich löschen?`)) {
removeOptimistically(doc.id);
const success = await handleDelete(doc.id);
if (!success) {
refetch(); // Revert on error
}
removeOptimistically(doc.id);
const success = await handleDelete(doc.id);
if (!success) {
refetch(); // Revert on error
}
};
@ -136,7 +136,7 @@ export const TrusteeDocumentsView: React.FC = () => {
window.URL.revokeObjectURL(url);
} catch (err) {
console.error('Download error:', err);
alert('Fehler beim Herunterladen des Dokuments.');
showError('Fehler', 'Fehler beim Herunterladen des Dokuments.');
} finally {
setDownloadingId(null);
}

View file

@ -117,14 +117,12 @@ export const TrusteePositionDocumentsView: React.FC = () => {
}
};
// Handle delete
// Handle delete (confirmation handled by DeleteActionButton)
const handleDeleteLink = async (link: TrusteePositionDocument) => {
if (window.confirm('Verknüpfung wirklich entfernen?')) {
removeOptimistically(link.id);
const success = await handleDelete(link.id);
if (!success) {
refetch(); // Revert on error
}
removeOptimistically(link.id);
const success = await handleDelete(link.id);
if (!success) {
refetch(); // Revert on error
}
};

View file

@ -50,19 +50,24 @@ export const TrusteePositionsView: React.FC = () => {
}
}, [instanceId]);
// Hidden columns (not shown in table view, but available in form)
const hiddenColumns = ['desc'];
// Generate columns from attributes
const columns = useMemo(() => {
return (attributes || []).map(attr => ({
key: attr.name,
label: attr.label || attr.name,
type: attr.type as any,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
}));
return (attributes || [])
.filter(attr => !hiddenColumns.includes(attr.name))
.map(attr => ({
key: attr.name,
label: attr.label || attr.name,
type: attr.type as any,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
}));
}, [attributes]);
// Check permissions
@ -108,14 +113,12 @@ export const TrusteePositionsView: React.FC = () => {
}
};
// Handle delete
// Handle delete (confirmation handled by DeleteActionButton)
const handleDeletePos = async (pos: TrusteePosition) => {
if (window.confirm(`Position "${pos.desc || pos.id}" wirklich löschen?`)) {
removeOptimistically(pos.id);
const success = await handleDelete(pos.id);
if (!success) {
refetch(); // Revert on error
}
removeOptimistically(pos.id);
const success = await handleDelete(pos.id);
if (!success) {
refetch(); // Revert on error
}
};

View file

@ -124,7 +124,7 @@ export const AutomationsPage: React.FC = () => {
// Generate columns from attributes - exclude ID fields from display
const columns = useMemo(() => {
const hiddenColumns = ['id', 'mandateId', '_createdBy', '_createdAt', '_modifiedAt', 'template', 'executionLogs'];
const hiddenColumns = ['id', 'mandateId', '_createdBy', '_createdAt', '_modifiedAt', 'template', 'executionLogs', 'placeholders'];
return (attributes || [])
.filter(attr => !hiddenColumns.includes(attr.name))
@ -188,14 +188,12 @@ export const AutomationsPage: React.FC = () => {
}
};
// Handle delete single automation
// Handle delete single automation (confirmation handled by DeleteActionButton)
const handleDelete = async (automation: Automation) => {
if (window.confirm(`Möchten Sie die Automatisierung "${automation.label}" wirklich löschen?`)) {
const success = await handleAutomationDelete(automation.id);
if (success) {
showSuccess('Automatisierung gelöscht');
await refetch();
}
const success = await handleAutomationDelete(automation.id);
if (success) {
showSuccess('Automatisierung gelöscht');
await refetch();
}
};
@ -274,10 +272,16 @@ export const AutomationsPage: React.FC = () => {
const logs: WorkflowLog[] = response?.items || response || [];
if (logs.length > 0) {
setExecutionModal(prev => ({
...prev,
logs: [...prev.logs, ...logs],
}));
setExecutionModal(prev => {
// Deduplicate logs by ID
const existingIds = new Set(prev.logs.map(l => l.id));
const newLogs = logs.filter(l => !existingIds.has(l.id));
return {
...prev,
logs: [...prev.logs, ...newLogs],
};
});
lastLogIdRef.current = logs[logs.length - 1].id;
}
@ -377,7 +381,7 @@ export const AutomationsPage: React.FC = () => {
try {
await request({
url: `/api/workflows/${executionModal.workflowId}/stop`,
url: `/api/chat/playground/${executionModal.workflowId}/stop`,
method: 'post',
});

View file

@ -9,12 +9,11 @@
import React, { useRef, useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useDashboardInputForm } from '../../hooks/usePlayground';
import { useUserWorkflows } from '../../hooks/useWorkflows';
import { useResizablePanels } from '../../hooks/useResizablePanels';
import { usePrompts } from '../../hooks/usePrompts';
import { FaComment, FaTasks, FaPaperPlane, FaStop, FaFile, FaPlus, FaMicrophone, FaSquare, FaFileAlt } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import { useVoiceLanguage, VoiceLanguageSelect } from '../../components/UiComponents';
import { useVoiceLanguage, VoiceLanguageSelect, Messages } from '../../components/UiComponents';
import api from '../../api';
import styles from './PlaygroundPage.module.css';
@ -44,11 +43,16 @@ export const PlaygroundPage: React.FC = () => {
workflowItems,
pendingFiles,
handleFileRemove,
handleFileDelete,
handleFileView,
handleFileDownload,
latestStats,
playgroundUIPermission,
deletingFiles,
previewingFiles,
downloadingFiles,
} = hookData;
const { data: workflows } = useUserWorkflows();
const { prompts, refetch: refetchPrompts } = usePrompts();
const { showError, showSuccess } = useToast();
@ -310,14 +314,7 @@ export const PlaygroundPage: React.FC = () => {
}
};
// Format timestamp for messages
const formatTime = (timestamp: number | undefined) => {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
};
// Render messages
// Render messages using the Messages component with document support
const renderMessages = () => {
if (!messages || messages.length === 0) {
return (
@ -332,43 +329,21 @@ export const PlaygroundPage: React.FC = () => {
}
return (
<div className={styles.messagesContainer}>
{messages.map((msg: any, index: number) => (
<div
key={msg.id || index}
style={{
padding: '0.75rem 1rem',
borderRadius: '8px',
background: msg.role === 'user'
? 'var(--bg-secondary)'
: 'var(--surface-color)',
border: '1px solid var(--border-color)',
marginLeft: msg.role === 'user' ? '2rem' : '0',
marginRight: msg.role === 'assistant' ? '2rem' : '0',
}}
>
<div style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: '0.5rem',
fontSize: '0.75rem',
color: 'var(--text-secondary)',
}}>
<span style={{ fontWeight: 500 }}>
{msg.role === 'user' ? 'Sie' : 'Assistent'}
</span>
<span>{formatTime(msg.publishedAt)}</span>
</div>
<div style={{
color: 'var(--text-primary)',
fontSize: '0.875rem',
whiteSpace: 'pre-wrap',
}}>
{msg.message || msg.content}
</div>
</div>
))}
</div>
<Messages
messages={messages}
variant="chat"
showDocuments={true}
showMetadata={false}
onFileDelete={handleFileDelete}
onFileRemove={handleFileRemove}
onFileView={handleFileView}
onFileDownload={handleFileDownload}
deletingFiles={deletingFiles}
previewingFiles={previewingFiles}
downloadingFiles={downloadingFiles}
workflowId={workflowId}
emptyMessage="Keine Nachrichten"
/>
);
};
@ -615,9 +590,9 @@ export const PlaygroundPage: React.FC = () => {
disabled={isRunning}
>
<option value="">Neuer Workflow</option>
{(workflowItems || workflows)?.map((item: any) => (
{workflowItems?.map((item: any) => (
<option key={item.id} value={item.id}>
{item.label || item.name || item.id}
{item.label || item.id}
</option>
))}
</select>

View file

@ -98,25 +98,20 @@ export const WorkflowsPage: React.FC = () => {
}
};
// Handle delete single workflow
// Handle delete single workflow (confirmation handled by DeleteActionButton)
const handleDelete = async (workflow: Workflow) => {
if (window.confirm(`Möchten Sie den Workflow "${workflow.name || workflow.id}" wirklich löschen?`)) {
const success = await handleWorkflowDelete(workflow.id);
if (success) {
refetch();
}
const success = await handleWorkflowDelete(workflow.id);
if (success) {
refetch();
}
};
// Handle delete multiple workflows
// Handle delete multiple workflows (confirmation handled by FormGenerator)
const handleDeleteMultiple = async (workflowsToDelete: Workflow[]) => {
const count = workflowsToDelete.length;
if (window.confirm(`Möchten Sie ${count} Workflow(s) wirklich löschen?`)) {
const ids = workflowsToDelete.map(w => w.id);
const success = await handleWorkflowDeleteMultiple(ids);
if (success) {
refetch();
}
const ids = workflowsToDelete.map(w => w.id);
const success = await handleWorkflowDeleteMultiple(ids);
if (success) {
refetch();
}
};