fixed ai call end to end with saas multimandate
This commit is contained in:
parent
28af4cb068
commit
f41e6d0088
30 changed files with 371 additions and 233 deletions
|
|
@ -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[]) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { DownloadActionButton, type DownloadActionButtonProps } from './DownloadActionButton';
|
||||
export { DownloadActionButton as default } from './DownloadActionButton';
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 || [],
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue