finished files page
This commit is contained in:
parent
b238ab87a5
commit
6988984cd7
59 changed files with 1263 additions and 902 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -63,3 +63,10 @@
|
|||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.submenuIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #181818;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface SidebarSubmenuItemData {
|
|||
id: string;
|
||||
name: string;
|
||||
link?: string;
|
||||
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||
}
|
||||
|
||||
// Sidebar state interface
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -26,4 +26,3 @@ export interface UploadButtonProps extends BaseButtonProps {
|
|||
icon?: IconType;
|
||||
iconPosition?: 'left' | 'right';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
2
src/components/ui/Button/UploadButton/index.ts
Normal file
2
src/components/ui/Button/UploadButton/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as UploadButton } from './UploadButton';
|
||||
export type { UploadButtonProps } from '../ButtonTypes';
|
||||
|
|
@ -1,3 +1,2 @@
|
|||
export { default as Button } from './Button';
|
||||
export * from './ButtonTypes';
|
||||
|
||||
|
|
|
|||
165
src/components/ui/DragDropOverlay/DragDropOverlay.module.css
Normal file
165
src/components/ui/DragDropOverlay/DragDropOverlay.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
195
src/components/ui/DragDropOverlay/DragDropOverlay.tsx
Normal file
195
src/components/ui/DragDropOverlay/DragDropOverlay.tsx
Normal 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;
|
||||
3
src/components/ui/DragDropOverlay/index.ts
Normal file
3
src/components/ui/DragDropOverlay/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { DragDropOverlay } from './DragDropOverlay';
|
||||
export type { DragDropConfig } from './DragDropOverlay';
|
||||
export { DragDropOverlay as default } from './DragDropOverlay';
|
||||
175
src/components/ui/MessageOverlay/MessageOverlay.module.css
Normal file
175
src/components/ui/MessageOverlay/MessageOverlay.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
131
src/components/ui/MessageOverlay/MessageOverlay.tsx
Normal file
131
src/components/ui/MessageOverlay/MessageOverlay.tsx
Normal 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;
|
||||
2
src/components/ui/MessageOverlay/index.ts
Normal file
2
src/components/ui/MessageOverlay/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './MessageOverlay';
|
||||
export type { MessageMode } from './MessageOverlay';
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export { default as UploadButton } from './UploadButton';
|
||||
export type { UploadButtonProps } from '../Button/ButtonTypes';
|
||||
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
};
|
||||
}
|
||||
|
|
@ -16,6 +16,3 @@ html, body {
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Import global button styles */
|
||||
@import './styles/buttons.css';
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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 */
|
||||
Loading…
Reference in a new issue