diff --git a/src/components/AccessRules/AccessRulesEditor.tsx b/src/components/AccessRules/AccessRulesEditor.tsx index 2f0c13b..8098eb9 100644 --- a/src/components/AccessRules/AccessRulesEditor.tsx +++ b/src/components/AccessRules/AccessRulesEditor.tsx @@ -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 = ({ mandateId, featureCode, }) => { + const { showError } = useToast(); const { rules, loading, @@ -580,9 +582,8 @@ export const AccessRulesEditor: React.FC = ({ }, [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 = ({ 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[]) => { diff --git a/src/components/FormGenerator/ActionButtons/DownloadActionButton/DownloadActionButton.tsx b/src/components/FormGenerator/ActionButtons/DownloadActionButton/DownloadActionButton.tsx new file mode 100644 index 0000000..3868ce0 --- /dev/null +++ b/src/components/FormGenerator/ActionButtons/DownloadActionButton/DownloadActionButton.tsx @@ -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 { + row: T; + onDownload: (row: T) => Promise | void; + disabled?: boolean | { disabled: boolean; message?: string }; + loading?: boolean; + className?: string; + title?: string; + hookData?: any; + idField?: string; + loadingStateName?: string; +} + +export function DownloadActionButton({ + row, + onDownload, + disabled = false, + loading = false, + className = '', + title, + hookData, + idField = 'id', + loadingStateName = 'downloadingFiles' +}: DownloadActionButtonProps) { + 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 ( + + ); +} + +export default DownloadActionButton; diff --git a/src/components/FormGenerator/ActionButtons/DownloadActionButton/index.ts b/src/components/FormGenerator/ActionButtons/DownloadActionButton/index.ts new file mode 100644 index 0000000..5d196ca --- /dev/null +++ b/src/components/FormGenerator/ActionButtons/DownloadActionButton/index.ts @@ -0,0 +1,2 @@ +export { DownloadActionButton, type DownloadActionButtonProps } from './DownloadActionButton'; +export { DownloadActionButton as default } from './DownloadActionButton'; diff --git a/src/components/FormGenerator/ActionButtons/index.ts b/src/components/FormGenerator/ActionButtons/index.ts index c7ce88b..464bce2 100644 --- a/src/components/FormGenerator/ActionButtons/index.ts +++ b/src/components/FormGenerator/ActionButtons/index.ts @@ -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'; diff --git a/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx b/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx index e96d403..5957c7b 100644 --- a/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx +++ b/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx @@ -853,7 +853,23 @@ export function FormGeneratorForm>({ 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>({ 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>({ 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>({ } }} /> -