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 { ProtectedRoute } from './auth/ProtectedRoute';
|
||||||
import { LanguageProvider } from './contexts/LanguageContext';
|
import { LanguageProvider } from './contexts/LanguageContext';
|
||||||
import Home from './pages/Home/Home';
|
import Home from './pages/Home/Home';
|
||||||
// Import the global light theme CSS variables as default
|
|
||||||
import './assets/styles/light.css';
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// Load saved theme preference and set app name on app mount
|
// 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 styles from './ConnectionEditModal.module.css';
|
||||||
import { ConnectionEditModalProps } from './connectionsInterfaces';
|
import { ConnectionEditModalProps } from './connectionsInterfaces';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ColumnConfig } from '../FormGenerator';
|
import { ColumnConfig } from '../FormGenerator';
|
||||||
import { EditFieldConfig } from '../Popup';
|
import { EditFieldConfig } from '../ui/Popup';
|
||||||
|
|
||||||
// Import React for component types
|
// Import React for component types
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { MdModeEdit } from 'react-icons/md';
|
||||||
import { useConnections, useOAuthConnect, useDisconnect } from '../../hooks/useConnections';
|
import { useConnections, useOAuthConnect, useDisconnect } from '../../hooks/useConnections';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
import { ColumnConfig } from '../FormGenerator';
|
import { ColumnConfig } from '../FormGenerator';
|
||||||
import { EditFieldConfig } from '../Popup';
|
import { EditFieldConfig } from '../ui/Popup';
|
||||||
import {
|
import {
|
||||||
Connection,
|
Connection,
|
||||||
CreateConnectionData,
|
CreateConnectionData,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import { IoIosDownload, IoIosCopy } from 'react-icons/io';
|
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 { useLanguage } from '../../contexts/LanguageContext';
|
||||||
import { useFileOperations } from '../../hooks/useFiles';
|
import { useFileOperations } from '../../hooks/useFiles';
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,11 @@
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Delete button loading state - no animation for user-friendly experience */
|
||||||
|
.actionButton.delete.loading .actionIcon {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,9 @@ export function DeleteActionButton<T = any>({
|
||||||
|
|
||||||
// Use loading state from hookData if available
|
// Use loading state from hookData if available
|
||||||
const isDeletingFromHook = loadingState?.has((row as any)[idField]) || false;
|
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) {
|
if (isConfirming) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -180,9 +183,9 @@ export function DeleteActionButton<T = any>({
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={handleDeleteClick}
|
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}
|
title={finalTitle}
|
||||||
disabled={isDisabled || loading || isDeleting || isDeletingFromHook}
|
disabled={isDisabled || loading || isDeleting || isAnyDeletionInProgress}
|
||||||
>
|
>
|
||||||
<span className={styles.actionIcon}>
|
<span className={styles.actionIcon}>
|
||||||
<IoIosTrash />
|
<IoIosTrash />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { MdModeEdit } from 'react-icons/md';
|
import { MdModeEdit } from 'react-icons/md';
|
||||||
import { useLanguage } from '../../../../contexts/LanguageContext';
|
import { useLanguage } from '../../../../contexts/LanguageContext';
|
||||||
import { Popup, EditForm } from '../../../Popup';
|
import { Popup, EditForm } from '../../../ui/Popup';
|
||||||
import styles from '../ActionButton.module.css';
|
import styles from '../ActionButton.module.css';
|
||||||
|
|
||||||
export interface EditActionButtonProps<T = any> {
|
export interface EditActionButtonProps<T = any> {
|
||||||
|
|
@ -72,13 +72,7 @@ export function EditActionButton<T = any>({
|
||||||
const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false;
|
const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false;
|
||||||
const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined;
|
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
|
// Validate that hookData is provided
|
||||||
if (!hookData) {
|
if (!hookData) {
|
||||||
|
|
@ -91,15 +85,7 @@ export function EditActionButton<T = any>({
|
||||||
setInternalLoading(true);
|
setInternalLoading(true);
|
||||||
try {
|
try {
|
||||||
// Debug logging to see what data we're working with
|
// 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
|
// Call the onEdit callback if provided
|
||||||
if (onEdit) {
|
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
|
// Validate required operation exists
|
||||||
if (!hookData[operationName]) {
|
if (!hookData[operationName]) {
|
||||||
|
|
@ -195,15 +170,7 @@ export function EditActionButton<T = any>({
|
||||||
// Determine the final button title (tooltip)
|
// Determine the final button title (tooltip)
|
||||||
const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,6 @@ export function ViewActionButton<T = any>({
|
||||||
if (!isDisabled && !loading && !isViewing && !internalLoading) {
|
if (!isDisabled && !loading && !isViewing && !internalLoading) {
|
||||||
setInternalLoading(true);
|
setInternalLoading(true);
|
||||||
try {
|
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
|
// Call the onView callback if provided
|
||||||
if (onView) {
|
if (onView) {
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ import {
|
||||||
DownloadActionButton,
|
DownloadActionButton,
|
||||||
ViewActionButton
|
ViewActionButton
|
||||||
} from './ActionButtons';
|
} from './ActionButtons';
|
||||||
|
import { Button } from '../ui/Button';
|
||||||
|
|
||||||
import { IoIosRefresh } from "react-icons/io";
|
import { IoIosRefresh } from "react-icons/io";
|
||||||
|
import { FaTrash } from "react-icons/fa";
|
||||||
|
|
||||||
// Types for the FormGenerator
|
// Types for the FormGenerator
|
||||||
export interface ColumnConfig {
|
export interface ColumnConfig {
|
||||||
|
|
@ -447,28 +449,28 @@ export function FormGenerator<T extends Record<string, any>>({
|
||||||
{selectable && selectedRows.size > 0 && (
|
{selectable && selectedRows.size > 0 && (
|
||||||
<div className={styles.deleteControlsIntegrated}>
|
<div className={styles.deleteControlsIntegrated}>
|
||||||
{selectedRows.size === 1 && onDelete && (
|
{selectedRows.size === 1 && onDelete && (
|
||||||
<button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const selectedIndex = Array.from(selectedRows)[0];
|
const selectedIndex = Array.from(selectedRows)[0];
|
||||||
const selectedRow = paginatedData[selectedIndex];
|
const selectedRow = paginatedData[selectedIndex];
|
||||||
handleDeleteSingle(selectedRow, selectedIndex);
|
handleDeleteSingle(selectedRow, selectedIndex);
|
||||||
}}
|
}}
|
||||||
className={styles.deleteButton}
|
variant="primary"
|
||||||
title={t('formgen.delete.single', 'Delete selected item')}
|
size="sm"
|
||||||
|
icon={FaTrash}
|
||||||
>
|
>
|
||||||
<span className={styles.deleteIcon}>✕</span>
|
|
||||||
{t('formgen.delete.single', 'Delete')}
|
{t('formgen.delete.single', 'Delete')}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{selectedRows.size > 1 && onDeleteMultiple && (
|
{selectedRows.size > 1 && onDeleteMultiple && (
|
||||||
<button
|
<Button
|
||||||
onClick={handleDeleteMultiple}
|
onClick={handleDeleteMultiple}
|
||||||
className={styles.deleteAllButton}
|
variant="primary"
|
||||||
title={t('formgen.delete.multiple', `Delete ${selectedRows.size} selected items`)}
|
size="sm"
|
||||||
|
icon={FaTrash}
|
||||||
>
|
>
|
||||||
<span className={styles.deleteIcon}>✕</span>
|
|
||||||
{t('formgen.delete.multiple', `Delete ${selectedRows.size} selected items`).replace('{count}', selectedRows.size.toString())}
|
{t('formgen.delete.multiple', `Delete ${selectedRows.size} selected items`).replace('{count}', selectedRows.size.toString())}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -768,20 +770,9 @@ export function FormGenerator<T extends Record<string, any>>({
|
||||||
? actionButton.title(row)
|
? actionButton.title(row)
|
||||||
: actionButton.title;
|
: actionButton.title;
|
||||||
const disabledResult = actionButton.disabled ? actionButton.disabled(row) : false;
|
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 isLoading = actionButton.loading ? actionButton.loading(row) : false;
|
||||||
const isProcessing = actionButton.isProcessing ? actionButton.isProcessing(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 = {
|
const baseProps = {
|
||||||
row,
|
row,
|
||||||
|
|
@ -796,12 +787,6 @@ export function FormGenerator<T extends Record<string, any>>({
|
||||||
operationName: actionButton.operationName,
|
operationName: actionButton.operationName,
|
||||||
loadingStateName: actionButton.loadingStateName
|
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) {
|
switch (actionButton.type) {
|
||||||
case 'edit':
|
case 'edit':
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { FormGenerator } from '../FormGenerator/FormGenerator';
|
import { FormGenerator } from '../FormGenerator/FormGenerator';
|
||||||
import { Popup } from '../Popup/Popup';
|
import { Popup } from '../ui/Popup/Popup';
|
||||||
import { EditForm, EditFieldConfig } from '../Popup/EditForm';
|
import { EditForm, EditFieldConfig } from '../ui/Popup/EditForm';
|
||||||
import { useMitgliederLogic } from './mitgliederLogic';
|
import { useMitgliederLogic } from './mitgliederLogic';
|
||||||
import { MitgliederTableProps } from './mitgliederTypes';
|
import { MitgliederTableProps } from './mitgliederTypes';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
|
||||||
|
|
||||||
import { FormGenerator } from '../FormGenerator/FormGenerator';
|
import { FormGenerator } from '../FormGenerator/FormGenerator';
|
||||||
import { Popup } from '../Popup/Popup';
|
import { Popup } from '../ui/Popup/Popup';
|
||||||
import { EditForm } from '../Popup/EditForm';
|
import { EditForm } from '../ui/Popup/EditForm';
|
||||||
import { usePromptsLogic } from './promptsLogic';
|
import { usePromptsLogic } from './promptsLogic';
|
||||||
import { PromptsTableProps, Prompt } from './promptsTypes';
|
import { PromptsTableProps, Prompt } from './promptsTypes';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { MdModeEdit } from 'react-icons/md';
|
||||||
|
|
||||||
import { usePrompts, usePromptOperations, Prompt } from '../../hooks/usePrompts';
|
import { usePrompts, usePromptOperations, Prompt } from '../../hooks/usePrompts';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
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
|
// Helper function to determine if a prompt can be deleted
|
||||||
const isPromptDeletable = (prompt: Prompt): boolean => {
|
const isPromptDeletable = (prompt: Prompt): boolean => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
.sidebarContainer {
|
.sidebarContainer {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
/*background-image: url('../../../assets/styles/bg.jpg');
|
/*background-image: url('../../../styles/assets/bg.jpg');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
|
||||||
|
|
@ -63,3 +63,10 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
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);
|
return () => window.removeEventListener('resize', checkOverflow);
|
||||||
}, [subitem.name]);
|
}, [subitem.name]);
|
||||||
|
|
||||||
|
const SubIcon = subitem.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={subitem.id}>
|
<li key={subitem.id}>
|
||||||
<Link
|
<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' }}>
|
||||||
{subitem.name}
|
{SubIcon && <SubIcon className={styles.submenuIcon} />}
|
||||||
|
<span style={{ marginLeft: SubIcon ? '8px' : '0' }}>
|
||||||
|
{subitem.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export interface SidebarSubmenuItemData {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
|
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sidebar state interface
|
// Sidebar state interface
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
|
|
||||||
import { FormGenerator } from '../FormGenerator/FormGenerator';
|
import { FormGenerator } from '../FormGenerator/FormGenerator';
|
||||||
import { Popup, EditForm } from '../Popup';
|
import { Popup, EditForm } from '../ui/Popup';
|
||||||
import { useWorkflowsLogic } from './workflowsLogic';
|
import { useWorkflowsLogic } from './workflowsLogic';
|
||||||
import { WorkflowsTableProps } from './workflowsTypes';
|
import { WorkflowsTableProps } from './workflowsTypes';
|
||||||
import styles from './WorkflowsTable.module.css';
|
import styles from './WorkflowsTable.module.css';
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { MdModeEdit } from 'react-icons/md';
|
||||||
import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
|
import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
|
||||||
import { useApiRequest } from '../../hooks/useApi';
|
import { useApiRequest } from '../../hooks/useApi';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
import type { EditFieldConfig } from '../Popup/EditForm';
|
import type { EditFieldConfig } from '../ui/Popup/EditForm';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
WorkflowsLogicReturn,
|
WorkflowsLogicReturn,
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,3 @@ export interface UploadButtonProps extends BaseButtonProps {
|
||||||
icon?: IconType;
|
icon?: IconType;
|
||||||
iconPosition?: 'left' | 'right';
|
iconPosition?: 'left' | 'right';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { UploadButtonProps } from '../Button/ButtonTypes';
|
import { UploadButtonProps } from '../ButtonTypes';
|
||||||
import Button from '../Button/Button';
|
import Button from '../Button';
|
||||||
|
|
||||||
const UploadButton: React.FC<UploadButtonProps> = ({
|
const UploadButton: React.FC<UploadButtonProps> = ({
|
||||||
onUpload,
|
onUpload,
|
||||||
|
|
@ -54,7 +54,6 @@ const UploadButton: React.FC<UploadButtonProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDisabled = disabled || loading || isUploading;
|
const isDisabled = disabled || loading || isUploading;
|
||||||
const isButtonLoading = loading || isUploading;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -63,12 +62,15 @@ const UploadButton: React.FC<UploadButtonProps> = ({
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
loading={isButtonLoading}
|
loading={false} // We handle the loading state manually
|
||||||
className={`uploadButton ${className}`}
|
className={`uploadButton ${className}`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
icon={icon}
|
icon={isUploading ? undefined : icon} // Hide original icon when uploading
|
||||||
iconPosition={iconPosition}
|
iconPosition={iconPosition}
|
||||||
>
|
>
|
||||||
|
{isUploading && (
|
||||||
|
<div className="spinnerIcon" style={{ marginRight: '8px' }} />
|
||||||
|
)}
|
||||||
{children || (isUploading ? 'Uploading...' : 'Upload File')}
|
{children || (isUploading ? 'Uploading...' : 'Upload File')}
|
||||||
</Button>
|
</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 { default as Button } from './Button';
|
||||||
export * from './ButtonTypes';
|
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 './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 { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { getPageDataByPath, GenericPageData, PageInstance } from './data';
|
import { getPageDataByPath, GenericPageData, PageInstance } from './data';
|
||||||
import PageRenderer from './PageRenderer';
|
import PageRenderer from './PageRenderer';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
|
||||||
|
|
||||||
interface PageManagerProps {
|
interface PageManagerProps {
|
||||||
loadingComponent: React.ComponentType;
|
loadingComponent: React.ComponentType;
|
||||||
|
|
@ -16,7 +15,6 @@ const PageManager: React.FC<PageManagerProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [pageInstances, setPageInstances] = useState<Map<string, PageInstance>>(new Map());
|
const [pageInstances, setPageInstances] = useState<Map<string, PageInstance>>(new Map());
|
||||||
const { currentLanguage } = useLanguage();
|
|
||||||
|
|
||||||
// Get current path
|
// Get current path
|
||||||
const getCurrentPath = () => {
|
const getCurrentPath = () => {
|
||||||
|
|
@ -100,7 +98,6 @@ const PageManager: React.FC<PageManagerProps> = ({
|
||||||
) : (
|
) : (
|
||||||
<PageRenderer
|
<PageRenderer
|
||||||
pageData={pageData}
|
pageData={pageData}
|
||||||
language={currentLanguage}
|
|
||||||
onButtonClick={(buttonId, button) => {
|
onButtonClick={(buttonId, button) => {
|
||||||
console.log(`Button clicked: ${buttonId}`, button);
|
console.log(`Button clicked: ${buttonId}`, button);
|
||||||
// Add global button click handling here
|
// Add global button click handling here
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,22 @@ import React from 'react';
|
||||||
import { GenericPageData, PageButton, PageContent, resolveLanguageText } from './pageInterface';
|
import { GenericPageData, PageButton, PageContent, resolveLanguageText } from './pageInterface';
|
||||||
import { FormGenerator } from '../../components/FormGenerator';
|
import { FormGenerator } from '../../components/FormGenerator';
|
||||||
import { Button, UploadButton } from '../../components/ui';
|
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 {
|
interface PageRendererProps {
|
||||||
pageData: GenericPageData;
|
pageData: GenericPageData;
|
||||||
onButtonClick?: (buttonId: string, button: PageButton) => void;
|
onButtonClick?: (buttonId: string, button: PageButton) => void;
|
||||||
language?: 'de' | 'en' | 'fr';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageRenderer: React.FC<PageRendererProps> = ({
|
const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
pageData,
|
pageData,
|
||||||
onButtonClick,
|
onButtonClick
|
||||||
language = 'de'
|
|
||||||
}) => {
|
}) => {
|
||||||
|
// Get translation function from language context
|
||||||
|
const { t } = useLanguage();
|
||||||
|
|
||||||
// Call the hook at the top level to ensure it persists across renders
|
// 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
|
// This is CRITICAL - hooks must be called in the same order on every render
|
||||||
const tableContent = pageData.content?.find(content => content.type === 'table');
|
const tableContent = pageData.content?.find(content => content.type === 'table');
|
||||||
|
|
@ -32,6 +35,12 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
// Call the hook to get the current data
|
// Call the hook to get the current data
|
||||||
// This will be called on every render, but it's the SAME hook instance
|
// This will be called on every render, but it's the SAME hook instance
|
||||||
const hookData = useTableData ? useTableData() : null;
|
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
|
// Handle button clicks
|
||||||
const handleButtonClick = async (button: PageButton) => {
|
const handleButtonClick = async (button: PageButton) => {
|
||||||
|
|
@ -67,13 +76,13 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
HeadingTag,
|
HeadingTag,
|
||||||
{ key: content.id, className: styles.contentHeading },
|
{ key: content.id, className: styles.contentHeading },
|
||||||
resolveLanguageText(content.content, language)
|
resolveLanguageText(content.content, t)
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'paragraph':
|
case 'paragraph':
|
||||||
return (
|
return (
|
||||||
<p key={content.id} className={styles.contentParagraph}>
|
<p key={content.id} className={styles.contentParagraph}>
|
||||||
{resolveLanguageText(content.content, language)}
|
{resolveLanguageText(content.content, t)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -81,12 +90,12 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
return (
|
return (
|
||||||
<div key={content.id} className={styles.listContainer}>
|
<div key={content.id} className={styles.listContainer}>
|
||||||
{content.content && (
|
{content.content && (
|
||||||
<p className={styles.listTitle}>{resolveLanguageText(content.content, language)}</p>
|
<p className={styles.listTitle}>{resolveLanguageText(content.content, t)}</p>
|
||||||
)}
|
)}
|
||||||
<ul className={styles.list}>
|
<ul className={styles.list}>
|
||||||
{content.items?.map((item, index) => (
|
{content.items?.map((item, index) => (
|
||||||
<li key={index} className={styles.listItem}>
|
<li key={index} className={styles.listItem}>
|
||||||
{resolveLanguageText(item, language)}
|
{resolveLanguageText(item, t)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -97,7 +106,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
return (
|
return (
|
||||||
<pre key={content.id} className={styles.codeBlock}>
|
<pre key={content.id} className={styles.codeBlock}>
|
||||||
<code className={content.language ? `language-${content.language}` : ''}>
|
<code className={content.language ? `language-${content.language}` : ''}>
|
||||||
{resolveLanguageText(content.content, language)}
|
{resolveLanguageText(content.content, t)}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
|
|
@ -142,7 +151,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
// CRITICAL: Resolve LanguageText objects in column labels
|
// CRITICAL: Resolve LanguageText objects in column labels
|
||||||
const resolvedColumns = columns.map(col => ({
|
const resolvedColumns = columns.map(col => ({
|
||||||
...col,
|
...col,
|
||||||
label: resolveLanguageText(col.label, language)
|
label: resolveLanguageText(col.label, t)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Convert action buttons to FormGenerator format
|
// Convert action buttons to FormGenerator format
|
||||||
|
|
@ -152,7 +161,7 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
type: action.type,
|
type: action.type,
|
||||||
onAction: action.onAction,
|
onAction: action.onAction,
|
||||||
// CRITICAL: Resolve LanguageText objects in action titles
|
// CRITICAL: Resolve LanguageText objects in action titles
|
||||||
title: resolveLanguageText(action.title, language),
|
title: resolveLanguageText(action.title, t),
|
||||||
isProcessing: action.loading || (() => false),
|
isProcessing: action.loading || (() => false),
|
||||||
disabled: action.disabled || (() => false),
|
disabled: action.disabled || (() => false),
|
||||||
// Preserve field mappings and operation names
|
// Preserve field mappings and operation names
|
||||||
|
|
@ -177,6 +186,8 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
loading={showLoadingSpinner}
|
loading={showLoadingSpinner}
|
||||||
actionButtons={formGeneratorActions}
|
actionButtons={formGeneratorActions}
|
||||||
hookData={hookData}
|
hookData={hookData}
|
||||||
|
onDelete={hookData.onDelete}
|
||||||
|
onDeleteMultiple={hookData.onDeleteMultiple}
|
||||||
{...tableProps}
|
{...tableProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -189,80 +200,121 @@ 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 (
|
return (
|
||||||
<div className={styles.pageContainer}>
|
<DragDropOverlay config={getDragDropConfig()}>
|
||||||
<div className={styles.pageCard}>
|
<div className={styles.pageContainer}>
|
||||||
{/* Page Header */}
|
<div className={styles.pageCard}>
|
||||||
<div className={styles.pageHeader}>
|
{/* Page Header */}
|
||||||
<div>
|
<div className={styles.pageHeader}>
|
||||||
<h1 className={styles.pageTitle}>{resolveLanguageText(pageData.title, language)}</h1>
|
<div>
|
||||||
{pageData.subtitle && (
|
<h1 className={styles.pageTitle}>{resolveLanguageText(pageData.title, t)}</h1>
|
||||||
<p className={styles.pageSubtitle}>{resolveLanguageText(pageData.subtitle, language)}</p>
|
{pageData.subtitle && (
|
||||||
|
<p className={styles.pageSubtitle}>{resolveLanguageText(pageData.subtitle, t)}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header Buttons */}
|
||||||
|
{pageData.headerButtons && pageData.headerButtons.length > 0 && (
|
||||||
|
<div className={styles.headerButtons}>
|
||||||
|
{pageData.headerButtons.map((button) => {
|
||||||
|
// Check if this is an upload button
|
||||||
|
if (button.id === 'upload-file') {
|
||||||
|
const handleUpload = (hookData as any)?.handleUpload;
|
||||||
|
|
||||||
|
if (handleUpload) {
|
||||||
|
return (
|
||||||
|
<UploadButton
|
||||||
|
key={button.id}
|
||||||
|
onUpload={handleUpload}
|
||||||
|
accept="*/*"
|
||||||
|
multiple={false}
|
||||||
|
variant={button.variant || 'primary'}
|
||||||
|
size={button.size || 'md'}
|
||||||
|
icon={button.icon}
|
||||||
|
disabled={button.disabled}
|
||||||
|
>
|
||||||
|
{resolveLanguageText(button.label, t)}
|
||||||
|
</UploadButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular button
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={button.id}
|
||||||
|
variant={button.variant || 'primary'}
|
||||||
|
size={button.size || 'md'}
|
||||||
|
icon={button.icon}
|
||||||
|
disabled={button.disabled}
|
||||||
|
onClick={() => handleButtonClick(button)}
|
||||||
|
>
|
||||||
|
{resolveLanguageText(button.label, t)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header Buttons */}
|
<div className={styles.horizontalDivider}></div>
|
||||||
{pageData.headerButtons && pageData.headerButtons.length > 0 && (
|
|
||||||
<div className={styles.headerButtons}>
|
{/* Page Content */}
|
||||||
{pageData.headerButtons.map((button) => {
|
<div className={styles.contentArea}>
|
||||||
// Check if this is an upload button
|
<div className={styles.scrollableContent}>
|
||||||
if (button.id === 'upload-file') {
|
{pageData.content?.map((content) => {
|
||||||
const handleUpload = (hookData as any)?.handleUpload;
|
// Check privilege for content
|
||||||
|
if (content.privilegeChecker) {
|
||||||
if (handleUpload) {
|
// For now, we'll render content and let the privilege checker handle it
|
||||||
return (
|
// In a real implementation, you might want to use a hook or context
|
||||||
<UploadButton
|
return renderContent(content);
|
||||||
key={button.id}
|
|
||||||
onUpload={handleUpload}
|
|
||||||
accept="*/*"
|
|
||||||
multiple={false}
|
|
||||||
variant={button.variant || 'primary'}
|
|
||||||
size={button.size || 'md'}
|
|
||||||
icon={button.icon}
|
|
||||||
disabled={button.disabled}
|
|
||||||
>
|
|
||||||
{resolveLanguageText(button.label, language)}
|
|
||||||
</UploadButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return renderContent(content);
|
||||||
// Regular button
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={button.id}
|
|
||||||
variant={button.variant || 'primary'}
|
|
||||||
size={button.size || 'md'}
|
|
||||||
icon={button.icon}
|
|
||||||
disabled={button.disabled}
|
|
||||||
onClick={() => handleButtonClick(button)}
|
|
||||||
>
|
|
||||||
{resolveLanguageText(button.label, language)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.horizontalDivider}></div>
|
|
||||||
|
|
||||||
{/* Page Content */}
|
|
||||||
<div className={styles.contentArea}>
|
|
||||||
<div className={styles.scrollableContent}>
|
|
||||||
{pageData.content?.map((content) => {
|
|
||||||
// Check privilege for content
|
|
||||||
if (content.privilegeChecker) {
|
|
||||||
// For now, we'll render content and let the privilege checker handle it
|
|
||||||
// In a real implementation, you might want to use a hook or context
|
|
||||||
return renderContent(content);
|
|
||||||
}
|
|
||||||
return renderContent(content);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Message Overlay Component */}
|
||||||
|
{hookData?.MessageOverlayComponent && <hookData.MessageOverlayComponent />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DragDropOverlay>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
import { allPageData, SidebarItem } from './data';
|
import { allPageData, SidebarItem } from './data';
|
||||||
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
|
import { resolveLanguageText } from './pageInterface';
|
||||||
|
|
||||||
interface SidebarContextType {
|
interface SidebarContextType {
|
||||||
sidebarItems: SidebarItem[];
|
sidebarItems: SidebarItem[];
|
||||||
|
|
@ -26,6 +28,9 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
const [sidebarItems, setSidebarItems] = useState<SidebarItem[]>([]);
|
const [sidebarItems, setSidebarItems] = useState<SidebarItem[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Get translation function from language context
|
||||||
|
const { t } = useLanguage();
|
||||||
|
|
||||||
// Get sidebar items from page data
|
// Get sidebar items from page data
|
||||||
const getSidebarItems = async (): Promise<SidebarItem[]> => {
|
const getSidebarItems = async (): Promise<SidebarItem[]> => {
|
||||||
|
|
@ -72,22 +77,23 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
// Create expandable item with submenu
|
// Create expandable item with submenu
|
||||||
items.push({
|
items.push({
|
||||||
id: pageData.id,
|
id: pageData.id,
|
||||||
name: pageData.name,
|
name: resolveLanguageText(pageData.name, t),
|
||||||
link: `/${pageData.path}`,
|
link: `/${pageData.path}`,
|
||||||
icon: pageData.icon,
|
icon: pageData.icon,
|
||||||
moduleEnabled: pageData.moduleEnabled ?? true,
|
moduleEnabled: pageData.moduleEnabled ?? true,
|
||||||
order: pageData.order || 0,
|
order: pageData.order || 0,
|
||||||
submenu: subpages.map(subpage => ({
|
submenu: subpages.map(subpage => ({
|
||||||
id: subpage.id,
|
id: subpage.id,
|
||||||
name: subpage.name,
|
name: resolveLanguageText(subpage.name, t),
|
||||||
link: `/${subpage.path}`
|
link: `/${subpage.path}`,
|
||||||
|
icon: subpage.icon
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// No subpages found, show as regular item
|
// No subpages found, show as regular item
|
||||||
items.push({
|
items.push({
|
||||||
id: pageData.id,
|
id: pageData.id,
|
||||||
name: pageData.name,
|
name: resolveLanguageText(pageData.name, t),
|
||||||
link: `/${pageData.path}`,
|
link: `/${pageData.path}`,
|
||||||
icon: pageData.icon,
|
icon: pageData.icon,
|
||||||
moduleEnabled: pageData.moduleEnabled ?? true,
|
moduleEnabled: pageData.moduleEnabled ?? true,
|
||||||
|
|
@ -98,7 +104,7 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
// No subpage privilege, show as regular non-expandable item
|
// No subpage privilege, show as regular non-expandable item
|
||||||
items.push({
|
items.push({
|
||||||
id: pageData.id,
|
id: pageData.id,
|
||||||
name: pageData.name,
|
name: resolveLanguageText(pageData.name, t),
|
||||||
link: `/${pageData.path}`,
|
link: `/${pageData.path}`,
|
||||||
icon: pageData.icon,
|
icon: pageData.icon,
|
||||||
moduleEnabled: pageData.moduleEnabled ?? true,
|
moduleEnabled: pageData.moduleEnabled ?? true,
|
||||||
|
|
@ -110,7 +116,7 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
// Fallback to regular item on error
|
// Fallback to regular item on error
|
||||||
items.push({
|
items.push({
|
||||||
id: pageData.id,
|
id: pageData.id,
|
||||||
name: pageData.name,
|
name: resolveLanguageText(pageData.name, t),
|
||||||
link: `/${pageData.path}`,
|
link: `/${pageData.path}`,
|
||||||
icon: pageData.icon,
|
icon: pageData.icon,
|
||||||
moduleEnabled: pageData.moduleEnabled ?? true,
|
moduleEnabled: pageData.moduleEnabled ?? true,
|
||||||
|
|
@ -121,7 +127,7 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
||||||
// Regular items without subpages
|
// Regular items without subpages
|
||||||
items.push({
|
items.push({
|
||||||
id: pageData.id,
|
id: pageData.id,
|
||||||
name: pageData.name,
|
name: resolveLanguageText(pageData.name, t),
|
||||||
link: `/${pageData.path}`,
|
link: `/${pageData.path}`,
|
||||||
icon: pageData.icon,
|
icon: pageData.icon,
|
||||||
moduleEnabled: pageData.moduleEnabled ?? true,
|
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(() => {
|
useEffect(() => {
|
||||||
refreshSidebar();
|
refreshSidebar();
|
||||||
}, []);
|
}, [t]);
|
||||||
|
|
||||||
const contextValue: SidebarContextType = {
|
const contextValue: SidebarContextType = {
|
||||||
sidebarItems,
|
sidebarItems,
|
||||||
|
|
|
||||||
|
|
@ -2,45 +2,45 @@ import { GenericPageData } from '../../pageInterface';
|
||||||
import { FaCogs } from 'react-icons/fa';
|
import { FaCogs } from 'react-icons/fa';
|
||||||
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
|
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
|
||||||
|
|
||||||
export const verwaltungPageData: GenericPageData = {
|
export const administrationPageData: GenericPageData = {
|
||||||
id: 'verwaltung',
|
id: 'administration',
|
||||||
path: 'verwaltung',
|
path: 'administration',
|
||||||
name: 'Verwaltung',
|
name: 'administration.title',
|
||||||
description: 'Administration and management tools',
|
description: 'administration.description',
|
||||||
|
|
||||||
// Visual
|
// Visual
|
||||||
icon: FaCogs,
|
icon: FaCogs,
|
||||||
title: 'Verwaltung',
|
title: 'administration.title',
|
||||||
subtitle: 'Administration and management tools',
|
subtitle: 'administration.subtitle',
|
||||||
|
|
||||||
// Content sections
|
// Content sections
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
id: 'intro',
|
id: 'intro',
|
||||||
type: 'heading',
|
type: 'heading',
|
||||||
content: 'Verwaltung',
|
content: 'administration.title',
|
||||||
level: 2
|
level: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'description',
|
id: 'description',
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: 'This section contains all administration and management tools for your workspace.'
|
content: 'administration.intro.description'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'features',
|
id: 'features',
|
||||||
type: 'heading',
|
type: 'heading',
|
||||||
content: 'Available Tools',
|
content: 'administration.features.title',
|
||||||
level: 3
|
level: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'features-list',
|
id: 'features-list',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
content: 'Management tools include:',
|
content: 'administration.features.description',
|
||||||
items: [
|
items: [
|
||||||
'File Management - Upload and organize documents',
|
'administration.features.file_management',
|
||||||
'User Management - Manage team members and permissions',
|
'administration.features.user_management',
|
||||||
'System Settings - Configure workspace settings',
|
'administration.features.system_settings',
|
||||||
'Data Management - Handle data imports and exports'
|
'administration.features.data_management'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -63,6 +63,6 @@ export const verwaltungPageData: GenericPageData = {
|
||||||
|
|
||||||
// Lifecycle hooks
|
// Lifecycle hooks
|
||||||
onActivate: async () => {
|
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 { useCallback } from 'react';
|
||||||
import { GenericPageData, LanguageText } from '../../pageInterface';
|
import { GenericPageData } from '../../pageInterface';
|
||||||
import { FaRegFileAlt, FaUpload } from 'react-icons/fa';
|
import { FaRegFileAlt, FaUpload } from 'react-icons/fa';
|
||||||
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
|
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
|
||||||
import { useUserFiles, useFileOperations } from '../../../../hooks/useFiles';
|
import { useUserFiles, useFileOperations } from '../../../../hooks/useFiles';
|
||||||
|
|
@ -11,13 +11,15 @@ const createFilesHook = () => {
|
||||||
const {
|
const {
|
||||||
handleFileDownload,
|
handleFileDownload,
|
||||||
handleFileDelete,
|
handleFileDelete,
|
||||||
|
handleFileDeleteMultiple,
|
||||||
handleFilePreview,
|
handleFilePreview,
|
||||||
handleFileUpdate,
|
handleFileUpdate,
|
||||||
handleFileUpload: hookHandleFileUpload,
|
handleFileUpload: hookHandleFileUpload,
|
||||||
downloadingFiles,
|
downloadingFiles,
|
||||||
deletingFiles,
|
deletingFiles,
|
||||||
previewingFiles,
|
previewingFiles,
|
||||||
editingFiles
|
editingFiles,
|
||||||
|
MessageOverlayComponent
|
||||||
} = useFileOperations();
|
} = useFileOperations();
|
||||||
|
|
||||||
// Upload function that can be called from header buttons
|
// Upload function that can be called from header buttons
|
||||||
|
|
@ -40,6 +42,33 @@ const createFilesHook = () => {
|
||||||
}
|
}
|
||||||
}, [hookHandleFileUpload, refetch]); // Only recreate if dependencies change
|
}, [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 {
|
return {
|
||||||
data: files,
|
data: files,
|
||||||
loading,
|
loading,
|
||||||
|
|
@ -49,14 +78,20 @@ const createFilesHook = () => {
|
||||||
// Operations
|
// Operations
|
||||||
handleDownload: handleFileDownload,
|
handleDownload: handleFileDownload,
|
||||||
handleDelete: handleFileDelete,
|
handleDelete: handleFileDelete,
|
||||||
|
handleDeleteMultiple: handleFileDeleteMultiple,
|
||||||
handlePreview: handleFilePreview,
|
handlePreview: handleFilePreview,
|
||||||
handleUpload: handleFileUpload,
|
handleUpload: handleFileUpload,
|
||||||
handleFileUpdate: handleFileUpdate,
|
handleFileUpdate: handleFileUpdate,
|
||||||
|
// FormGenerator specific handlers
|
||||||
|
onDelete: handleDeleteSingle,
|
||||||
|
onDeleteMultiple: handleDeleteMultiple,
|
||||||
// Loading states
|
// Loading states
|
||||||
downloadingFiles,
|
downloadingFiles,
|
||||||
deletingFiles,
|
deletingFiles,
|
||||||
previewingFiles,
|
previewingFiles,
|
||||||
editingFiles
|
editingFiles,
|
||||||
|
// Message overlay component
|
||||||
|
MessageOverlayComponent
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -65,11 +100,7 @@ const createFilesHook = () => {
|
||||||
const filesColumns = [
|
const filesColumns = [
|
||||||
{
|
{
|
||||||
key: 'file_name',
|
key: 'file_name',
|
||||||
label: {
|
label: 'files.column.filename',
|
||||||
de: 'Dateiname',
|
|
||||||
en: 'Filename',
|
|
||||||
fr: 'Nom de fichier'
|
|
||||||
},
|
|
||||||
type: 'string',
|
type: 'string',
|
||||||
width: 300,
|
width: 300,
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
|
|
@ -80,11 +111,7 @@ const filesColumns = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'mime_type',
|
key: 'mime_type',
|
||||||
label: {
|
label: 'files.column.mimetype',
|
||||||
de: 'Dateityp',
|
|
||||||
en: 'File Type',
|
|
||||||
fr: 'Type de fichier'
|
|
||||||
},
|
|
||||||
type: 'string',
|
type: 'string',
|
||||||
width: 200,
|
width: 200,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
|
|
@ -95,11 +122,7 @@ const filesColumns = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'size',
|
key: 'size',
|
||||||
label: {
|
label: 'files.column.filesize',
|
||||||
de: 'Dateigröße',
|
|
||||||
en: 'File Size',
|
|
||||||
fr: 'Taille du fichier'
|
|
||||||
},
|
|
||||||
type: 'number',
|
type: 'number',
|
||||||
width: 140,
|
width: 140,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
|
|
@ -109,11 +132,7 @@ const filesColumns = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'created_at',
|
key: 'created_at',
|
||||||
label: {
|
label: 'files.column.creationdate',
|
||||||
de: 'Erstellungsdatum',
|
|
||||||
en: 'Creation Date',
|
|
||||||
fr: 'Date de création'
|
|
||||||
},
|
|
||||||
type: 'date',
|
type: 'date',
|
||||||
width: 200,
|
width: 200,
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
|
|
@ -123,41 +142,25 @@ const filesColumns = [
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const dateienPageData: GenericPageData = {
|
export const filesPageData: GenericPageData = {
|
||||||
id: 'verwaltung-dateien',
|
id: 'administration-files',
|
||||||
path: 'verwaltung/dateien',
|
path: 'administration/files',
|
||||||
name: 'Dateien',
|
name: 'files.title',
|
||||||
description: {
|
description: 'files.title',
|
||||||
de: 'Dateiverwaltung und -organisation',
|
|
||||||
en: 'File management and organization',
|
|
||||||
fr: 'Gestion et organisation des fichiers'
|
|
||||||
},
|
|
||||||
|
|
||||||
// Parent page
|
// Parent page
|
||||||
parentPath: 'verwaltung',
|
parentPath: 'administration',
|
||||||
|
|
||||||
// Visual
|
// Visual
|
||||||
icon: FaRegFileAlt,
|
icon: FaRegFileAlt,
|
||||||
title: {
|
title: 'files.title',
|
||||||
de: 'Dateien',
|
subtitle: 'files.title',
|
||||||
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'
|
|
||||||
},
|
|
||||||
|
|
||||||
// Header buttons
|
// Header buttons
|
||||||
headerButtons: [
|
headerButtons: [
|
||||||
{
|
{
|
||||||
id: 'upload-file',
|
id: 'upload-file',
|
||||||
label: {
|
label: 'files.upload_button',
|
||||||
de: 'Datei hochladen',
|
|
||||||
en: 'Upload File',
|
|
||||||
fr: 'Télécharger un fichier'
|
|
||||||
},
|
|
||||||
icon: FaUpload,
|
icon: FaUpload,
|
||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
// onClick will be handled by PageRenderer to render UploadButton
|
// onClick will be handled by PageRenderer to render UploadButton
|
||||||
|
|
@ -176,11 +179,7 @@ export const dateienPageData: GenericPageData = {
|
||||||
actionButtons: [
|
actionButtons: [
|
||||||
{
|
{
|
||||||
type: 'view',
|
type: 'view',
|
||||||
title: {
|
title: 'files.action.preview',
|
||||||
de: 'Datei vorschauen',
|
|
||||||
en: 'Preview file',
|
|
||||||
fr: 'Aperçu du fichier'
|
|
||||||
},
|
|
||||||
idField: 'id',
|
idField: 'id',
|
||||||
nameField: 'file_name',
|
nameField: 'file_name',
|
||||||
typeField: 'mime_type',
|
typeField: 'mime_type',
|
||||||
|
|
@ -189,11 +188,7 @@ export const dateienPageData: GenericPageData = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
title: {
|
title: 'files.action.edit',
|
||||||
de: 'Datei bearbeiten',
|
|
||||||
en: 'Edit file',
|
|
||||||
fr: 'Modifier le fichier'
|
|
||||||
},
|
|
||||||
idField: 'id',
|
idField: 'id',
|
||||||
nameField: 'file_name',
|
nameField: 'file_name',
|
||||||
typeField: 'mime_type',
|
typeField: 'mime_type',
|
||||||
|
|
@ -207,22 +202,14 @@ export const dateienPageData: GenericPageData = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'download',
|
type: 'download',
|
||||||
title: {
|
title: 'files.action.download',
|
||||||
de: 'Datei herunterladen',
|
|
||||||
en: 'Download file',
|
|
||||||
fr: 'Télécharger le fichier'
|
|
||||||
},
|
|
||||||
idField: 'id',
|
idField: 'id',
|
||||||
operationName: 'handleDownload',
|
operationName: 'handleDownload',
|
||||||
loadingStateName: 'downloadingFiles'
|
loadingStateName: 'downloadingFiles'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
title: {
|
title: 'files.action.delete',
|
||||||
de: 'Datei löschen',
|
|
||||||
en: 'Delete file',
|
|
||||||
fr: 'Supprimer le fichier'
|
|
||||||
},
|
|
||||||
idField: 'id',
|
idField: 'id',
|
||||||
operationName: 'handleDelete',
|
operationName: 'handleDelete',
|
||||||
loadingStateName: 'deletingFiles'
|
loadingStateName: 'deletingFiles'
|
||||||
|
|
@ -234,7 +221,7 @@ export const dateienPageData: GenericPageData = {
|
||||||
resizable: true,
|
resizable: true,
|
||||||
pagination: true,
|
pagination: true,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
className: 'dateien-table'
|
className: 'files-table'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -247,17 +234,25 @@ export const dateienPageData: GenericPageData = {
|
||||||
preload: false,
|
preload: false,
|
||||||
moduleEnabled: true,
|
moduleEnabled: true,
|
||||||
|
|
||||||
// Sidebar - will be shown as subpage under Verwaltung
|
// Sidebar - will be shown as subpage under Administration
|
||||||
showInSidebar: false,
|
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
|
// Lifecycle hooks
|
||||||
onActivate: async () => {
|
onActivate: async () => {
|
||||||
if (import.meta.env.DEV) console.log('Dateien activated');
|
if (import.meta.env.DEV) console.log('Files activated');
|
||||||
},
|
},
|
||||||
onLoad: async () => {
|
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 () => {
|
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 all page data
|
||||||
export { dashboardPageData } from './dashboard';
|
export { dashboardPageData } from './dashboard';
|
||||||
export { dateienPageData } from './dateien';
|
export { filesPageData } from './files';
|
||||||
export { teamBereichPageData } from './team-bereich';
|
export { teamBereichPageData } from './team-bereich';
|
||||||
export { examplePageData, exampleSubpage1Data, exampleSubpage2Data } from './example-page';
|
export { administrationPageData } from './administration';
|
||||||
export { verwaltungPageData } from './verwaltung';
|
|
||||||
|
|
||||||
// Import all page data
|
// Import all page data
|
||||||
import { dashboardPageData } from './dashboard';
|
import { dashboardPageData } from './dashboard';
|
||||||
import { dateienPageData } from './dateien';
|
import { administrationPageData } from './administration';
|
||||||
|
import { filesPageData } from './files';
|
||||||
import { teamBereichPageData } from './team-bereich';
|
import { teamBereichPageData } from './team-bereich';
|
||||||
import { examplePageData, exampleSubpage1Data, exampleSubpage2Data } from './example-page';
|
|
||||||
import { verwaltungPageData } from './verwaltung';
|
|
||||||
|
|
||||||
// Array of all page data
|
// Array of all page data
|
||||||
export const allPageData = [
|
export const allPageData = [
|
||||||
dashboardPageData,
|
dashboardPageData,
|
||||||
verwaltungPageData,
|
administrationPageData,
|
||||||
dateienPageData,
|
filesPageData,
|
||||||
teamBereichPageData,
|
teamBereichPageData,
|
||||||
examplePageData,
|
|
||||||
exampleSubpage1Data,
|
|
||||||
exampleSubpage2Data
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Helper function to get page data by path
|
// Helper function to get page data by path
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IconType } from 'react-icons';
|
import { IconType } from 'react-icons';
|
||||||
|
import { DragDropConfig } from '../../components/ui/DragDropOverlay/DragDropOverlay';
|
||||||
|
|
||||||
// Generic privilege checker function type
|
// Generic privilege checker function type
|
||||||
export type PrivilegeChecker = () => boolean | Promise<boolean>;
|
export type PrivilegeChecker = () => boolean | Promise<boolean>;
|
||||||
|
|
@ -44,6 +45,11 @@ export interface GenericDataHook {
|
||||||
handleDownload?: (fileId: string, fileName: string) => Promise<boolean>; // For file download functionality
|
handleDownload?: (fileId: string, fileName: string) => Promise<boolean>; // For file download functionality
|
||||||
handleDelete?: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>; // For file delete 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
|
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
|
// Action button configuration
|
||||||
|
|
@ -84,10 +90,18 @@ export interface LanguageText {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility function to resolve language text
|
// 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 (!text) return '';
|
||||||
if (typeof text === 'string') return text;
|
if (typeof text === 'string') {
|
||||||
return text[language] || text.de || '';
|
// 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
|
// Generic page data interface
|
||||||
|
|
@ -135,6 +149,9 @@ export interface GenericPageData {
|
||||||
|
|
||||||
// Custom component override (optional)
|
// Custom component override (optional)
|
||||||
customComponent?: React.ComponentType<any>;
|
customComponent?: React.ComponentType<any>;
|
||||||
|
|
||||||
|
// Drag and drop configuration
|
||||||
|
dragDropConfig?: DragDropConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page data file structure
|
// Page data file structure
|
||||||
|
|
@ -159,6 +176,7 @@ export interface SidebarSubmenuItemData {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
link: string;
|
link: string;
|
||||||
|
icon?: IconType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page instance for PageManager
|
// 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 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
|
// File interfaces - exactly matching backend FileItem model
|
||||||
export interface FileInfo {
|
export interface FileInfo {
|
||||||
|
|
@ -29,51 +32,18 @@ export function useUserFiles() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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 () => {
|
const fetchFiles = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
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
|
// Debug: Check what auth headers are being sent
|
||||||
const authData = localStorage.getItem('auth_data');
|
const authData = localStorage.getItem('auth_data');
|
||||||
if (authData) {
|
if (authData) {
|
||||||
try {
|
try {
|
||||||
const tokenData = JSON.parse(authData);
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('❌ Failed to parse auth_data:', e);
|
console.error('❌ Failed to parse auth_data:', e);
|
||||||
}
|
}
|
||||||
|
|
@ -81,14 +51,10 @@ export function useUserFiles() {
|
||||||
|
|
||||||
const response = await api.get('/api/files/list');
|
const response = await api.get('/api/files/list');
|
||||||
const data = response.data;
|
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
|
// Ensure data is an array, handle null/undefined responses
|
||||||
const fileList = Array.isArray(data) ? data : [];
|
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
|
// Filter out invalid files and map API response to our frontend model
|
||||||
const validFiles = fileList.filter((apiFile: any): boolean => {
|
const validFiles = fileList.filter((apiFile: any): boolean => {
|
||||||
|
|
@ -114,30 +80,9 @@ export function useUserFiles() {
|
||||||
apiFile.creationDate > 0
|
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;
|
return isValid;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`✨ Filtered to ${validFiles.length} valid files`);
|
|
||||||
if (validFiles.length !== fileList.length) {
|
if (validFiles.length !== fileList.length) {
|
||||||
console.warn(`⚠️ Filtered out ${fileList.length - validFiles.length} invalid files`);
|
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);
|
setFiles(mappedFiles);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('❌ Error fetching files:', error);
|
console.error('❌ Error fetching files:', error);
|
||||||
|
|
@ -208,7 +152,6 @@ export function useUserFiles() {
|
||||||
|
|
||||||
// Provide informative placeholder when CORS blocks the request
|
// Provide informative placeholder when CORS blocks the request
|
||||||
if (error.message === 'Keine Antwort vom Server erhalten' || error.message === 'Network Error') {
|
if (error.message === 'Keine Antwort vom Server erhalten' || error.message === 'Network Error') {
|
||||||
console.log('📝 CORS blocking files API - providing informative placeholder');
|
|
||||||
const corsPlaceholderFile: UserFile = {
|
const corsPlaceholderFile: UserFile = {
|
||||||
id: 'cors-info',
|
id: 'cors-info',
|
||||||
file_name: 'Files Service Temporarily Unavailable',
|
file_name: 'Files Service Temporarily Unavailable',
|
||||||
|
|
@ -253,12 +196,10 @@ export function useUserFiles() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('🔄 useUserFiles useEffect triggered - fetching files on mount');
|
|
||||||
fetchFiles();
|
fetchFiles();
|
||||||
}, [fetchFiles]); // Depend on fetchFiles which is memoized with useCallback
|
}, [fetchFiles]); // Depend on fetchFiles which is memoized with useCallback
|
||||||
|
|
||||||
const refetch = useCallback(async () => {
|
const refetch = useCallback(async () => {
|
||||||
console.log('🔄 Refetching files...');
|
|
||||||
setIsRefetching(true);
|
setIsRefetching(true);
|
||||||
try {
|
try {
|
||||||
await fetchFiles();
|
await fetchFiles();
|
||||||
|
|
@ -290,14 +231,20 @@ export function useFileOperations() {
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
const [previewingFiles, setPreviewingFiles] = useState<Set<string>>(new Set());
|
const [previewingFiles, setPreviewingFiles] = useState<Set<string>>(new Set());
|
||||||
const [previewError, setPreviewError] = useState<string | null>(null);
|
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) => {
|
const handleFileDownload = async (fileId: string, fileName: string) => {
|
||||||
setDownloadError(null);
|
setDownloadError(null);
|
||||||
setDownloadingFiles(prev => new Set(prev).add(fileId));
|
setDownloadingFiles(prev => new Set(prev).add(fileId));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`📥 Starting download for file: ${fileName} (ID: ${fileId})`);
|
|
||||||
|
|
||||||
// Try to get the file download
|
// Try to get the file download
|
||||||
const response = await api.get(`/api/files/${fileId}/download`, {
|
const response = await api.get(`/api/files/${fileId}/download`, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
|
|
@ -306,8 +253,7 @@ export function useFileOperations() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const blob = response.data;
|
const blob = response.data;
|
||||||
console.log(`✅ Download successful for: ${fileName}`, { size: blob.size, type: blob.type });
|
|
||||||
|
|
||||||
// Create a download link and trigger the download
|
// Create a download link and trigger the download
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
|
|
@ -350,12 +296,9 @@ export function useFileOperations() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`🗑️ Starting delete for file ID: ${fileId}`);
|
|
||||||
|
|
||||||
await api.delete(`/api/files/${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
|
// Add a small delay to ensure backend has time to process
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
return true;
|
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!
|
* File upload function - backend bug has been fixed!
|
||||||
*
|
*
|
||||||
|
|
@ -396,12 +389,6 @@ export function useFileOperations() {
|
||||||
setUploadingFile(true);
|
setUploadingFile(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('📤 Starting file upload...', {
|
|
||||||
fileName: file.name,
|
|
||||||
fileSize: file.size,
|
|
||||||
fileType: file.type,
|
|
||||||
workflowId: workflowId
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate file before upload
|
// Validate file before upload
|
||||||
if (!file || !file.name || file.name.trim() === '') {
|
if (!file || !file.name || file.name.trim() === '') {
|
||||||
|
|
@ -421,22 +408,6 @@ export function useFileOperations() {
|
||||||
|
|
||||||
// FormData is now correctly configured for backend
|
// 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, {
|
const response = await api.post('/api/files/upload', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -446,6 +417,46 @@ export function useFileOperations() {
|
||||||
const fileData = response.data;
|
const fileData = response.data;
|
||||||
|
|
||||||
console.log('✅ Upload successful:', fileData);
|
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 };
|
return { success: true, fileData };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('❌ Upload failed:', {
|
console.error('❌ Upload failed:', {
|
||||||
|
|
@ -482,12 +493,7 @@ export function useFileOperations() {
|
||||||
setEditingFiles(prev => new Set(prev).add(fileId));
|
setEditingFiles(prev => new Set(prev).add(fileId));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`✏️ Starting update for file ID: ${fileId}`, {
|
|
||||||
fileId,
|
|
||||||
updateData,
|
|
||||||
url: `/api/files/${fileId}`,
|
|
||||||
method: 'put'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use PUT request with complete file object
|
// Use PUT request with complete file object
|
||||||
// Always use current timestamp for creationDate to avoid validation issues
|
// 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
|
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, {
|
const response = await api.put(`/api/files/${fileId}`, completeFileObject, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -522,7 +518,6 @@ export function useFileOperations() {
|
||||||
});
|
});
|
||||||
const updatedFile = response.data;
|
const updatedFile = response.data;
|
||||||
|
|
||||||
console.log(`✅ Update successful for file ID: ${fileId}`, updatedFile);
|
|
||||||
return { success: true, fileData: updatedFile };
|
return { success: true, fileData: updatedFile };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`❌ Update failed for file ID ${fileId}:`, {
|
console.error(`❌ Update failed for file ID ${fileId}:`, {
|
||||||
|
|
@ -565,11 +560,9 @@ export function useFileOperations() {
|
||||||
setPreviewingFiles(prev => new Set(prev).add(fileId));
|
setPreviewingFiles(prev => new Set(prev).add(fileId));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`👁️ Starting preview for file: ${fileName} (ID: ${fileId})`, { mimeType });
|
|
||||||
|
|
||||||
// For PDF files, try JSON response first (API returns base64-encoded PDF)
|
// For PDF files, try JSON response first (API returns base64-encoded PDF)
|
||||||
if (mimeType === 'application/pdf') {
|
if (mimeType === 'application/pdf') {
|
||||||
console.log('📄 PDF file detected, trying JSON response with base64 content');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/api/files/${fileId}/preview`, {
|
const response = await api.get(`/api/files/${fileId}/preview`, {
|
||||||
|
|
@ -580,56 +573,31 @@ export function useFileOperations() {
|
||||||
});
|
});
|
||||||
const jsonResponse = response.data;
|
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
|
// Check if response has base64-encoded PDF content
|
||||||
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
|
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
|
||||||
let content = jsonResponse.content;
|
let content = jsonResponse.content;
|
||||||
const responseMimeType = jsonResponse.mimeType || 'application/pdf';
|
|
||||||
|
|
||||||
// The content field contains base64-encoded JSON, so decode it first
|
// The content field contains base64-encoded JSON, so decode it first
|
||||||
if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
|
if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
|
||||||
console.log('📄 Content appears to be base64-encoded, decoding first...');
|
|
||||||
try {
|
try {
|
||||||
const decodedJsonString = atob(content);
|
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
|
// Parse the decoded JSON string
|
||||||
const nestedJson = JSON.parse(decodedJsonString);
|
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) {
|
if (nestedJson && typeof nestedJson === 'object' && 'content' in nestedJson) {
|
||||||
const innerContent = nestedJson.content;
|
const innerContent = nestedJson.content;
|
||||||
const isBase64 = /^[A-Za-z0-9+/=]+$/.test(innerContent);
|
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) {
|
if (isBase64) {
|
||||||
// It's base64-encoded PDF content
|
// It's base64-encoded PDF content
|
||||||
content = innerContent;
|
content = innerContent;
|
||||||
} else {
|
} else {
|
||||||
// It's plain text content, not a PDF
|
// 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 the text content for the FilePreview to handle as text
|
||||||
return {
|
return {
|
||||||
success: true,
|
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
|
// Decode base64 content
|
||||||
let decodedContent;
|
let decodedContent;
|
||||||
try {
|
try {
|
||||||
decodedContent = atob(content);
|
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
|
// Verify it's actually a PDF
|
||||||
const isPDF = decodedContent.startsWith('%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) {
|
if (!isPDF) {
|
||||||
console.warn('⚠️ Decoded content does not appear to be a valid PDF');
|
console.warn('⚠️ Decoded content does not appear to be a valid PDF');
|
||||||
console.log('📄 Full decoded content preview:', decodedContent.substring(0, 200));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (decodeError) {
|
} catch (decodeError) {
|
||||||
|
|
@ -695,21 +645,14 @@ export function useFileOperations() {
|
||||||
const blob = new Blob([uint8Array], { type: 'application/pdf' });
|
const blob = new Blob([uint8Array], { type: 'application/pdf' });
|
||||||
const url = window.URL.createObjectURL(blob);
|
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 };
|
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No content field in PDF response');
|
throw new Error('No content field in PDF response');
|
||||||
}
|
}
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
console.log('📄 JSON PDF response failed, trying blob response...', jsonError);
|
|
||||||
|
|
||||||
// Fallback to blob response
|
// Fallback to blob response
|
||||||
const response = await api.get(`/api/files/${fileId}/preview`, {
|
const response = await api.get(`/api/files/${fileId}/preview`, {
|
||||||
|
|
@ -720,11 +663,7 @@ export function useFileOperations() {
|
||||||
});
|
});
|
||||||
const previewData = response.data;
|
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);
|
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)
|
// For image files, try JSON response first (API returns base64-encoded images)
|
||||||
if (mimeType?.startsWith('image/')) {
|
if (mimeType?.startsWith('image/')) {
|
||||||
console.log('🖼️ Image file detected, trying JSON response with base64 content');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/api/files/${fileId}/preview`, {
|
const response = await api.get(`/api/files/${fileId}/preview`, {
|
||||||
|
|
@ -745,12 +684,7 @@ export function useFileOperations() {
|
||||||
});
|
});
|
||||||
const jsonResponse = response.data;
|
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
|
// Check if response has base64-encoded image content
|
||||||
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
|
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
|
// The content field contains base64-encoded data, decode it first
|
||||||
if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
|
if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
|
||||||
console.log('🖼️ Content appears to be base64-encoded, decoding first...');
|
|
||||||
try {
|
try {
|
||||||
const decodedString = atob(content);
|
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
|
// Check if it's JSON (nested structure) or direct image data
|
||||||
if (decodedString.startsWith('{')) {
|
if (decodedString.startsWith('{')) {
|
||||||
// It's JSON, parse it
|
// It's JSON, parse it
|
||||||
const nestedJson = JSON.parse(decodedString);
|
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) {
|
if (nestedJson && typeof nestedJson === 'object' && 'content' in nestedJson) {
|
||||||
const innerContent = nestedJson.content;
|
const innerContent = nestedJson.content;
|
||||||
const isBase64 = /^[A-Za-z0-9+/=]+$/.test(innerContent);
|
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) {
|
if (isBase64) {
|
||||||
// It's base64-encoded image content
|
// 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')) {
|
} 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
|
// 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
|
content = btoa(decodedString); // Re-encode as base64 for processing
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Decoded content is neither JSON nor image data');
|
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
|
// Decode base64 content
|
||||||
let decodedContent;
|
let decodedContent;
|
||||||
try {
|
try {
|
||||||
decodedContent = atob(content);
|
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
|
// Verify it's actually an image by checking for common image headers
|
||||||
const isJPEG = decodedContent.startsWith('\xFF\xD8\xFF');
|
const isJPEG = decodedContent.startsWith('\xFF\xD8\xFF');
|
||||||
|
|
@ -831,13 +743,7 @@ export function useFileOperations() {
|
||||||
const isGIF = decodedContent.startsWith('GIF8');
|
const isGIF = decodedContent.startsWith('GIF8');
|
||||||
const isWebP = decodedContent.startsWith('RIFF') && decodedContent.includes('WEBP');
|
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) {
|
if (!isJPEG && !isPNG && !isGIF && !isWebP) {
|
||||||
console.warn('⚠️ Decoded content does not appear to be a valid image');
|
console.warn('⚠️ Decoded content does not appear to be a valid image');
|
||||||
|
|
@ -858,20 +764,14 @@ export function useFileOperations() {
|
||||||
const blob = new Blob([uint8Array], { type: responseMimeType });
|
const blob = new Blob([uint8Array], { type: responseMimeType });
|
||||||
const url = window.URL.createObjectURL(blob);
|
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 };
|
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No content field in image response');
|
throw new Error('No content field in image response');
|
||||||
}
|
}
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
console.log('🖼️ JSON image response failed, trying blob response...', jsonError);
|
|
||||||
|
|
||||||
// Fallback to blob response
|
// Fallback to blob response
|
||||||
const response = await api.get(`/api/files/${fileId}/preview`, {
|
const response = await api.get(`/api/files/${fileId}/preview`, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
|
|
@ -880,12 +780,7 @@ export function useFileOperations() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const previewData = response.data;
|
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);
|
const url = window.URL.createObjectURL(previewData);
|
||||||
|
|
||||||
|
|
@ -902,36 +797,23 @@ export function useFileOperations() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const jsonResponse = response.data;
|
const jsonResponse = response.data;
|
||||||
|
|
||||||
console.log(`✅ JSON preview successful for: ${fileName}`, jsonResponse);
|
|
||||||
|
|
||||||
// Check if response has content field (structured response)
|
// Check if response has content field (structured response)
|
||||||
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
|
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
|
||||||
const content = jsonResponse.content;
|
const content = jsonResponse.content;
|
||||||
const mimeType = jsonResponse.mimeType || 'text/plain';
|
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)
|
// Check if content is base64 encoded (common pattern)
|
||||||
let decodedContent = content;
|
let decodedContent = content;
|
||||||
try {
|
try {
|
||||||
// Try to decode as base64 if it looks like base64
|
// Try to decode as base64 if it looks like base64
|
||||||
if (content && typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
|
if (content && typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
|
||||||
console.log('📄 Content appears to be base64 encoded, attempting decode...');
|
|
||||||
decodedContent = atob(content);
|
decodedContent = atob(content);
|
||||||
console.log('📄 Base64 decode successful:', {
|
|
||||||
originalLength: content.length,
|
|
||||||
decodedLength: decodedContent.length,
|
|
||||||
decodedPreview: decodedContent.substring(0, 200) + '...'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (decodeError) {
|
} catch (decodeError) {
|
||||||
console.log('📄 Base64 decode failed, using original content:', decodeError);
|
|
||||||
decodedContent = content;
|
decodedContent = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -939,36 +821,22 @@ export function useFileOperations() {
|
||||||
const blob = new Blob([decodedContent], { type: mimeType });
|
const blob = new Blob([decodedContent], { type: mimeType });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
console.log('🔗 Created blob URL:', url);
|
|
||||||
|
|
||||||
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
|
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
|
||||||
} else if (jsonResponse && typeof jsonResponse === 'object' && 'result' in jsonResponse) {
|
} else if (jsonResponse && typeof jsonResponse === 'object' && 'result' in jsonResponse) {
|
||||||
// Handle base64 encoded content in 'result' field
|
// Handle base64 encoded content in 'result' field
|
||||||
console.log('📄 Base64 encoded content detected in result field');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Decode base64 content
|
// Decode base64 content
|
||||||
const decodedContent = atob(jsonResponse.result);
|
const decodedContent = atob(jsonResponse.result);
|
||||||
const mimeType = jsonResponse.mimeType || 'application/json';
|
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
|
// Create a blob from the decoded content
|
||||||
const blob = new Blob([decodedContent], { type: mimeType });
|
const blob = new Blob([decodedContent], { type: mimeType });
|
||||||
const url = window.URL.createObjectURL(blob);
|
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 };
|
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
|
||||||
} catch (decodeError) {
|
} catch (decodeError) {
|
||||||
|
|
@ -980,7 +848,6 @@ export function useFileOperations() {
|
||||||
return { success: true, previewUrl: url, blob: blob, isJsonContent: true };
|
return { success: true, previewUrl: url, blob: blob, isJsonContent: true };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('📄 Raw JSON response, treating as content');
|
|
||||||
// If it's not structured JSON, treat as raw content
|
// If it's not structured JSON, treat as raw content
|
||||||
const blob = new Blob([JSON.stringify(jsonResponse, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(jsonResponse, null, 2)], { type: 'application/json' });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
@ -988,7 +855,6 @@ export function useFileOperations() {
|
||||||
return { success: true, previewUrl: url, blob: blob, isJsonContent: true };
|
return { success: true, previewUrl: url, blob: blob, isJsonContent: true };
|
||||||
}
|
}
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
console.log('JSON preview failed, trying blob response...', jsonError);
|
|
||||||
|
|
||||||
// Fallback to blob response for binary files
|
// Fallback to blob response for binary files
|
||||||
const response = await api.get(`/api/files/${fileId}/preview`, {
|
const response = await api.get(`/api/files/${fileId}/preview`, {
|
||||||
|
|
@ -999,7 +865,6 @@ export function useFileOperations() {
|
||||||
});
|
});
|
||||||
const previewData = response.data;
|
const previewData = response.data;
|
||||||
|
|
||||||
console.log(`✅ Blob preview successful for: ${fileName}`, { size: previewData.size, type: previewData.type });
|
|
||||||
|
|
||||||
// Create a blob URL for preview
|
// Create a blob URL for preview
|
||||||
const url = window.URL.createObjectURL(previewData);
|
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 {
|
return {
|
||||||
downloadingFiles,
|
downloadingFiles,
|
||||||
deletingFiles,
|
deletingFiles,
|
||||||
|
|
@ -1041,9 +915,20 @@ export function useFileOperations() {
|
||||||
previewError,
|
previewError,
|
||||||
handleFileDownload,
|
handleFileDownload,
|
||||||
handleFileDelete,
|
handleFileDelete,
|
||||||
|
handleFileDeleteMultiple,
|
||||||
handleFileUpload,
|
handleFileUpload,
|
||||||
handleFileUpdate,
|
handleFileUpdate,
|
||||||
handleFilePreview,
|
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
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -15,7 +15,4 @@ html, body {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Import global button styles */
|
|
||||||
@import './styles/buttons.css';
|
|
||||||
|
|
@ -497,11 +497,10 @@ export default {
|
||||||
'users.add.create': 'Benutzer erstellen',
|
'users.add.create': 'Benutzer erstellen',
|
||||||
'users.delete.title': 'Benutzer löschen',
|
'users.delete.title': 'Benutzer löschen',
|
||||||
'users.delete.message': 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
|
'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.delete.warning': 'Diese Aktion kann nicht rückgängig gemacht werden.',
|
||||||
'users.action.edit': 'Bearbeiten',
|
'users.action.edit': 'Bearbeiten',
|
||||||
'users.action.delete': 'Löschen',
|
'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.delete.confirmMultiple': 'Sind Sie sicher, dass Sie {count} Benutzer löschen möchten?',
|
||||||
'users.error.loading': 'Fehler beim Laden der Benutzer:',
|
'users.error.loading': 'Fehler beim Laden der Benutzer:',
|
||||||
|
|
||||||
|
|
@ -561,8 +560,6 @@ export default {
|
||||||
'speech.info.about_link': 'Mehr erfahren',
|
'speech.info.about_link': 'Mehr erfahren',
|
||||||
|
|
||||||
'speech.signup.button': 'Verbinden',
|
'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.back': 'Zurück zur Sprach Integration',
|
||||||
'speech.signup.submit': 'Mandat erstellen',
|
'speech.signup.submit': 'Mandat erstellen',
|
||||||
'speech.signup.cancel': 'Abbrechen',
|
'speech.signup.cancel': 'Abbrechen',
|
||||||
|
|
@ -665,4 +662,34 @@ export default {
|
||||||
'speech.settings.reset_success': 'Einstellungen wurden erfolgreich zurückgesetzt.',
|
'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.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',
|
'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.add.create': 'Create User',
|
||||||
'users.delete.title': 'Delete User',
|
'users.delete.title': 'Delete User',
|
||||||
'users.delete.message': 'Are you sure you want to delete this 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.delete.warning': 'This action cannot be undone.',
|
||||||
'users.action.edit': 'Edit',
|
'users.action.edit': 'Edit',
|
||||||
'users.action.delete': 'Delete',
|
'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.delete.confirmMultiple': 'Are you sure you want to delete {count} users?',
|
||||||
'users.error.loading': 'Error loading users:',
|
'users.error.loading': 'Error loading users:',
|
||||||
|
|
||||||
|
|
@ -561,8 +560,6 @@ export default {
|
||||||
'speech.info.about_link': 'Learn more',
|
'speech.info.about_link': 'Learn more',
|
||||||
|
|
||||||
'speech.signup.button': 'Connect',
|
'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.back': 'Back to Speech Integration',
|
||||||
'speech.signup.submit': 'Create Mandate',
|
'speech.signup.submit': 'Create Mandate',
|
||||||
'speech.signup.cancel': 'Cancel',
|
'speech.signup.cancel': 'Cancel',
|
||||||
|
|
@ -665,4 +662,34 @@ export default {
|
||||||
'speech.settings.reset_success': 'Settings have been reset successfully.',
|
'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.no_data': 'No speech integration data found. Please sign up first to access settings.',
|
||||||
'speech.settings.sign_up_now': 'Sign Up Now',
|
'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.add.create': 'Créer l\'utilisateur',
|
||||||
'users.delete.title': 'Supprimer l\'utilisateur',
|
'users.delete.title': 'Supprimer l\'utilisateur',
|
||||||
'users.delete.message': 'Êtes-vous sûr de vouloir supprimer cet 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.delete.warning': 'Cette action ne peut pas être annulée.',
|
||||||
'users.action.edit': 'Modifier',
|
'users.action.edit': 'Modifier',
|
||||||
'users.action.delete': 'Supprimer',
|
'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.delete.confirmMultiple': 'Êtes-vous sûr de vouloir supprimer {count} utilisateurs ?',
|
||||||
'users.error.loading': 'Erreur lors du chargement des utilisateurs:',
|
'users.error.loading': 'Erreur lors du chargement des utilisateurs:',
|
||||||
|
|
||||||
|
|
@ -561,8 +560,6 @@ export default {
|
||||||
'speech.info.about_link': 'En savoir plus',
|
'speech.info.about_link': 'En savoir plus',
|
||||||
|
|
||||||
'speech.signup.button': 'Connecter',
|
'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.back': 'Retour à l\'Intégration Vocale',
|
||||||
'speech.signup.submit': 'Créer le Mandat',
|
'speech.signup.submit': 'Créer le Mandat',
|
||||||
'speech.signup.cancel': 'Annuler',
|
'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.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.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',
|
'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 { createRoot } from 'react-dom/client'
|
||||||
import App from './App.tsx'
|
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(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ import { useState } from 'react';
|
||||||
import { IoMdAdd } from 'react-icons/io';
|
import { IoMdAdd } from 'react-icons/io';
|
||||||
import { PromptsTable } from '../../components/Prompts';
|
import { PromptsTable } from '../../components/Prompts';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
import { Popup } from '../../components/Popup/Popup';
|
import { Popup } from '../../components/ui/Popup/Popup';
|
||||||
import { EditForm } from '../../components/Popup/EditForm';
|
import { EditForm } from '../../components/ui/Popup/EditForm';
|
||||||
import { usePrompts, usePromptOperations } from '../../hooks/usePrompts';
|
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';
|
import sharedStyles from '../../core/PageManager/pages.module.css';
|
||||||
|
|
||||||
function Prompts() {
|
function Prompts() {
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
|
@ -27,7 +27,7 @@
|
||||||
--button-warning-text: #212529;
|
--button-warning-text: #212529;
|
||||||
|
|
||||||
/* Button Sizes */
|
/* Button Sizes */
|
||||||
--button-sm-padding: 6px 12px;
|
--button-sm-padding: 8px 12px;
|
||||||
--button-sm-font-size: 12px;
|
--button-sm-font-size: 12px;
|
||||||
--button-sm-icon-size: 14px;
|
--button-sm-icon-size: 14px;
|
||||||
|
|
||||||
|
|
@ -202,6 +202,16 @@
|
||||||
display: none;
|
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 */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.buttonSm {
|
.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;
|
--color-gray-disabled: #505357;
|
||||||
|
|
||||||
--font-family: "Trebuchet MS", sans-serif;
|
--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;
|
--color-highlight-gray-disabled: #F5F3ED80;
|
||||||
|
|
||||||
--font-family: "DM Sans", sans-serif;
|
--font-family: "DM Sans", sans-serif;
|
||||||
|
--object-radius-large: 30px;
|
||||||
|
--object-radius-medium: 15px;
|
||||||
|
--object-radius-small: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme overrides */
|
/* Dark theme overrides */
|
||||||
Loading…
Reference in a new issue