finished files page

This commit is contained in:
Ida Dittrich 2025-10-12 14:36:39 +02:00
parent b238ab87a5
commit 6988984cd7
59 changed files with 1263 additions and 902 deletions

View file

@ -11,8 +11,6 @@ import { AuthProvider } from './auth/authProvider';
import { ProtectedRoute } from './auth/ProtectedRoute';
import { LanguageProvider } from './contexts/LanguageContext';
import Home from './pages/Home/Home';
// Import the global light theme CSS variables as default
import './assets/styles/light.css';
function App() {
// Load saved theme preference and set app name on app mount

View file

@ -1,5 +1,5 @@
import { Popup, EditForm } from '../Popup';
import { Popup, EditForm } from '../ui/Popup';
import styles from './ConnectionEditModal.module.css';
import { ConnectionEditModalProps } from './connectionsInterfaces';
import { useLanguage } from '../../contexts/LanguageContext';

View file

@ -1,5 +1,5 @@
import { ColumnConfig } from '../FormGenerator';
import { EditFieldConfig } from '../Popup';
import { EditFieldConfig } from '../ui/Popup';
// Import React for component types
import React from 'react';

View file

@ -7,7 +7,7 @@ import { MdModeEdit } from 'react-icons/md';
import { useConnections, useOAuthConnect, useDisconnect } from '../../hooks/useConnections';
import { useLanguage } from '../../contexts/LanguageContext';
import { ColumnConfig } from '../FormGenerator';
import { EditFieldConfig } from '../Popup';
import { EditFieldConfig } from '../ui/Popup';
import {
Connection,
CreateConnectionData,

View file

@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { IoIosDownload, IoIosCopy } from 'react-icons/io';
import { Popup, PopupAction } from '../Popup/Popup';
import { Popup, PopupAction } from '../ui/Popup/Popup';
import { useLanguage } from '../../contexts/LanguageContext';
import { useFileOperations } from '../../hooks/useFiles';
import {

View file

@ -67,6 +67,11 @@
animation: spin 1s linear infinite;
}
/* Delete button loading state - no animation for user-friendly experience */
.actionButton.delete.loading .actionIcon {
animation: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }

View file

@ -147,6 +147,9 @@ export function DeleteActionButton<T = any>({
// Use loading state from hookData if available
const isDeletingFromHook = loadingState?.has((row as any)[idField]) || false;
// Check if ANY deletion is in progress (not just this specific item)
const isAnyDeletionInProgress = loadingState && loadingState.size > 0;
if (isConfirming) {
return (
<div className={styles.deleteConfirmButtons}>
@ -180,9 +183,9 @@ export function DeleteActionButton<T = any>({
return (
<button
onClick={handleDeleteClick}
className={`${styles.actionButton} ${styles.delete} ${loading || isDeleting || isDeletingFromHook ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`}
className={`${styles.actionButton} ${styles.delete} ${loading || isDeleting || isAnyDeletionInProgress ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`}
title={finalTitle}
disabled={isDisabled || loading || isDeleting || isDeletingFromHook}
disabled={isDisabled || loading || isDeleting || isAnyDeletionInProgress}
>
<span className={styles.actionIcon}>
<IoIosTrash />

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { MdModeEdit } from 'react-icons/md';
import { useLanguage } from '../../../../contexts/LanguageContext';
import { Popup, EditForm } from '../../../Popup';
import { Popup, EditForm } from '../../../ui/Popup';
import styles from '../ActionButton.module.css';
export interface EditActionButtonProps<T = any> {
@ -72,13 +72,7 @@ export function EditActionButton<T = any>({
const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false;
const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined;
// Debug logging for disabled state
if (import.meta.env.DEV) {
console.log('EditActionButton disabled prop:', disabled);
console.log('EditActionButton isDisabled:', isDisabled);
console.log('EditActionButton disabledMessage:', disabledMessage);
console.log('EditActionButton row:', row);
}
// Validate that hookData is provided
if (!hookData) {
@ -91,15 +85,7 @@ export function EditActionButton<T = any>({
setInternalLoading(true);
try {
// Debug logging to see what data we're working with
if (import.meta.env.DEV) {
console.log('EditActionButton received row:', row);
console.log('Field mappings:', { idField, nameField, typeField });
console.log('Extracted values:', {
id: (row as any)[idField],
name: (row as any)[nameField],
type: (row as any)[typeField]
});
}
// Call the onEdit callback if provided
if (onEdit) {
@ -137,18 +123,7 @@ export function EditActionButton<T = any>({
}
});
// Debug logging
if (import.meta.env.DEV) {
console.log('EditActionButton - Update data:', {
itemId,
updateData,
originalData: editData,
updatedData,
editFields: editFields.map(f => ({ key: f.key, value: (updatedData as any)[f.key] })),
updateDataKeys: Object.keys(updateData),
updateDataValues: Object.values(updateData)
});
}
// Validate required operation exists
if (!hookData[operationName]) {
@ -195,15 +170,7 @@ export function EditActionButton<T = any>({
// Determine the final button title (tooltip)
const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle;
// Debug logging for button rendering
if (import.meta.env.DEV) {
console.log('EditActionButton rendering with:', {
isDisabled,
isLoading,
finalTitle,
className: `${styles.actionButton} ${styles.edit} ${isLoading ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`
});
}
return (
<>

View file

@ -47,16 +47,6 @@ export function ViewActionButton<T = any>({
if (!isDisabled && !loading && !isViewing && !internalLoading) {
setInternalLoading(true);
try {
// Debug logging to see what data we're working with
if (import.meta.env.DEV) {
console.log('ViewActionButton received row:', row);
console.log('Field mappings:', { idField, nameField, typeField });
console.log('Extracted values:', {
id: (row as any)[idField],
name: (row as any)[nameField],
type: (row as any)[typeField]
});
}
// Call the onView callback if provided
if (onView) {

View file

@ -7,8 +7,10 @@ import {
DownloadActionButton,
ViewActionButton
} from './ActionButtons';
import { Button } from '../ui/Button';
import { IoIosRefresh } from "react-icons/io";
import { FaTrash } from "react-icons/fa";
// Types for the FormGenerator
export interface ColumnConfig {
@ -447,28 +449,28 @@ export function FormGenerator<T extends Record<string, any>>({
{selectable && selectedRows.size > 0 && (
<div className={styles.deleteControlsIntegrated}>
{selectedRows.size === 1 && onDelete && (
<button
<Button
onClick={() => {
const selectedIndex = Array.from(selectedRows)[0];
const selectedRow = paginatedData[selectedIndex];
handleDeleteSingle(selectedRow, selectedIndex);
}}
className={styles.deleteButton}
title={t('formgen.delete.single', 'Delete selected item')}
variant="primary"
size="sm"
icon={FaTrash}
>
<span className={styles.deleteIcon}></span>
{t('formgen.delete.single', 'Delete')}
</button>
</Button>
)}
{selectedRows.size > 1 && onDeleteMultiple && (
<button
<Button
onClick={handleDeleteMultiple}
className={styles.deleteAllButton}
title={t('formgen.delete.multiple', `Delete ${selectedRows.size} selected items`)}
variant="primary"
size="sm"
icon={FaTrash}
>
<span className={styles.deleteIcon}></span>
{t('formgen.delete.multiple', `Delete ${selectedRows.size} selected items`).replace('{count}', selectedRows.size.toString())}
</button>
</Button>
)}
</div>
@ -768,20 +770,9 @@ export function FormGenerator<T extends Record<string, any>>({
? actionButton.title(row)
: actionButton.title;
const disabledResult = actionButton.disabled ? actionButton.disabled(row) : false;
const isDisabled = typeof disabledResult === 'boolean' ? disabledResult : disabledResult?.disabled || false;
const isLoading = actionButton.loading ? actionButton.loading(row) : false;
const isProcessing = actionButton.isProcessing ? actionButton.isProcessing(row) : false;
// Debug logging for disabled state
if (actionButton.type === 'edit' && import.meta.env.DEV) {
console.log('FormGenerator edit button:', {
hasDisabledFn: !!actionButton.disabled,
disabledFn: actionButton.disabled,
row,
disabledResult,
isDisabled
});
}
const baseProps = {
row,
@ -797,12 +788,6 @@ export function FormGenerator<T extends Record<string, any>>({
loadingStateName: actionButton.loadingStateName
};
// Debug logging for view buttons
if (actionButton.type === 'view' && import.meta.env.DEV) {
console.log('FormGenerator actionButton config:', actionButton);
console.log('FormGenerator baseProps:', baseProps);
}
switch (actionButton.type) {
case 'edit':
return <EditActionButton

View file

@ -1,6 +1,6 @@
import { FormGenerator } from '../FormGenerator/FormGenerator';
import { Popup } from '../Popup/Popup';
import { EditForm, EditFieldConfig } from '../Popup/EditForm';
import { Popup } from '../ui/Popup/Popup';
import { EditForm, EditFieldConfig } from '../ui/Popup/EditForm';
import { useMitgliederLogic } from './mitgliederLogic';
import { MitgliederTableProps } from './mitgliederTypes';
import { useLanguage } from '../../contexts/LanguageContext';

View file

@ -1,8 +1,8 @@
import { FormGenerator } from '../FormGenerator/FormGenerator';
import { Popup } from '../Popup/Popup';
import { EditForm } from '../Popup/EditForm';
import { Popup } from '../ui/Popup/Popup';
import { EditForm } from '../ui/Popup/EditForm';
import { usePromptsLogic } from './promptsLogic';
import { PromptsTableProps, Prompt } from './promptsTypes';
import { useLanguage } from '../../contexts/LanguageContext';

View file

@ -4,7 +4,7 @@ import { MdModeEdit } from 'react-icons/md';
import { usePrompts, usePromptOperations, Prompt } from '../../hooks/usePrompts';
import { useLanguage } from '../../contexts/LanguageContext';
import type { EditFieldConfig } from '../Popup/EditForm';
import type { EditFieldConfig } from '../ui/Popup/EditForm';
// Helper function to determine if a prompt can be deleted
const isPromptDeletable = (prompt: Prompt): boolean => {

View file

@ -2,7 +2,7 @@
.sidebarContainer {
border-radius: 0px;
background: var(--color-bg);
/*background-image: url('../../../assets/styles/bg.jpg');
/*background-image: url('../../../styles/assets/bg.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;

View file

@ -63,3 +63,10 @@
overflow: hidden;
white-space: nowrap;
}
.submenuIcon {
width: 16px;
height: 16px;
color: #181818;
flex-shrink: 0;
}

View file

@ -39,6 +39,8 @@ const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen }) => {
return () => window.removeEventListener('resize', checkOverflow);
}, [subitem.name]);
const SubIcon = subitem.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
return (
<li key={subitem.id}>
<Link
@ -67,8 +69,11 @@ const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen }) => {
}
})}
>
<div style={{ display: 'inline-block', paddingRight: '10px' }}>
<div style={{ display: 'flex', alignItems: 'center', paddingRight: '10px' }}>
{SubIcon && <SubIcon className={styles.submenuIcon} />}
<span style={{ marginLeft: SubIcon ? '8px' : '0' }}>
{subitem.name}
</span>
</div>
</motion.span>
</div>

View file

@ -15,6 +15,7 @@ export interface SidebarSubmenuItemData {
id: string;
name: string;
link?: string;
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}
// Sidebar state interface

View file

@ -1,7 +1,7 @@
import { FormGenerator } from '../FormGenerator/FormGenerator';
import { Popup, EditForm } from '../Popup';
import { Popup, EditForm } from '../ui/Popup';
import { useWorkflowsLogic } from './workflowsLogic';
import { WorkflowsTableProps } from './workflowsTypes';
import styles from './WorkflowsTable.module.css';

View file

@ -7,7 +7,7 @@ import { MdModeEdit } from 'react-icons/md';
import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
import { useApiRequest } from '../../hooks/useApi';
import { useLanguage } from '../../contexts/LanguageContext';
import type { EditFieldConfig } from '../Popup/EditForm';
import type { EditFieldConfig } from '../ui/Popup/EditForm';
import type {
WorkflowsLogicReturn,

View file

@ -26,4 +26,3 @@ export interface UploadButtonProps extends BaseButtonProps {
icon?: IconType;
iconPosition?: 'left' | 'right';
}

View file

@ -1,6 +1,6 @@
import React, { useRef, useState } from 'react';
import { UploadButtonProps } from '../Button/ButtonTypes';
import Button from '../Button/Button';
import { UploadButtonProps } from '../ButtonTypes';
import Button from '../Button';
const UploadButton: React.FC<UploadButtonProps> = ({
onUpload,
@ -54,7 +54,6 @@ const UploadButton: React.FC<UploadButtonProps> = ({
};
const isDisabled = disabled || loading || isUploading;
const isButtonLoading = loading || isUploading;
return (
<>
@ -63,12 +62,15 @@ const UploadButton: React.FC<UploadButtonProps> = ({
variant={variant}
size={size}
disabled={isDisabled}
loading={isButtonLoading}
loading={false} // We handle the loading state manually
className={`uploadButton ${className}`}
onClick={handleClick}
icon={icon}
icon={isUploading ? undefined : icon} // Hide original icon when uploading
iconPosition={iconPosition}
>
{isUploading && (
<div className="spinnerIcon" style={{ marginRight: '8px' }} />
)}
{children || (isUploading ? 'Uploading...' : 'Upload File')}
</Button>

View file

@ -0,0 +1,2 @@
export { default as UploadButton } from './UploadButton';
export type { UploadButtonProps } from '../ButtonTypes';

View file

@ -1,3 +1,2 @@
export { default as Button } from './Button';
export * from './ButtonTypes';

View file

@ -0,0 +1,165 @@
.dragDropContainer {
position: relative;
width: 100%;
height: 100%;
}
.hiddenFileInput {
position: absolute;
top: -9999px;
left: -9999px;
opacity: 0;
pointer-events: none;
}
.dragOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(var(--color-primary-rgb, 0, 123, 255), 0.1);
border: 2px dashed var(--color-primary, #007bff);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(2px);
animation: fadeIn 0.2s ease-in-out;
}
.dragOverlayContent {
text-align: center;
color: var(--color-primary, #007bff);
padding: 2rem;
}
.dragIcon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.8;
}
.dragText {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.dragSubtext {
font-size: 1rem;
opacity: 0.8;
max-width: 300px;
line-height: 1.4;
}
.processingOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(var(--color-bg-rgb, 255, 255, 255), 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 1001;
backdrop-filter: blur(2px);
animation: fadeIn 0.2s ease-in-out;
}
.processingContent {
text-align: center;
color: var(--color-text, #333);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--color-primary, #007bff);
border-top: 3px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
.processingText {
font-size: 1.1rem;
font-weight: 500;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Dark theme support */
[data-theme="dark"] .dragOverlay {
background: rgba(var(--color-primary-rgb, 0, 123, 255), 0.15);
border-color: var(--color-primary, #007bff);
}
[data-theme="dark"] .dragOverlayContent {
color: var(--color-primary, #007bff);
}
[data-theme="dark"] .processingOverlay {
background: rgba(var(--color-bg-rgb, 0, 0, 0), 0.9);
}
[data-theme="dark"] .processingContent {
color: var(--color-text, #fff);
}
/* Responsive design */
@media (max-width: 768px) {
.dragOverlayContent {
padding: 1.5rem;
}
.dragIcon {
font-size: 2.5rem;
}
.dragText {
font-size: 1.25rem;
}
.dragSubtext {
font-size: 0.9rem;
}
}
@media (max-width: 480px) {
.dragOverlayContent {
padding: 1rem;
}
.dragIcon {
font-size: 2rem;
}
.dragText {
font-size: 1.1rem;
}
.dragSubtext {
font-size: 0.85rem;
}
}

View file

@ -0,0 +1,195 @@
import React, { useState, useCallback } from 'react';
import { useLanguage } from '../../../contexts/LanguageContext';
import styles from './DragDropOverlay.module.css';
import { IoFolderOpen } from 'react-icons/io5';
export interface DragDropConfig {
enabled: boolean;
onDrop?: (files: File[]) => Promise<void> | void;
accept?: string; // MIME types or file extensions
multiple?: boolean;
disabled?: boolean;
overlayText?: string;
overlaySubtext?: string;
}
interface DragDropOverlayProps {
config: DragDropConfig;
children: React.ReactNode;
className?: string;
}
export function DragDropOverlay({
config,
children,
className = ''
}: DragDropOverlayProps) {
const { t } = useLanguage();
const [isDragOver, setIsDragOver] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (config.disabled || !config.enabled) return;
setIsDragOver(true);
}, [config.disabled, config.enabled]);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// Only hide overlay if we're leaving the container entirely
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragOver(false);
}
}, []);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (config.disabled || !config.enabled) return;
setIsDragOver(false);
setIsProcessing(true);
try {
const files = Array.from(e.dataTransfer.files);
// Filter files by accept type if specified
let filteredFiles = files;
if (config.accept && config.accept !== '*/*') {
filteredFiles = files.filter(file => {
const acceptTypes = config.accept!.split(',').map(type => type.trim());
return acceptTypes.some(acceptType => {
// Handle MIME types
if (acceptType.startsWith('.')) {
return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
}
// Handle MIME types like "image/*" or "application/pdf"
if (acceptType.includes('*')) {
const baseType = acceptType.split('/')[0];
return file.type.startsWith(baseType);
}
return file.type === acceptType;
});
});
}
console.log('🔍 DragDropOverlay file filtering:', {
originalFiles: files.length,
filteredFiles: filteredFiles.length,
accept: config.accept,
fileDetails: files.map(f => ({ name: f.name, type: f.type }))
});
// Respect multiple setting
const filesToProcess = config.multiple ? filteredFiles : filteredFiles.slice(0, 1);
if (filesToProcess.length > 0 && config.onDrop) {
console.log('🎯 DragDropOverlay calling onDrop with files:', filesToProcess);
await config.onDrop(filesToProcess);
console.log('✅ DragDropOverlay onDrop completed');
} else {
console.log('⚠️ DragDropOverlay: No files to process or no onDrop handler');
console.log('Files to process:', filesToProcess.length);
console.log('Has onDrop:', !!config.onDrop);
}
} catch (error) {
console.error(t('dragdrop.overlay.error', 'Error processing files'), error);
} finally {
setIsProcessing(false);
}
}, [config, t]);
const handleFileInputChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
if (config.disabled || !config.enabled) return;
const files = Array.from(e.target.files || []);
console.log('🔍 DragDropOverlay file input:', {
fileCount: files.length,
accept: config.accept,
fileDetails: files.map(f => ({ name: f.name, type: f.type }))
});
if (files.length > 0 && config.onDrop) {
setIsProcessing(true);
try {
await config.onDrop(files);
} catch (error) {
console.error(t('dragdrop.overlay.error', 'Error processing files'), error);
} finally {
setIsProcessing(false);
// Reset input
e.target.value = '';
}
}
}, [config, t]);
if (!config.enabled) {
return <>{children}</>;
}
return (
<div
className={`${styles.dragDropContainer} ${className}`}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
{children}
{/* Hidden file input for click-to-upload */}
<input
type="file"
accept={config.accept}
multiple={config.multiple}
onChange={handleFileInputChange}
className={styles.hiddenFileInput}
disabled={config.disabled}
/>
{/* Drag overlay */}
{isDragOver && (
<div className={styles.dragOverlay}>
<div className={styles.dragOverlayContent}>
<div className={styles.dragIcon}>
<IoFolderOpen />
</div>
<div className={styles.dragText}>
{config.overlayText || t('dragdrop.overlay.default_text', 'Drop files here')}
</div>
{(config.overlaySubtext || !config.overlayText) && (
<div className={styles.dragSubtext}>
{config.overlaySubtext || t('dragdrop.overlay.default_subtext', 'You can also click the upload button')}
</div>
)}
</div>
</div>
)}
{/* Processing overlay */}
{isProcessing && (
<div className={styles.processingOverlay}>
<div className={styles.processingContent}>
<div className={styles.spinner}></div>
<div className={styles.processingText}>
{t('dragdrop.overlay.processing', 'Processing files...')}
</div>
</div>
</div>
)}
</div>
);
}
export default DragDropOverlay;

View file

@ -0,0 +1,3 @@
export { DragDropOverlay } from './DragDropOverlay';
export type { DragDropConfig } from './DragDropOverlay';
export { DragDropOverlay as default } from './DragDropOverlay';

View file

@ -0,0 +1,175 @@
.messageOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.1);
pointer-events: auto;
animation: fadeIn 0.3s ease-out forwards;
}
.messageOverlay.closing {
animation: fadeOut 0.3s ease-out forwards;
pointer-events: none;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slideIn {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(-100%);
opacity: 0;
}
}
.messageContainer {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 30px;
pointer-events: none;
}
.messageContent {
border-radius: var(--object-radius-large);
padding: 30px;
max-width: 500px;
width: 100%;
position: relative;
border: 2px solid;
pointer-events: auto;
animation: slideIn 0.4s ease-out forwards;
}
.messageOverlay.closing .messageContent {
animation: slideOut 0.3s ease-out forwards;
}
/* Warning Mode */
.warningMode {
background-color: color-mix(in srgb, var(--color-red) 20%, transparent);
border-color: var(--color-red);
box-shadow: 0 8px 32px rgba(220, 38, 38, 0.4);
}
/* Error Mode */
.errorMode {
background-color: color-mix(in srgb, var(--color-red) 20%, transparent);
border-color: var(--color-red);
box-shadow: 0 8px 32px rgba(220, 38, 38, 0.4);
}
/* Success Mode */
.successMode {
background-color: color-mix(in srgb, var(--color-green, #16a34a) 90%, black);
border-color: var(--color-green, #16a34a);
box-shadow: 0 8px 32px rgba(22, 163, 74, 0.4);
}
/* Info Mode */
.infoMode {
background-color: color-mix(in srgb, var(--color-blue, #2563eb) 90%, black);
border-color: var(--color-blue, #2563eb);
box-shadow: 0 8px 32px rgba(37, 99, 235, 0.4);
}
.messageHeader {
font-size: 18px;
font-weight: 600;
color: white;
line-height: 1.4;
}
.messageText {
font-size: 16px;
color: rgba(255, 255, 255, 0.9);
line-height: 1.5;
}
.closeButton {
position: absolute;
top: 12px;
right: 12px;
background: none;
border: none;
color: var(--color-red);
font-size: 24px;
font-weight: bold;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s ease, opacity 0.2s ease;
line-height: 1;
}
.closeButton:hover {
background: rgba(255, 255, 255, 0.2);
opacity: 1;
}
.closeButton:active {
background: rgba(255, 255, 255, 0.3);
opacity: 1;
}
/* Responsive design */
@media (max-width: 640px) {
.messageContainer {
padding: 15px;
}
.messageContent {
padding: 20px;
}
.messageHeader {
font-size: 16px;
}
.messageText {
font-size: 14px;
}
.closeButton {
top: 8px;
right: 8px;
font-size: 20px;
padding: 2px 6px;
}
}

View file

@ -0,0 +1,131 @@
import React, { useEffect, useState, useRef } from 'react';
import styles from './MessageOverlay.module.css';
export type MessageMode = 'warning' | 'error' | 'success' | 'info';
interface MessageOverlayProps {
header: string;
message: string;
isVisible: boolean;
mode?: MessageMode;
onClose?: () => void;
autoClose?: boolean;
autoCloseDelay?: number;
}
const MessageOverlay: React.FC<MessageOverlayProps> = ({
header,
message,
isVisible,
mode = 'info',
onClose,
autoClose = true,
autoCloseDelay = 5000
}) => {
const [isClosing, setIsClosing] = useState(false);
const autoCloseTimeoutRef = useRef<number | null>(null);
const unmountTimeoutRef = useRef<number | null>(null);
// Handle auto-close
useEffect(() => {
if (autoCloseTimeoutRef.current) {
clearTimeout(autoCloseTimeoutRef.current);
}
if (isVisible && autoClose && onClose) {
autoCloseTimeoutRef.current = window.setTimeout(() => {
setIsClosing(true);
// After animation completes, call onClose
unmountTimeoutRef.current = window.setTimeout(() => {
onClose();
}, 700); // Match slideOut animation duration
}, autoCloseDelay);
}
return () => {
if (autoCloseTimeoutRef.current) {
clearTimeout(autoCloseTimeoutRef.current);
}
};
}, [isVisible, autoClose, autoCloseDelay, onClose]);
// Handle manual close
const handleClose = () => {
setIsClosing(true);
setTimeout(() => {
if (onClose) {
onClose();
}
}, 700);
};
// Cleanup on unmount
useEffect(() => {
return () => {
if (autoCloseTimeoutRef.current) {
clearTimeout(autoCloseTimeoutRef.current);
}
if (unmountTimeoutRef.current) {
clearTimeout(unmountTimeoutRef.current);
}
};
}, []);
if (!isVisible) {
return null;
}
const getModeClass = () => {
switch (mode) {
case 'warning':
return styles.warningMode;
case 'error':
return styles.errorMode;
case 'success':
return styles.successMode;
case 'info':
default:
return styles.infoMode;
}
};
const getAriaLabel = () => {
switch (mode) {
case 'warning':
return 'Close warning';
case 'error':
return 'Close error';
case 'success':
return 'Close success message';
case 'info':
default:
return 'Close message';
}
};
return (
<div className={`${styles.messageOverlay} ${isClosing ? styles.closing : ''}`}>
<div className={styles.messageContainer}>
<div className={`${styles.messageContent} ${getModeClass()}`}>
<div className={styles.messageHeader}>
{header}
</div>
<div className={styles.messageText}>
{message}
</div>
{onClose && (
<button
className={styles.closeButton}
onClick={handleClose}
aria-label={getAriaLabel()}
>
×
</button>
)}
</div>
</div>
</div>
);
};
export default MessageOverlay;

View file

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

View file

@ -1,3 +0,0 @@
export { default as UploadButton } from './UploadButton';
export type { UploadButtonProps } from '../Button/ButtonTypes';

View file

@ -1,3 +1,5 @@
export * from './Button';
export * from './UploadButton';
export * from './Button/UploadButton';
export { default as MessageOverlay } from './MessageOverlay';
export type { MessageMode } from './MessageOverlay';
export * from './DragDropOverlay';

View file

@ -3,7 +3,6 @@ import { useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { getPageDataByPath, GenericPageData, PageInstance } from './data';
import PageRenderer from './PageRenderer';
import { useLanguage } from '../../contexts/LanguageContext';
interface PageManagerProps {
loadingComponent: React.ComponentType;
@ -16,7 +15,6 @@ const PageManager: React.FC<PageManagerProps> = ({
}) => {
const location = useLocation();
const [pageInstances, setPageInstances] = useState<Map<string, PageInstance>>(new Map());
const { currentLanguage } = useLanguage();
// Get current path
const getCurrentPath = () => {
@ -100,7 +98,6 @@ const PageManager: React.FC<PageManagerProps> = ({
) : (
<PageRenderer
pageData={pageData}
language={currentLanguage}
onButtonClick={(buttonId, button) => {
console.log(`Button clicked: ${buttonId}`, button);
// Add global button click handling here

View file

@ -2,19 +2,22 @@ import React from 'react';
import { GenericPageData, PageButton, PageContent, resolveLanguageText } from './pageInterface';
import { FormGenerator } from '../../components/FormGenerator';
import { Button, UploadButton } from '../../components/ui';
import styles from './pages.module.css';
import { DragDropOverlay } from '../../components/ui/DragDropOverlay';
import { useLanguage } from '../../contexts/LanguageContext';
import styles from '../../styles/pages.module.css';
interface PageRendererProps {
pageData: GenericPageData;
onButtonClick?: (buttonId: string, button: PageButton) => void;
language?: 'de' | 'en' | 'fr';
}
const PageRenderer: React.FC<PageRendererProps> = ({
pageData,
onButtonClick,
language = 'de'
onButtonClick
}) => {
// Get translation function from language context
const { t } = useLanguage();
// Call the hook at the top level to ensure it persists across renders
// This is CRITICAL - hooks must be called in the same order on every render
const tableContent = pageData.content?.find(content => content.type === 'table');
@ -33,6 +36,12 @@ const PageRenderer: React.FC<PageRendererProps> = ({
// This will be called on every render, but it's the SAME hook instance
const hookData = useTableData ? useTableData() : null;
// Debug hook data
if (import.meta.env.DEV && hookData) {
console.log('🔍 PageRenderer hookData:', hookData);
console.log('🔍 PageRenderer has handleUpload:', !!hookData.handleUpload);
}
// Handle button clicks
const handleButtonClick = async (button: PageButton) => {
try {
@ -67,13 +76,13 @@ const PageRenderer: React.FC<PageRendererProps> = ({
return React.createElement(
HeadingTag,
{ key: content.id, className: styles.contentHeading },
resolveLanguageText(content.content, language)
resolveLanguageText(content.content, t)
);
case 'paragraph':
return (
<p key={content.id} className={styles.contentParagraph}>
{resolveLanguageText(content.content, language)}
{resolveLanguageText(content.content, t)}
</p>
);
@ -81,12 +90,12 @@ const PageRenderer: React.FC<PageRendererProps> = ({
return (
<div key={content.id} className={styles.listContainer}>
{content.content && (
<p className={styles.listTitle}>{resolveLanguageText(content.content, language)}</p>
<p className={styles.listTitle}>{resolveLanguageText(content.content, t)}</p>
)}
<ul className={styles.list}>
{content.items?.map((item, index) => (
<li key={index} className={styles.listItem}>
{resolveLanguageText(item, language)}
{resolveLanguageText(item, t)}
</li>
))}
</ul>
@ -97,7 +106,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
return (
<pre key={content.id} className={styles.codeBlock}>
<code className={content.language ? `language-${content.language}` : ''}>
{resolveLanguageText(content.content, language)}
{resolveLanguageText(content.content, t)}
</code>
</pre>
);
@ -142,7 +151,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
// CRITICAL: Resolve LanguageText objects in column labels
const resolvedColumns = columns.map(col => ({
...col,
label: resolveLanguageText(col.label, language)
label: resolveLanguageText(col.label, t)
}));
// Convert action buttons to FormGenerator format
@ -152,7 +161,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
type: action.type,
onAction: action.onAction,
// CRITICAL: Resolve LanguageText objects in action titles
title: resolveLanguageText(action.title, language),
title: resolveLanguageText(action.title, t),
isProcessing: action.loading || (() => false),
disabled: action.disabled || (() => false),
// Preserve field mappings and operation names
@ -177,6 +186,8 @@ const PageRenderer: React.FC<PageRendererProps> = ({
loading={showLoadingSpinner}
actionButtons={formGeneratorActions}
hookData={hookData}
onDelete={hookData.onDelete}
onDeleteMultiple={hookData.onDeleteMultiple}
{...tableProps}
/>
</div>
@ -189,15 +200,52 @@ const PageRenderer: React.FC<PageRendererProps> = ({
}
};
// Create enhanced drag drop config with hook data integration
const getDragDropConfig = () => {
if (!pageData.dragDropConfig) {
return { enabled: false, onDrop: () => {} };
}
console.log('🔍 DragDrop Debug - hookData:', hookData);
console.log('🔍 DragDrop Debug - has handleUpload:', !!hookData?.handleUpload);
// If the page has drag drop config and hook data with handleUpload, integrate them
if (hookData?.handleUpload) {
return {
...pageData.dragDropConfig,
onDrop: async (files: File[]) => {
console.log('🚀 DragDrop onDrop triggered with files:', files);
try {
// Process each file through the hook's handleUpload function
for (const file of files) {
if (hookData.handleUpload) {
console.log('📤 Uploading file:', file.name);
await hookData.handleUpload(file);
console.log('✅ File uploaded successfully:', file.name);
}
}
} catch (error) {
console.error('❌ Error uploading dropped files:', error);
}
}
};
}
console.log('⚠️ DragDrop Debug - No handleUpload found, using fallback config');
// Fallback to the original config
return pageData.dragDropConfig;
};
return (
<DragDropOverlay config={getDragDropConfig()}>
<div className={styles.pageContainer}>
<div className={styles.pageCard}>
{/* Page Header */}
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{resolveLanguageText(pageData.title, language)}</h1>
<h1 className={styles.pageTitle}>{resolveLanguageText(pageData.title, t)}</h1>
{pageData.subtitle && (
<p className={styles.pageSubtitle}>{resolveLanguageText(pageData.subtitle, language)}</p>
<p className={styles.pageSubtitle}>{resolveLanguageText(pageData.subtitle, t)}</p>
)}
</div>
@ -221,7 +269,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
icon={button.icon}
disabled={button.disabled}
>
{resolveLanguageText(button.label, language)}
{resolveLanguageText(button.label, t)}
</UploadButton>
);
}
@ -237,7 +285,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
disabled={button.disabled}
onClick={() => handleButtonClick(button)}
>
{resolveLanguageText(button.label, language)}
{resolveLanguageText(button.label, t)}
</Button>
);
})}
@ -262,7 +310,11 @@ const PageRenderer: React.FC<PageRendererProps> = ({
</div>
</div>
</div>
{/* Message Overlay Component */}
{hookData?.MessageOverlayComponent && <hookData.MessageOverlayComponent />}
</div>
</DragDropOverlay>
);
};

View file

@ -1,5 +1,7 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { allPageData, SidebarItem } from './data';
import { useLanguage } from '../../contexts/LanguageContext';
import { resolveLanguageText } from './pageInterface';
interface SidebarContextType {
sidebarItems: SidebarItem[];
@ -27,6 +29,9 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Get translation function from language context
const { t } = useLanguage();
// Get sidebar items from page data
const getSidebarItems = async (): Promise<SidebarItem[]> => {
const items: SidebarItem[] = [];
@ -72,22 +77,23 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
// Create expandable item with submenu
items.push({
id: pageData.id,
name: pageData.name,
name: resolveLanguageText(pageData.name, t),
link: `/${pageData.path}`,
icon: pageData.icon,
moduleEnabled: pageData.moduleEnabled ?? true,
order: pageData.order || 0,
submenu: subpages.map(subpage => ({
id: subpage.id,
name: subpage.name,
link: `/${subpage.path}`
name: resolveLanguageText(subpage.name, t),
link: `/${subpage.path}`,
icon: subpage.icon
}))
});
} else {
// No subpages found, show as regular item
items.push({
id: pageData.id,
name: pageData.name,
name: resolveLanguageText(pageData.name, t),
link: `/${pageData.path}`,
icon: pageData.icon,
moduleEnabled: pageData.moduleEnabled ?? true,
@ -98,7 +104,7 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
// No subpage privilege, show as regular non-expandable item
items.push({
id: pageData.id,
name: pageData.name,
name: resolveLanguageText(pageData.name, t),
link: `/${pageData.path}`,
icon: pageData.icon,
moduleEnabled: pageData.moduleEnabled ?? true,
@ -110,7 +116,7 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
// Fallback to regular item on error
items.push({
id: pageData.id,
name: pageData.name,
name: resolveLanguageText(pageData.name, t),
link: `/${pageData.path}`,
icon: pageData.icon,
moduleEnabled: pageData.moduleEnabled ?? true,
@ -121,7 +127,7 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
// Regular items without subpages
items.push({
id: pageData.id,
name: pageData.name,
name: resolveLanguageText(pageData.name, t),
link: `/${pageData.path}`,
icon: pageData.icon,
moduleEnabled: pageData.moduleEnabled ?? true,
@ -149,10 +155,10 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
}
};
// Load sidebar items on mount
// Load sidebar items on mount and when language changes
useEffect(() => {
refreshSidebar();
}, []);
}, [t]);
const contextValue: SidebarContextType = {
sidebarItems,

View file

@ -2,45 +2,45 @@ import { GenericPageData } from '../../pageInterface';
import { FaCogs } from 'react-icons/fa';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
export const verwaltungPageData: GenericPageData = {
id: 'verwaltung',
path: 'verwaltung',
name: 'Verwaltung',
description: 'Administration and management tools',
export const administrationPageData: GenericPageData = {
id: 'administration',
path: 'administration',
name: 'administration.title',
description: 'administration.description',
// Visual
icon: FaCogs,
title: 'Verwaltung',
subtitle: 'Administration and management tools',
title: 'administration.title',
subtitle: 'administration.subtitle',
// Content sections
content: [
{
id: 'intro',
type: 'heading',
content: 'Verwaltung',
content: 'administration.title',
level: 2
},
{
id: 'description',
type: 'paragraph',
content: 'This section contains all administration and management tools for your workspace.'
content: 'administration.intro.description'
},
{
id: 'features',
type: 'heading',
content: 'Available Tools',
content: 'administration.features.title',
level: 3
},
{
id: 'features-list',
type: 'list',
content: 'Management tools include:',
content: 'administration.features.description',
items: [
'File Management - Upload and organize documents',
'User Management - Manage team members and permissions',
'System Settings - Configure workspace settings',
'Data Management - Handle data imports and exports'
'administration.features.file_management',
'administration.features.user_management',
'administration.features.system_settings',
'administration.features.data_management'
]
}
],
@ -63,6 +63,6 @@ export const verwaltungPageData: GenericPageData = {
// Lifecycle hooks
onActivate: async () => {
if (import.meta.env.DEV) console.log('Verwaltung activated');
if (import.meta.env.DEV) console.log('Administration activated');
}
};

View file

@ -1,256 +0,0 @@
import { GenericPageData } from '../../pageInterface';
import { FaCog, FaPlus, FaEdit, FaTrash, FaDownload } from 'react-icons/fa';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
// Example main page with subpages
export const examplePageData: GenericPageData = {
id: 'example-main',
path: 'example',
name: 'Example Page',
description: 'An example page showing the generic page system capabilities',
// Visual
icon: FaCog,
title: 'Example Page',
subtitle: 'This demonstrates the generic page system',
// Header buttons
headerButtons: [
{
id: 'add-item',
label: 'Add Item',
variant: 'primary',
size: 'md',
icon: FaPlus,
onClick: () => {
console.log('Adding new item...');
// Add your logic here
}
},
{
id: 'edit-mode',
label: 'Edit Mode',
variant: 'secondary',
size: 'md',
icon: FaEdit,
onClick: () => {
console.log('Toggling edit mode...');
// Add your logic here
}
},
{
id: 'export-data',
label: 'Export Data',
variant: 'success',
size: 'md',
icon: FaDownload,
onClick: () => {
console.log('Exporting data...');
// Add your logic here
}
},
{
id: 'delete-all',
label: 'Delete All',
variant: 'danger',
size: 'md',
icon: FaTrash,
onClick: () => {
console.log('Deleting all items...');
// Add your logic here
},
privilegeChecker: privilegeCheckers.adminRole // Only admins can delete all
}
],
// Content sections
content: [
{
id: 'intro',
type: 'heading',
content: 'Welcome to the Example Page',
level: 2
},
{
id: 'description',
type: 'paragraph',
content: 'This page demonstrates how to create rich, interactive pages using only data configuration. No React components needed!'
},
{
id: 'features',
type: 'heading',
content: 'Features Demonstrated',
level: 3
},
{
id: 'features-list',
type: 'list',
content: 'This page shows:',
items: [
'Dynamic header with multiple action buttons',
'Different button variants and sizes',
'Privilege-based button visibility',
'Rich content sections with headings and lists',
'Code blocks and formatted text',
'Subpage support for hierarchical navigation'
]
},
{
id: 'code-example',
type: 'heading',
content: 'Code Example',
level: 3
},
{
id: 'code-block',
type: 'code',
content: `// Example of how to create a new page
export const myPageData: GenericPageData = {
id: 'my-page',
path: 'my-page',
name: 'My Page',
title: 'My Custom Page',
content: [
{
id: 'intro',
type: 'heading',
content: 'Hello World!',
level: 2
}
]
};`,
language: 'typescript'
},
{
id: 'divider',
type: 'divider'
},
{
id: 'subpages',
type: 'heading',
content: 'Subpages',
level: 3
},
{
id: 'subpages-text',
type: 'paragraph',
content: 'This page has subpages that demonstrate hierarchical navigation. Check the sidebar to see the submenu!'
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Subpage support
hasSubpages: true,
subpagePrivilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: false,
preload: true,
moduleEnabled: true,
// Sidebar
order: 10,
showInSidebar: true,
// Lifecycle hooks
onActivate: async () => {
if (import.meta.env.DEV) console.log('Example page activated');
},
onLoad: async () => {
if (import.meta.env.DEV) console.log('Example page loaded');
}
};
// Example subpage 1
export const exampleSubpage1Data: GenericPageData = {
id: 'example-sub1',
path: 'example/subpage1',
name: 'Subpage 1',
description: 'First subpage example',
// Parent page
parentPath: 'example',
// Visual
title: 'Subpage 1',
subtitle: 'This is the first subpage',
// Content
content: [
{
id: 'intro',
type: 'heading',
content: 'Subpage 1 Content',
level: 2
},
{
id: 'description',
type: 'paragraph',
content: 'This is content for the first subpage. Subpages can have their own content, buttons, and functionality.'
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: false,
preload: false,
moduleEnabled: true,
// Sidebar - will be shown as subpage under Example
showInSidebar: false,
// Lifecycle hooks
onActivate: async () => {
if (import.meta.env.DEV) console.log('Example subpage 1 activated');
}
};
// Example subpage 2
export const exampleSubpage2Data: GenericPageData = {
id: 'example-sub2',
path: 'example/subpage2',
name: 'Subpage 2',
description: 'Second subpage example',
// Parent page
parentPath: 'example',
// Visual
title: 'Subpage 2',
subtitle: 'This is the second subpage',
// Content
content: [
{
id: 'intro',
type: 'heading',
content: 'Subpage 2 Content',
level: 2
},
{
id: 'description',
type: 'paragraph',
content: 'This is content for the second subpage. You can create as many subpages as needed!'
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: false,
preload: false,
moduleEnabled: true,
// Sidebar - will be shown as subpage under Example
showInSidebar: false,
// Lifecycle hooks
onActivate: async () => {
if (import.meta.env.DEV) console.log('Example subpage 2 activated');
}
};

View file

@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { GenericPageData, LanguageText } from '../../pageInterface';
import { GenericPageData } from '../../pageInterface';
import { FaRegFileAlt, FaUpload } from 'react-icons/fa';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
import { useUserFiles, useFileOperations } from '../../../../hooks/useFiles';
@ -11,13 +11,15 @@ const createFilesHook = () => {
const {
handleFileDownload,
handleFileDelete,
handleFileDeleteMultiple,
handleFilePreview,
handleFileUpdate,
handleFileUpload: hookHandleFileUpload,
downloadingFiles,
deletingFiles,
previewingFiles,
editingFiles
editingFiles,
MessageOverlayComponent
} = useFileOperations();
// Upload function that can be called from header buttons
@ -40,6 +42,33 @@ const createFilesHook = () => {
}
}, [hookHandleFileUpload, refetch]); // Only recreate if dependencies change
// Handle multiple file deletion for FormGenerator
const handleDeleteMultiple = useCallback(async (selectedFiles: any[]) => {
const fileIds = selectedFiles.map(file => file.id);
const success = await handleFileDeleteMultiple(fileIds, (deletedIds) => {
// Optimistically remove files from UI
deletedIds.forEach(fileId => {
removeFileOptimistically(fileId);
});
});
if (success) {
// Refetch to sync with backend
refetch();
}
}, [handleFileDeleteMultiple, removeFileOptimistically, refetch]);
// Handle single file deletion for FormGenerator
const handleDeleteSingle = useCallback(async (file: any) => {
const success = await handleFileDelete(file.id, () => {
removeFileOptimistically(file.id);
});
if (success) {
refetch();
}
}, [handleFileDelete, removeFileOptimistically, refetch]);
return {
data: files,
loading,
@ -49,14 +78,20 @@ const createFilesHook = () => {
// Operations
handleDownload: handleFileDownload,
handleDelete: handleFileDelete,
handleDeleteMultiple: handleFileDeleteMultiple,
handlePreview: handleFilePreview,
handleUpload: handleFileUpload,
handleFileUpdate: handleFileUpdate,
// FormGenerator specific handlers
onDelete: handleDeleteSingle,
onDeleteMultiple: handleDeleteMultiple,
// Loading states
downloadingFiles,
deletingFiles,
previewingFiles,
editingFiles
editingFiles,
// Message overlay component
MessageOverlayComponent
};
};
};
@ -65,11 +100,7 @@ const createFilesHook = () => {
const filesColumns = [
{
key: 'file_name',
label: {
de: 'Dateiname',
en: 'Filename',
fr: 'Nom de fichier'
},
label: 'files.column.filename',
type: 'string',
width: 300,
minWidth: 200,
@ -80,11 +111,7 @@ const filesColumns = [
},
{
key: 'mime_type',
label: {
de: 'Dateityp',
en: 'File Type',
fr: 'Type de fichier'
},
label: 'files.column.mimetype',
type: 'string',
width: 200,
minWidth: 150,
@ -95,11 +122,7 @@ const filesColumns = [
},
{
key: 'size',
label: {
de: 'Dateigröße',
en: 'File Size',
fr: 'Taille du fichier'
},
label: 'files.column.filesize',
type: 'number',
width: 140,
minWidth: 120,
@ -109,11 +132,7 @@ const filesColumns = [
},
{
key: 'created_at',
label: {
de: 'Erstellungsdatum',
en: 'Creation Date',
fr: 'Date de création'
},
label: 'files.column.creationdate',
type: 'date',
width: 200,
minWidth: 180,
@ -123,41 +142,25 @@ const filesColumns = [
}
];
export const dateienPageData: GenericPageData = {
id: 'verwaltung-dateien',
path: 'verwaltung/dateien',
name: 'Dateien',
description: {
de: 'Dateiverwaltung und -organisation',
en: 'File management and organization',
fr: 'Gestion et organisation des fichiers'
},
export const filesPageData: GenericPageData = {
id: 'administration-files',
path: 'administration/files',
name: 'files.title',
description: 'files.title',
// Parent page
parentPath: 'verwaltung',
parentPath: 'administration',
// Visual
icon: FaRegFileAlt,
title: {
de: 'Dateien',
en: 'Files',
fr: 'Fichiers'
},
subtitle: {
de: 'Verwalten Sie Ihre Dateien und Dokumente',
en: 'Manage your files and documents',
fr: 'Gérez vos fichiers et documents'
},
title: 'files.title',
subtitle: 'files.title',
// Header buttons
headerButtons: [
{
id: 'upload-file',
label: {
de: 'Datei hochladen',
en: 'Upload File',
fr: 'Télécharger un fichier'
},
label: 'files.upload_button',
icon: FaUpload,
variant: 'primary',
// onClick will be handled by PageRenderer to render UploadButton
@ -176,11 +179,7 @@ export const dateienPageData: GenericPageData = {
actionButtons: [
{
type: 'view',
title: {
de: 'Datei vorschauen',
en: 'Preview file',
fr: 'Aperçu du fichier'
},
title: 'files.action.preview',
idField: 'id',
nameField: 'file_name',
typeField: 'mime_type',
@ -189,11 +188,7 @@ export const dateienPageData: GenericPageData = {
},
{
type: 'edit',
title: {
de: 'Datei bearbeiten',
en: 'Edit file',
fr: 'Modifier le fichier'
},
title: 'files.action.edit',
idField: 'id',
nameField: 'file_name',
typeField: 'mime_type',
@ -207,22 +202,14 @@ export const dateienPageData: GenericPageData = {
},
{
type: 'download',
title: {
de: 'Datei herunterladen',
en: 'Download file',
fr: 'Télécharger le fichier'
},
title: 'files.action.download',
idField: 'id',
operationName: 'handleDownload',
loadingStateName: 'downloadingFiles'
},
{
type: 'delete',
title: {
de: 'Datei löschen',
en: 'Delete file',
fr: 'Supprimer le fichier'
},
title: 'files.action.delete',
idField: 'id',
operationName: 'handleDelete',
loadingStateName: 'deletingFiles'
@ -234,7 +221,7 @@ export const dateienPageData: GenericPageData = {
resizable: true,
pagination: true,
pageSize: 10,
className: 'dateien-table'
className: 'files-table'
}
}
],
@ -247,17 +234,25 @@ export const dateienPageData: GenericPageData = {
preload: false,
moduleEnabled: true,
// Sidebar - will be shown as subpage under Verwaltung
// Sidebar - will be shown as subpage under Administration
showInSidebar: false,
// Drag and drop configuration
dragDropConfig: {
enabled: true,
accept: '*/*', // Accept all file types
multiple: true, // Allow multiple files
// overlayText and overlaySubtext will use default translations from DragDropOverlay
},
// Lifecycle hooks
onActivate: async () => {
if (import.meta.env.DEV) console.log('Dateien activated');
if (import.meta.env.DEV) console.log('Files activated');
},
onLoad: async () => {
if (import.meta.env.DEV) console.log('Dateien loaded - can initialize file lists here');
if (import.meta.env.DEV) console.log('Files loaded - can initialize file lists here');
},
onUnload: async () => {
if (import.meta.env.DEV) console.log('Dateien unloaded - cleanup file references');
if (import.meta.env.DEV) console.log('Files unloaded - cleanup file references');
}
};

View file

@ -1,26 +1,22 @@
// Export all page data
export { dashboardPageData } from './dashboard';
export { dateienPageData } from './dateien';
export { filesPageData } from './files';
export { teamBereichPageData } from './team-bereich';
export { examplePageData, exampleSubpage1Data, exampleSubpage2Data } from './example-page';
export { verwaltungPageData } from './verwaltung';
export { administrationPageData } from './administration';
// Import all page data
import { dashboardPageData } from './dashboard';
import { dateienPageData } from './dateien';
import { administrationPageData } from './administration';
import { filesPageData } from './files';
import { teamBereichPageData } from './team-bereich';
import { examplePageData, exampleSubpage1Data, exampleSubpage2Data } from './example-page';
import { verwaltungPageData } from './verwaltung';
// Array of all page data
export const allPageData = [
dashboardPageData,
verwaltungPageData,
dateienPageData,
administrationPageData,
filesPageData,
teamBereichPageData,
examplePageData,
exampleSubpage1Data,
exampleSubpage2Data
];
// Helper function to get page data by path

View file

@ -1,5 +1,6 @@
import React from 'react';
import { IconType } from 'react-icons';
import { DragDropConfig } from '../../components/ui/DragDropOverlay/DragDropOverlay';
// Generic privilege checker function type
export type PrivilegeChecker = () => boolean | Promise<boolean>;
@ -44,6 +45,11 @@ export interface GenericDataHook {
handleDownload?: (fileId: string, fileName: string) => Promise<boolean>; // For file download functionality
handleDelete?: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>; // For file delete functionality
handlePreview?: (fileId: string, fileName: string, mimeType?: string) => Promise<any>; // For file preview functionality
// FormGenerator specific handlers
onDelete?: (row: any) => Promise<void>; // For single item deletion
onDeleteMultiple?: (rows: any[]) => Promise<void>; // For multiple item deletion
// Message overlay component
MessageOverlayComponent?: () => React.ReactElement;
}
// Action button configuration
@ -84,10 +90,18 @@ export interface LanguageText {
}
// Utility function to resolve language text
export const resolveLanguageText = (text: string | LanguageText | undefined, language: 'de' | 'en' | 'fr' = 'de'): string => {
export const resolveLanguageText = (text: string | LanguageText | undefined, t?: (key: string, fallback?: string) => string): string => {
if (!text) return '';
if (typeof text === 'string') return text;
return text[language] || text.de || '';
if (typeof text === 'string') {
// Always use the translation function for strings (language keys)
if (t) {
return t(text);
}
return text;
}
// For LanguageText objects, we should convert them to language keys
// For now, fallback to the first available language
return text.de || text.en || text.fr || '';
};
// Generic page data interface
@ -135,6 +149,9 @@ export interface GenericPageData {
// Custom component override (optional)
customComponent?: React.ComponentType<any>;
// Drag and drop configuration
dragDropConfig?: DragDropConfig;
}
// Page data file structure
@ -159,6 +176,7 @@ export interface SidebarSubmenuItemData {
id: string;
name: string;
link: string;
icon?: IconType;
}
// Page instance for PageManager

View file

@ -1,5 +1,8 @@
import { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import api from '../api';
import { MessageOverlay } from '../components/ui';
import type { MessageMode } from '../components/ui';
import { useLanguage } from '../contexts/LanguageContext';
// File interfaces - exactly matching backend FileItem model
export interface FileInfo {
@ -29,51 +32,18 @@ export function useUserFiles() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Log hook state for debugging
console.log('🔄 useUserFiles hook', { filesCount: files.length, loading, isRefetching, hasError: !!error });
const fetchFiles = useCallback(async () => {
try {
setLoading(true);
setError(null);
console.log('🔍 Fetching files from API...');
console.log('🔍 Current auth authority:', localStorage.getItem('auth_authority'));
console.log('🔍 Has JWT token:', !!localStorage.getItem('auth_data'));
console.log('🚀 Making API request to /api/files/list...');
// Debug: Check what auth headers are being sent
const authData = localStorage.getItem('auth_data');
if (authData) {
try {
const tokenData = JSON.parse(authData);
console.log('🔍 JWT token being sent:', {
hasTokenAccess: !!tokenData.tokenAccess,
tokenAccessStart: tokenData.tokenAccess?.substring(0, 50) + '...',
tokenType: tokenData.tokenType
});
// Decode JWT payload to see what's inside
if (tokenData.tokenAccess) {
try {
const jwtParts = tokenData.tokenAccess.split('.');
if (jwtParts.length === 3) {
const payload = JSON.parse(atob(jwtParts[1]));
console.log('🔍 JWT payload contents:', {
sub: payload.sub,
userId: payload.userId,
authenticationAuthority: payload.authenticationAuthority,
exp: payload.exp,
expiredAt: new Date(payload.exp * 1000).toISOString(),
isExpired: payload.exp < Date.now() / 1000,
allKeys: Object.keys(payload)
});
}
} catch (decodeError) {
console.error('❌ Failed to decode JWT payload:', decodeError);
}
}
JSON.parse(authData);
} catch (e) {
console.error('❌ Failed to parse auth_data:', e);
}
@ -82,13 +52,9 @@ export function useUserFiles() {
const response = await api.get('/api/files/list');
const data = response.data;
console.log('✅ API request completed successfully!');
console.log('📥 Raw API response:', data);
// Ensure data is an array, handle null/undefined responses
const fileList = Array.isArray(data) ? data : [];
console.log(`📋 Processing ${fileList.length} files from API`);
// Filter out invalid files and map API response to our frontend model
const validFiles = fileList.filter((apiFile: any): boolean => {
@ -114,30 +80,9 @@ export function useUserFiles() {
apiFile.creationDate > 0
);
if (!isValid) {
console.warn('❌ Filtering out invalid file record:', {
id: apiFile?.id,
mandateId: apiFile?.mandateId,
fileName: apiFile?.fileName,
mimeType: apiFile?.mimeType,
fileHash: apiFile?.fileHash,
fileSize: apiFile?.fileSize,
creationDate: apiFile?.creationDate,
creationDateType: typeof apiFile?.creationDate,
fullObject: apiFile
});
} else {
console.log('✅ Valid file:', {
id: apiFile.id,
fileName: apiFile.fileName,
creationDate: apiFile.creationDate
});
}
return isValid;
});
console.log(`✨ Filtered to ${validFiles.length} valid files`);
if (validFiles.length !== fileList.length) {
console.warn(`⚠️ Filtered out ${fileList.length - validFiles.length} invalid files`);
}
@ -191,7 +136,6 @@ export function useUserFiles() {
};
});
console.log(`✅ Successfully processed ${mappedFiles.length} files from API`);
setFiles(mappedFiles);
} catch (error: any) {
console.error('❌ Error fetching files:', error);
@ -208,7 +152,6 @@ export function useUserFiles() {
// Provide informative placeholder when CORS blocks the request
if (error.message === 'Keine Antwort vom Server erhalten' || error.message === 'Network Error') {
console.log('📝 CORS blocking files API - providing informative placeholder');
const corsPlaceholderFile: UserFile = {
id: 'cors-info',
file_name: 'Files Service Temporarily Unavailable',
@ -253,12 +196,10 @@ export function useUserFiles() {
};
useEffect(() => {
console.log('🔄 useUserFiles useEffect triggered - fetching files on mount');
fetchFiles();
}, [fetchFiles]); // Depend on fetchFiles which is memoized with useCallback
const refetch = useCallback(async () => {
console.log('🔄 Refetching files...');
setIsRefetching(true);
try {
await fetchFiles();
@ -291,12 +232,18 @@ export function useFileOperations() {
const [previewingFiles, setPreviewingFiles] = useState<Set<string>>(new Set());
const [previewError, setPreviewError] = useState<string | null>(null);
// Warning state
const [showWarning, setShowWarning] = useState(false);
const [warningData, setWarningData] = useState<{ header: string; message: string; mode: MessageMode } | null>(null);
// Language context
const { t } = useLanguage();
const handleFileDownload = async (fileId: string, fileName: string) => {
setDownloadError(null);
setDownloadingFiles(prev => new Set(prev).add(fileId));
try {
console.log(`📥 Starting download for file: ${fileName} (ID: ${fileId})`);
// Try to get the file download
const response = await api.get(`/api/files/${fileId}/download`, {
@ -306,7 +253,6 @@ export function useFileOperations() {
}
});
const blob = response.data;
console.log(`✅ Download successful for: ${fileName}`, { size: blob.size, type: blob.type });
// Create a download link and trigger the download
const url = window.URL.createObjectURL(blob);
@ -350,12 +296,9 @@ export function useFileOperations() {
}
try {
console.log(`🗑️ Starting delete for file ID: ${fileId}`);
await api.delete(`/api/files/${fileId}`);
console.log(`✅ Delete successful for file ID: ${fileId}`);
// Add a small delay to ensure backend has time to process
await new Promise(resolve => setTimeout(resolve, 300));
return true;
@ -383,6 +326,56 @@ export function useFileOperations() {
}
};
const handleFileDeleteMultiple = async (fileIds: string[], onOptimisticDelete?: (fileIds: string[]) => void) => {
setDeleteError(null);
setDeletingFiles(prev => {
const newSet = new Set(prev);
fileIds.forEach(id => newSet.add(id));
return newSet;
});
// Optimistically remove from UI if callback provided
if (onOptimisticDelete) {
onOptimisticDelete(fileIds);
}
try {
// Delete files one by one since there's no bulk delete endpoint
const deletePromises = fileIds.map(fileId =>
api.delete(`/api/files/${fileId}`).catch(error => {
console.error(`❌ Delete failed for file ID ${fileId}:`, error);
// Return the error for this specific file
return { error, fileId };
})
);
const results = await Promise.all(deletePromises);
// Check if any deletions failed
const failures = results.filter(result => result && 'error' in result);
if (failures.length > 0) {
console.error(`${failures.length} out of ${fileIds.length} files failed to delete`);
// For now, we'll consider it successful if at least some files were deleted
// In a more robust implementation, you might want to handle partial failures differently
}
// Add a small delay to ensure backend has time to process
await new Promise(resolve => setTimeout(resolve, 300));
return true;
} catch (error: any) {
console.error(`❌ Bulk delete failed:`, error);
setDeleteError(error.message || 'Bulk delete failed');
return false;
} finally {
setDeletingFiles(prev => {
const newSet = new Set(prev);
fileIds.forEach(id => newSet.delete(id));
return newSet;
});
}
};
/**
* File upload function - backend bug has been fixed!
*
@ -396,12 +389,6 @@ export function useFileOperations() {
setUploadingFile(true);
try {
console.log('📤 Starting file upload...', {
fileName: file.name,
fileSize: file.size,
fileType: file.type,
workflowId: workflowId
});
// Validate file before upload
if (!file || !file.name || file.name.trim() === '') {
@ -421,22 +408,6 @@ export function useFileOperations() {
// FormData is now correctly configured for backend
console.log('📋 FormData prepared:', {
hasFile: formData.has('file'),
hasWorkflowId: formData.has('workflowId'),
workflowId: workflowId
});
// Log the actual file object being sent
console.log('📁 File object details:', {
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified,
constructor: file.constructor.name
});
console.log('🚀 Sending upload request...');
const response = await api.post('/api/files/upload', formData, {
headers: {
@ -446,6 +417,46 @@ export function useFileOperations() {
const fileData = response.data;
console.log('✅ Upload successful:', fileData);
console.log('📤 Upload response details:', {
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data,
config: {
url: response.config.url,
method: response.config.method,
headers: response.config.headers
}
});
// Check if the response indicates a duplicate file
if (fileData && fileData.isDuplicate && fileData.message) {
const fileName = fileData.originalFileName || file.name;
const messageTemplate = t('warning.duplicate_file.message');
const message = messageTemplate.replace('{fileName}', fileName);
// Close any existing warning first
if (showWarning) {
setShowWarning(false);
// Wait a moment before showing the new warning
setTimeout(() => {
setWarningData({
header: t('warning.duplicate_file.title'),
message: message,
mode: 'warning'
});
setShowWarning(true);
}, 600);
} else {
setWarningData({
header: t('warning.duplicate_file.title'),
message: message,
mode: 'warning'
});
setShowWarning(true);
}
}
return { success: true, fileData };
} catch (error: any) {
console.error('❌ Upload failed:', {
@ -482,12 +493,7 @@ export function useFileOperations() {
setEditingFiles(prev => new Set(prev).add(fileId));
try {
console.log(`✏️ Starting update for file ID: ${fileId}`, {
fileId,
updateData,
url: `/api/files/${fileId}`,
method: 'put'
});
// Use PUT request with complete file object
// Always use current timestamp for creationDate to avoid validation issues
@ -504,16 +510,6 @@ export function useFileOperations() {
creationDate: Math.floor(creationDate) // Ensure it's an integer
};
console.log('🔍 Sending complete file object with PUT:', {
completeFileObject,
originalFileData,
updateData,
creationDateType: typeof creationDate,
creationDateValue: creationDate,
creationDateFormatted: new Date(creationDate * 1000).toISOString(),
currentTime: new Date().toISOString(),
currentTimestamp: Math.floor(Date.now() / 1000)
});
const response = await api.put(`/api/files/${fileId}`, completeFileObject, {
headers: {
@ -522,7 +518,6 @@ export function useFileOperations() {
});
const updatedFile = response.data;
console.log(`✅ Update successful for file ID: ${fileId}`, updatedFile);
return { success: true, fileData: updatedFile };
} catch (error: any) {
console.error(`❌ Update failed for file ID ${fileId}:`, {
@ -565,11 +560,9 @@ export function useFileOperations() {
setPreviewingFiles(prev => new Set(prev).add(fileId));
try {
console.log(`👁️ Starting preview for file: ${fileName} (ID: ${fileId})`, { mimeType });
// For PDF files, try JSON response first (API returns base64-encoded PDF)
if (mimeType === 'application/pdf') {
console.log('📄 PDF file detected, trying JSON response with base64 content');
try {
const response = await api.get(`/api/files/${fileId}/preview`, {
@ -580,56 +573,31 @@ export function useFileOperations() {
});
const jsonResponse = response.data;
console.log('📄 PDF JSON response received:', {
hasContent: 'content' in jsonResponse,
hasMimeType: 'mimeType' in jsonResponse,
contentLength: jsonResponse.content?.length,
mimeType: jsonResponse.mimeType,
contentType: typeof jsonResponse.content,
contentStartsWith: jsonResponse.content?.substring(0, 10),
fullResponse: jsonResponse
});
// Check if response has base64-encoded PDF content
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
let content = jsonResponse.content;
const responseMimeType = jsonResponse.mimeType || 'application/pdf';
// The content field contains base64-encoded JSON, so decode it first
if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
console.log('📄 Content appears to be base64-encoded, decoding first...');
try {
const decodedJsonString = atob(content);
console.log('📄 Decoded JSON string:', {
length: decodedJsonString.length,
startsWith: decodedJsonString.substring(0, 20),
isJson: decodedJsonString.startsWith('{')
});
// Parse the decoded JSON string
const nestedJson = JSON.parse(decodedJsonString);
console.log('📄 Parsed nested JSON:', {
hasContent: 'content' in nestedJson,
hasDocumentCount: 'documentCount' in nestedJson,
keys: Object.keys(nestedJson)
});
if (nestedJson && typeof nestedJson === 'object' && 'content' in nestedJson) {
const innerContent = nestedJson.content;
const isBase64 = /^[A-Za-z0-9+/=]+$/.test(innerContent);
console.log('📄 Extracted inner content:', {
innerContentLength: innerContent?.length,
innerContentPreview: innerContent?.substring(0, 100) + '...',
isBase64: isBase64
});
if (isBase64) {
// It's base64-encoded PDF content
content = innerContent;
} else {
// It's plain text content, not a PDF
console.log('📄 Inner content is plain text, not PDF. This appears to be a text file with PDF extension.');
// Return the text content for the FilePreview to handle as text
return {
success: true,
@ -646,38 +614,20 @@ export function useFileOperations() {
}
}
console.log('📄 Processing base64 PDF content:', {
contentLength: content?.length,
mimeType: responseMimeType,
contentPreview: content?.substring(0, 100) + '...',
firstChars: content?.substring(0, 20),
lastChars: content?.substring(content.length - 20),
isBase64: /^[A-Za-z0-9+/=]+$/.test(content)
});
// Decode base64 content
let decodedContent;
try {
decodedContent = atob(content);
console.log('📄 Base64 decode successful:', {
originalLength: content.length,
decodedLength: decodedContent.length,
firstBytes: decodedContent.substring(0, 10),
firstBytesHex: Array.from(decodedContent.substring(0, 10)).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ')
});
// Verify it's actually a PDF
const isPDF = decodedContent.startsWith('%PDF');
console.log('📄 PDF header verification:', {
isPDF: isPDF,
firstBytes: decodedContent.substring(0, 4),
firstBytesHex: Array.from(decodedContent.substring(0, 4)).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' '),
first20Chars: decodedContent.substring(0, 20)
});
if (!isPDF) {
console.warn('⚠️ Decoded content does not appear to be a valid PDF');
console.log('📄 Full decoded content preview:', decodedContent.substring(0, 200));
}
} catch (decodeError) {
@ -695,21 +645,14 @@ export function useFileOperations() {
const blob = new Blob([uint8Array], { type: 'application/pdf' });
const url = window.URL.createObjectURL(blob);
console.log('🔗 Created PDF blob URL from base64:', url, {
blobSize: blob.size,
blobType: blob.type,
url: url,
uint8ArrayLength: uint8Array.length,
firstBytes: Array.from(uint8Array.slice(0, 4)).map(b => String.fromCharCode(b)).join(''),
firstBytesHex: Array.from(uint8Array.slice(0, 4)).map(b => b.toString(16).padStart(2, '0')).join(' ')
});
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
} else {
throw new Error('No content field in PDF response');
}
} catch (jsonError) {
console.log('📄 JSON PDF response failed, trying blob response...', jsonError);
// Fallback to blob response
const response = await api.get(`/api/files/${fileId}/preview`, {
@ -720,11 +663,7 @@ export function useFileOperations() {
});
const previewData = response.data;
console.log(`✅ PDF blob preview successful for: ${fileName}`, {
size: previewData.size,
type: previewData.type,
expectedType: 'application/pdf'
});
const url = window.URL.createObjectURL(previewData);
@ -734,7 +673,7 @@ export function useFileOperations() {
// For image files, try JSON response first (API returns base64-encoded images)
if (mimeType?.startsWith('image/')) {
console.log('🖼️ Image file detected, trying JSON response with base64 content');
try {
const response = await api.get(`/api/files/${fileId}/preview`, {
@ -745,12 +684,7 @@ export function useFileOperations() {
});
const jsonResponse = response.data;
console.log('🖼️ Image JSON response received:', {
hasContent: 'content' in jsonResponse,
hasMimeType: 'mimeType' in jsonResponse,
contentLength: jsonResponse.content?.length,
mimeType: jsonResponse.mimeType
});
// Check if response has base64-encoded image content
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
@ -759,34 +693,22 @@ export function useFileOperations() {
// The content field contains base64-encoded data, decode it first
if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
console.log('🖼️ Content appears to be base64-encoded, decoding first...');
try {
const decodedString = atob(content);
console.log('🖼️ Decoded string:', {
length: decodedString.length,
startsWith: decodedString.substring(0, 20),
isJson: decodedString.startsWith('{'),
isImage: decodedString.startsWith('\x89PNG') || decodedString.startsWith('\xFF\xD8\xFF')
});
// Check if it's JSON (nested structure) or direct image data
if (decodedString.startsWith('{')) {
// It's JSON, parse it
const nestedJson = JSON.parse(decodedString);
console.log('🖼️ Parsed nested JSON:', {
hasContent: 'content' in nestedJson,
keys: Object.keys(nestedJson)
});
if (nestedJson && typeof nestedJson === 'object' && 'content' in nestedJson) {
const innerContent = nestedJson.content;
const isBase64 = /^[A-Za-z0-9+/=]+$/.test(innerContent);
console.log('🖼️ Extracted inner content:', {
innerContentLength: innerContent?.length,
innerContentPreview: innerContent?.substring(0, 100) + '...',
isBase64: isBase64
});
if (isBase64) {
// It's base64-encoded image content
@ -797,7 +719,6 @@ export function useFileOperations() {
}
} else if (decodedString.startsWith('\x89PNG') || decodedString.startsWith('\xFF\xD8\xFF') || decodedString.startsWith('GIF8') || decodedString.startsWith('RIFF')) {
// It's direct image data, use it as is
console.log('🖼️ Direct image data detected, using as is');
content = btoa(decodedString); // Re-encode as base64 for processing
} else {
throw new Error('Decoded content is neither JSON nor image data');
@ -808,22 +729,13 @@ export function useFileOperations() {
}
}
console.log('🖼️ Processing base64 image content:', {
contentLength: content?.length,
mimeType: responseMimeType,
contentPreview: content?.substring(0, 100) + '...',
isBase64: /^[A-Za-z0-9+/=]+$/.test(content)
});
// Decode base64 content
let decodedContent;
try {
decodedContent = atob(content);
console.log('🖼️ Base64 decode successful:', {
originalLength: content.length,
decodedLength: decodedContent.length,
firstBytes: decodedContent.substring(0, 10)
});
// Verify it's actually an image by checking for common image headers
const isJPEG = decodedContent.startsWith('\xFF\xD8\xFF');
@ -831,13 +743,7 @@ export function useFileOperations() {
const isGIF = decodedContent.startsWith('GIF8');
const isWebP = decodedContent.startsWith('RIFF') && decodedContent.includes('WEBP');
console.log('🖼️ Image header verification:', {
isJPEG: isJPEG,
isPNG: isPNG,
isGIF: isGIF,
isWebP: isWebP,
firstBytes: decodedContent.substring(0, 4)
});
if (!isJPEG && !isPNG && !isGIF && !isWebP) {
console.warn('⚠️ Decoded content does not appear to be a valid image');
@ -858,19 +764,13 @@ export function useFileOperations() {
const blob = new Blob([uint8Array], { type: responseMimeType });
const url = window.URL.createObjectURL(blob);
console.log('🔗 Created image blob URL from base64:', url, {
blobSize: blob.size,
blobType: blob.type,
url: url,
uint8ArrayLength: uint8Array.length
});
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
} else {
throw new Error('No content field in image response');
}
} catch (jsonError) {
console.log('🖼️ JSON image response failed, trying blob response...', jsonError);
// Fallback to blob response
const response = await api.get(`/api/files/${fileId}/preview`, {
@ -881,11 +781,6 @@ export function useFileOperations() {
});
const previewData = response.data;
console.log(`✅ Image blob preview successful for: ${fileName}`, {
size: previewData.size,
type: previewData.type,
expectedType: mimeType
});
const url = window.URL.createObjectURL(previewData);
@ -903,35 +798,22 @@ export function useFileOperations() {
});
const jsonResponse = response.data;
console.log(`✅ JSON preview successful for: ${fileName}`, jsonResponse);
// Check if response has content field (structured response)
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
const content = jsonResponse.content;
const mimeType = jsonResponse.mimeType || 'text/plain';
console.log('📄 Structured JSON response detected:', {
hasContent: !!content,
mimeType: mimeType,
contentLength: content?.length,
contentPreview: content?.substring(0, 100) + '...'
});
// Check if content is base64 encoded (common pattern)
let decodedContent = content;
try {
// Try to decode as base64 if it looks like base64
if (content && typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
console.log('📄 Content appears to be base64 encoded, attempting decode...');
decodedContent = atob(content);
console.log('📄 Base64 decode successful:', {
originalLength: content.length,
decodedLength: decodedContent.length,
decodedPreview: decodedContent.substring(0, 200) + '...'
});
}
} catch (decodeError) {
console.log('📄 Base64 decode failed, using original content:', decodeError);
decodedContent = content;
}
@ -939,36 +821,22 @@ export function useFileOperations() {
const blob = new Blob([decodedContent], { type: mimeType });
const url = window.URL.createObjectURL(blob);
console.log('🔗 Created blob URL:', url);
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
} else if (jsonResponse && typeof jsonResponse === 'object' && 'result' in jsonResponse) {
// Handle base64 encoded content in 'result' field
console.log('📄 Base64 encoded content detected in result field');
try {
// Decode base64 content
const decodedContent = atob(jsonResponse.result);
const mimeType = jsonResponse.mimeType || 'application/json';
console.log('📄 Decoded content:', {
length: decodedContent.length,
preview: decodedContent.substring(0, 200) + '...',
mimeType: mimeType,
originalResult: jsonResponse.result.substring(0, 100) + '...',
decodedFirstChars: decodedContent.substring(0, 50)
});
// Create a blob from the decoded content
const blob = new Blob([decodedContent], { type: mimeType });
const url = window.URL.createObjectURL(blob);
console.log('🔗 Created blob URL for decoded content:', url);
console.log('🔍 Blob details:', {
size: blob.size,
type: blob.type,
url: url
});
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
} catch (decodeError) {
@ -980,7 +848,6 @@ export function useFileOperations() {
return { success: true, previewUrl: url, blob: blob, isJsonContent: true };
}
} else {
console.log('📄 Raw JSON response, treating as content');
// If it's not structured JSON, treat as raw content
const blob = new Blob([JSON.stringify(jsonResponse, null, 2)], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
@ -988,7 +855,6 @@ export function useFileOperations() {
return { success: true, previewUrl: url, blob: blob, isJsonContent: true };
}
} catch (jsonError) {
console.log('JSON preview failed, trying blob response...', jsonError);
// Fallback to blob response for binary files
const response = await api.get(`/api/files/${fileId}/preview`, {
@ -999,7 +865,6 @@ export function useFileOperations() {
});
const previewData = response.data;
console.log(`✅ Blob preview successful for: ${fileName}`, { size: previewData.size, type: previewData.type });
// Create a blob URL for preview
const url = window.URL.createObjectURL(previewData);
@ -1029,6 +894,15 @@ export function useFileOperations() {
}
};
// Function to close warning
const closeWarning = useCallback(() => {
setShowWarning(false);
// Delay clearing the data to allow exit animation to complete (matches CSS transition)
setTimeout(() => {
setWarningData(null);
}, 700);
}, []);
return {
downloadingFiles,
deletingFiles,
@ -1041,9 +915,20 @@ export function useFileOperations() {
previewError,
handleFileDownload,
handleFileDelete,
handleFileDeleteMultiple,
handleFileUpload,
handleFileUpdate,
handleFilePreview,
isLoading
isLoading,
// Message overlay component
MessageOverlayComponent: () => React.createElement(MessageOverlay, {
header: warningData?.header || '',
message: warningData?.message || '',
isVisible: showWarning,
mode: warningData?.mode || 'info',
onClose: closeWarning,
autoClose: true,
autoCloseDelay: 5000
})
};
}

View file

@ -16,6 +16,3 @@ html, body {
margin: 0;
padding: 0;
}
/* Import global button styles */
@import './styles/buttons.css';

View file

@ -497,11 +497,10 @@ export default {
'users.add.create': 'Benutzer erstellen',
'users.delete.title': 'Benutzer löschen',
'users.delete.message': 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
'users.delete.confirm': 'Löschen',
'users.delete.confirm': 'Sind Sie sicher, dass Sie "{name}" löschen möchten?',
'users.delete.warning': 'Diese Aktion kann nicht rückgängig gemacht werden.',
'users.action.edit': 'Bearbeiten',
'users.action.delete': 'Löschen',
'users.delete.confirm': 'Sind Sie sicher, dass Sie "{name}" löschen möchten?',
'users.delete.confirmMultiple': 'Sind Sie sicher, dass Sie {count} Benutzer löschen möchten?',
'users.error.loading': 'Fehler beim Laden der Benutzer:',
@ -561,8 +560,6 @@ export default {
'speech.info.about_link': 'Mehr erfahren',
'speech.signup.button': 'Verbinden',
'speech.signup.title': 'Mandat für Sprach Integration erstellen',
'speech.signup.subtitle': 'Erstellen Sie Ihr Mandat für die Spitch.ai Integration',
'speech.signup.back': 'Zurück zur Sprach Integration',
'speech.signup.submit': 'Mandat erstellen',
'speech.signup.cancel': 'Abbrechen',
@ -665,4 +662,34 @@ export default {
'speech.settings.reset_success': 'Einstellungen wurden erfolgreich zurückgesetzt.',
'speech.settings.no_data': 'Keine Sprach-Integrations-Daten gefunden. Bitte melden Sie sich zuerst an, um auf die Einstellungen zuzugreifen.',
'speech.settings.sign_up_now': 'Jetzt anmelden',
// Message Overlay Types
'message.success.title': 'Erfolgreich',
'message.success.upload': 'Datei erfolgreich hochgeladen!',
'message.info.title': 'Information',
'message.info.processing': 'Ihre Anfrage wird verarbeitet...',
'message.error.title': 'Fehler',
'message.error.upload_failed': 'Upload fehlgeschlagen. Bitte versuchen Sie es erneut.',
// Warning Messages
'warning.duplicate_file.title': 'Datei bereits vorhanden',
'warning.duplicate_file.message': 'Die Datei "{fileName}" existiert bereits mit identischem Inhalt. Die vorhandene Datei wird wiederverwendet.',
// Administration
'administration.title': 'Verwaltung',
'administration.description': 'Verwaltungs- und Management-Tools',
'administration.subtitle': 'Verwaltungs- und Management-Tools',
'administration.intro.description': 'Dieser Bereich enthält alle Verwaltungs- und Management-Tools für Ihren Arbeitsbereich.',
'administration.features.title': 'Verfügbare Tools',
'administration.features.description': 'Management-Tools umfassen:',
'administration.features.file_management': 'Dateiverwaltung - Dokumente hochladen und organisieren',
'administration.features.user_management': 'Benutzerverwaltung - Teammitglieder und Berechtigungen verwalten',
'administration.features.system_settings': 'Systemeinstellungen - Arbeitsbereich-Einstellungen konfigurieren',
'administration.features.data_management': 'Datenverwaltung - Datenimporte und -exporte verwalten',
// Drag and Drop
'dragdrop.overlay.default_text': 'Dateien hier ablegen',
'dragdrop.overlay.default_subtext': 'Sie können auch auf den Upload-Button klicken',
'dragdrop.overlay.processing': 'Dateien werden verarbeitet...',
'dragdrop.overlay.error': 'Fehler beim Verarbeiten der Dateien',
};

View file

@ -497,11 +497,10 @@ export default {
'users.add.create': 'Create User',
'users.delete.title': 'Delete User',
'users.delete.message': 'Are you sure you want to delete this user?',
'users.delete.confirm': 'Delete',
'users.delete.confirm': 'Are you sure you want to delete "{name}"?',
'users.delete.warning': 'This action cannot be undone.',
'users.action.edit': 'Edit',
'users.action.delete': 'Delete',
'users.delete.confirm': 'Are you sure you want to delete "{name}"?',
'users.delete.confirmMultiple': 'Are you sure you want to delete {count} users?',
'users.error.loading': 'Error loading users:',
@ -561,8 +560,6 @@ export default {
'speech.info.about_link': 'Learn more',
'speech.signup.button': 'Connect',
'speech.signup.title': 'Create Mandate for Speech Integration',
'speech.signup.subtitle': 'Create your mandate for Spitch.ai integration',
'speech.signup.back': 'Back to Speech Integration',
'speech.signup.submit': 'Create Mandate',
'speech.signup.cancel': 'Cancel',
@ -665,4 +662,34 @@ export default {
'speech.settings.reset_success': 'Settings have been reset successfully.',
'speech.settings.no_data': 'No speech integration data found. Please sign up first to access settings.',
'speech.settings.sign_up_now': 'Sign Up Now',
// Message Overlay Types
'message.success.title': 'Success',
'message.success.upload': 'File uploaded successfully!',
'message.info.title': 'Information',
'message.info.processing': 'Processing your request...',
'message.error.title': 'Error',
'message.error.upload_failed': 'Upload failed. Please try again.',
// Warning Messages
'warning.duplicate_file.title': 'File Already Exists',
'warning.duplicate_file.message': 'The file "{fileName}" already exists with identical content. The existing file will be reused.',
// Administration
'administration.title': 'Administration',
'administration.description': 'Administration and management tools',
'administration.subtitle': 'Administration and management tools',
'administration.intro.description': 'This section contains all administration and management tools for your workspace.',
'administration.features.title': 'Available Tools',
'administration.features.description': 'Management tools include:',
'administration.features.file_management': 'File Management - Upload and organize documents',
'administration.features.user_management': 'User Management - Manage team members and permissions',
'administration.features.system_settings': 'System Settings - Configure workspace settings',
'administration.features.data_management': 'Data Management - Handle data imports and exports',
// Drag and Drop
'dragdrop.overlay.default_text': 'Drop files here',
'dragdrop.overlay.default_subtext': 'You can also click the upload button',
'dragdrop.overlay.processing': 'Processing files...',
'dragdrop.overlay.error': 'Error processing files',
};

View file

@ -497,11 +497,10 @@ export default {
'users.add.create': 'Créer l\'utilisateur',
'users.delete.title': 'Supprimer l\'utilisateur',
'users.delete.message': 'Êtes-vous sûr de vouloir supprimer cet utilisateur ?',
'users.delete.confirm': 'Supprimer',
'users.delete.confirm': 'Êtes-vous sûr de vouloir supprimer "{name}" ?',
'users.delete.warning': 'Cette action ne peut pas être annulée.',
'users.action.edit': 'Modifier',
'users.action.delete': 'Supprimer',
'users.delete.confirm': 'Êtes-vous sûr de vouloir supprimer "{name}" ?',
'users.delete.confirmMultiple': 'Êtes-vous sûr de vouloir supprimer {count} utilisateurs ?',
'users.error.loading': 'Erreur lors du chargement des utilisateurs:',
@ -561,8 +560,6 @@ export default {
'speech.info.about_link': 'En savoir plus',
'speech.signup.button': 'Connecter',
'speech.signup.title': 'Créer un Mandat pour l\'Intégration Vocale',
'speech.signup.subtitle': 'Créez votre mandat pour l\'intégration Spitch.ai',
'speech.signup.back': 'Retour à l\'Intégration Vocale',
'speech.signup.submit': 'Créer le Mandat',
'speech.signup.cancel': 'Annuler',
@ -665,4 +662,34 @@ export default {
'speech.settings.reset_success': 'Les paramètres ont été réinitialisés avec succès.',
'speech.settings.no_data': 'Aucune donnée d\'intégration vocale trouvée. Veuillez d\'abord vous inscrire pour accéder aux paramètres.',
'speech.settings.sign_up_now': 'S\'inscrire Maintenant',
// Message Overlay Types
'message.success.title': 'Succès',
'message.success.upload': 'Fichier téléchargé avec succès !',
'message.info.title': 'Information',
'message.info.processing': 'Traitement de votre demande...',
'message.error.title': 'Erreur',
'message.error.upload_failed': 'Échec du téléchargement. Veuillez réessayer.',
// Warning Messages
'warning.duplicate_file.title': 'Fichier Déjà Existant',
'warning.duplicate_file.message': 'Le fichier "{fileName}" existe déjà avec un contenu identique. Le fichier existant sera réutilisé.',
// Administration
'administration.title': 'Administration',
'administration.description': 'Outils d\'administration et de gestion',
'administration.subtitle': 'Outils d\'administration et de gestion',
'administration.intro.description': 'Cette section contient tous les outils d\'administration et de gestion pour votre espace de travail.',
'administration.features.title': 'Outils Disponibles',
'administration.features.description': 'Les outils de gestion incluent:',
'administration.features.file_management': 'Gestion des Fichiers - Télécharger et organiser les documents',
'administration.features.user_management': 'Gestion des Utilisateurs - Gérer les membres de l\'équipe et les permissions',
'administration.features.system_settings': 'Paramètres Système - Configurer les paramètres de l\'espace de travail',
'administration.features.data_management': 'Gestion des Données - Gérer les imports et exports de données',
// Drag and Drop
'dragdrop.overlay.default_text': 'Déposer les fichiers ici',
'dragdrop.overlay.default_subtext': 'Vous pouvez aussi cliquer sur le bouton de téléchargement',
'dragdrop.overlay.processing': 'Traitement des fichiers...',
'dragdrop.overlay.error': 'Erreur lors du traitement des fichiers',
};

View file

@ -2,6 +2,11 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
// Import all global styles
import './index.css'
import './styles/themes/light.css'
import './styles/buttons.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />

View file

@ -3,10 +3,10 @@ import { useState } from 'react';
import { IoMdAdd } from 'react-icons/io';
import { PromptsTable } from '../../components/Prompts';
import { useLanguage } from '../../contexts/LanguageContext';
import { Popup } from '../../components/Popup/Popup';
import { EditForm } from '../../components/Popup/EditForm';
import { Popup } from '../../components/ui/Popup/Popup';
import { EditForm } from '../../components/ui/Popup/EditForm';
import { usePrompts, usePromptOperations } from '../../hooks/usePrompts';
import type { EditFieldConfig } from '../../components/Popup/EditForm';
import type { EditFieldConfig } from '../../components/ui/Popup/EditForm';
import sharedStyles from '../../core/PageManager/pages.module.css';
function Prompts() {

View file

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

View file

@ -27,7 +27,7 @@
--button-warning-text: #212529;
/* Button Sizes */
--button-sm-padding: 6px 12px;
--button-sm-padding: 8px 12px;
--button-sm-font-size: 12px;
--button-sm-icon-size: 14px;
@ -202,6 +202,16 @@
display: none;
}
/* Spinner Icon for Upload Button */
.spinnerIcon {
width: 1em;
height: 1em;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* Responsive Design */
@media (max-width: 768px) {
.buttonSm {

View file

@ -1,64 +0,0 @@
/* Light Theme CSS Variables */
:root {
--color-bg: #F8F9FA; /* war vorher surface */
--color-surface: #EFEDE5; /* war vorher bg */
--color-text: #3A3A3A;
--color-primary: #C7C5B2;
--color-primary-hover: #D9D7C6;
--color-primary-disabled: #E3E2D8;
--color-secondary: #F25843;
--color-secondary-hover: #FF6A55;
--color-secondary-disabled: #F5B0A4;
--color-red: #dc3545;
--color-red-hover: #f5c6cb;
--color-red-disabled: #f8d7da;
--color-secondary-red: #B94A55;
--color-secondary-red-hover: #D46872;
--color-secondary-red-disabled: #E8B7BA;
--color-gray: #6F7373;
--color-gray-hover: #565A5A;
--color-gray-disabled: #B7BBBA;
--color-medium-gray: #E0DDD3;
--color-medium-gray-hover: #D1CEC5;
--color-medium-gray-disabled: #E0DDD380;
--color-highlight-gray: #F5F3ED;
--color-highlight-gray-hover: #E6E3DC;
--color-highlight-gray-disabled: #F5F3ED80;
--font-family: "DM Sans", sans-serif;
}
/* Dark Theme Overrides */
.dark-theme {
--color-bg: #181818; /* war vorher surface */
--color-surface: #1E1D1A; /* war vorher bg */
--color-text: #E5E7EB;
--color-primary: #C7C5B2;
--color-primary-hover: #E0DECC;
--color-primary-disabled: #59584F;
--color-secondary: #F25843;
--color-secondary-hover: #FF715C;
--color-secondary-disabled: #6E3E36;
--color-red: #dc3545;
--color-red-hover: #f5c6cb;
--color-red-disabled: #f8d7da;
--color-secondary-red: #D65D6A;
--color-secondary-red-hover: #E17683;
--color-secondary-red-disabled: #70363C;
--color-gray: #181818;
--color-gray-hover: #2E2E2E;
--color-gray-disabled: #505050;
}

View file

@ -24,5 +24,8 @@
--color-gray-disabled: #505357;
--font-family: "Trebuchet MS", sans-serif;
--object-radius-large: 30px;
--object-radius-medium: 15px;
--object-radius-small: 5px;
}

View file

@ -32,6 +32,9 @@
--color-highlight-gray-disabled: #F5F3ED80;
--font-family: "DM Sans", sans-serif;
--object-radius-large: 30px;
--object-radius-medium: 15px;
--object-radius-small: 5px;
}
/* Dark theme overrides */