worked on Dateien page
This commit is contained in:
parent
fa1cb5be2e
commit
7cbdb0aea5
41 changed files with 1679 additions and 2258 deletions
BIN
src/assets/styles/bg.jpg
Normal file
BIN
src/assets/styles/bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Popup, EditForm } from '../Popup';
|
||||
import styles from './ConnectionEditModal.module.css';
|
||||
import { ConnectionEditModalProps } from './interfaces';
|
||||
import { ConnectionEditModalProps } from './connectionsInterfaces';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export function ConnectionEditModal({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import styles from './ConnectionsErrorDisplay.module.css';
|
||||
import { ConnectionsErrorDisplayProps } from './interfaces';
|
||||
import { ConnectionsErrorDisplayProps } from './connectionsInterfaces';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export function ConnectionsErrorDisplay({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { FormGenerator } from '../FormGenerator';
|
||||
import styles from './ConnectionsTable.module.css';
|
||||
import { ConnectionsTableProps } from './interfaces';
|
||||
import { ConnectionsTableProps } from './connectionsInterfaces';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export function ConnectionsTable({
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
CreateConnectionData,
|
||||
ConnectionsLogicReturn,
|
||||
TableAction
|
||||
} from './interfaces';
|
||||
} from './connectionsInterfaces';
|
||||
|
||||
export function useConnectionsLogic(): ConnectionsLogicReturn {
|
||||
const { t } = useLanguage();
|
||||
|
|
@ -4,7 +4,7 @@ export { ConnectionEditModal } from './ConnectionEditModal';
|
|||
export { ConnectionsErrorDisplay } from './ConnectionsErrorDisplay';
|
||||
|
||||
// Export logic hook
|
||||
export { useConnectionsLogic } from './logic';
|
||||
export { useConnectionsLogic } from './connectionsLogic';
|
||||
|
||||
// Export all interfaces and types
|
||||
export * from './interfaces';
|
||||
export * from './connectionsInterfaces';
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import DateienItem from './DateienItem';
|
||||
import { UserFile } from '../../hooks/useFiles';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import styles from './DateienLists.module.css';
|
||||
|
||||
// Sort types
|
||||
type SortField = 'file_name' | 'action' | 'size' | 'created_at' | 'source';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
interface DateienAllProps {
|
||||
files: UserFile[];
|
||||
onFileDeleted: () => void;
|
||||
onOptimisticDelete?: (fileId: number) => void;
|
||||
}
|
||||
|
||||
const DateienAll: React.FC<DateienAllProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
|
||||
const { t } = useLanguage();
|
||||
const [sortField, setSortField] = useState<SortField>('created_at');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||
|
||||
// Handle sorting
|
||||
const handleSort = (field: SortField) => {
|
||||
if (field === sortField) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
|
||||
}
|
||||
};
|
||||
|
||||
// Sort all files
|
||||
const sortedFiles = [...files].sort((a, b) => {
|
||||
let result = 0;
|
||||
|
||||
switch (sortField) {
|
||||
case 'file_name':
|
||||
result = a.file_name.localeCompare(b.file_name);
|
||||
break;
|
||||
case 'action':
|
||||
result = a.action.localeCompare(b.action);
|
||||
break;
|
||||
case 'size':
|
||||
const sizeA = a.size ?? 0;
|
||||
const sizeB = b.size ?? 0;
|
||||
result = sizeA - sizeB;
|
||||
break;
|
||||
case 'created_at':
|
||||
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
||||
break;
|
||||
case 'source':
|
||||
result = a.source?.localeCompare(b.source ?? '') ?? 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return sortDirection === 'asc' ? result : -result;
|
||||
});
|
||||
|
||||
// Helper to render sort icon
|
||||
const renderSortIcon = (field: SortField) => {
|
||||
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
|
||||
return sortDirection === 'asc' ?
|
||||
<FaSortUp className={styles.sortIcon} /> :
|
||||
<FaSortDown className={styles.sortIcon} />;
|
||||
};
|
||||
|
||||
if (sortedFiles.length === 0) {
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.noFilesMessage}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<p>{t('files.no_files', 'No files found.')}</p>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.filesTable}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{/* Table Headers */}
|
||||
<div className={styles.tableHeader}>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
|
||||
<span>{t('files.header.name', 'Name')}</span>
|
||||
{renderSortIcon('file_name')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('action')}>
|
||||
<span>{t('files.header.type', 'Type')}</span>
|
||||
{renderSortIcon('action')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('size')}>
|
||||
<span>{t('files.header.size', 'Size')}</span>
|
||||
{renderSortIcon('size')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
|
||||
<span>{t('files.header.date', 'Date')}</span>
|
||||
{renderSortIcon('created_at')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Files List */}
|
||||
<motion.ul
|
||||
className={styles.filesList}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4, delay: 0.1 }}
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{sortedFiles.map((file: UserFile) => (
|
||||
<motion.div
|
||||
key={file.id}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.95,
|
||||
x: -50,
|
||||
transition: { duration: 0.2, ease: "easeIn" }
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
layout
|
||||
>
|
||||
<DateienItem
|
||||
file={file}
|
||||
onDelete={onFileDeleted}
|
||||
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.ul>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateienAll;
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import DateienItem from './DateienItem';
|
||||
import { UserFile } from '../../hooks/useFiles';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import styles from './DateienLists.module.css';
|
||||
|
||||
// Sort types
|
||||
type SortField = 'file_name' | 'action' | 'size' | 'created_at';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
interface DateienCreatedProps {
|
||||
files: UserFile[];
|
||||
onFileDeleted: () => void;
|
||||
onOptimisticDelete?: (fileId: number) => void;
|
||||
}
|
||||
|
||||
const DateienCreated: React.FC<DateienCreatedProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
|
||||
const { t } = useLanguage();
|
||||
const [sortField, setSortField] = useState<SortField>('created_at');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||
|
||||
// Filter files for created (agent_created)
|
||||
const createdFiles = files.filter(file => file.source === 'agent_created');
|
||||
|
||||
// Handle sorting
|
||||
const handleSort = (field: SortField) => {
|
||||
if (field === sortField) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
|
||||
}
|
||||
};
|
||||
|
||||
// Sort files
|
||||
const sortedFiles = [...createdFiles].sort((a, b) => {
|
||||
let result = 0;
|
||||
|
||||
switch (sortField) {
|
||||
case 'file_name':
|
||||
result = a.file_name.localeCompare(b.file_name);
|
||||
break;
|
||||
case 'action':
|
||||
result = a.action.localeCompare(b.action);
|
||||
break;
|
||||
case 'size':
|
||||
const sizeA = a.size ?? 0;
|
||||
const sizeB = b.size ?? 0;
|
||||
result = sizeA - sizeB;
|
||||
break;
|
||||
case 'created_at':
|
||||
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
||||
break;
|
||||
}
|
||||
|
||||
return sortDirection === 'asc' ? result : -result;
|
||||
});
|
||||
|
||||
// Helper to render sort icon
|
||||
const renderSortIcon = (field: SortField) => {
|
||||
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
|
||||
return sortDirection === 'asc' ?
|
||||
<FaSortUp className={styles.sortIcon} /> :
|
||||
<FaSortDown className={styles.sortIcon} />;
|
||||
};
|
||||
|
||||
if (sortedFiles.length === 0) {
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.noFilesMessage}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<p>{t('files.no_ai_files', 'No AI-created files found.')}</p>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.filesTable}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{/* Table Headers */}
|
||||
<div className={styles.tableHeader}>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
|
||||
<span>{t('files.header.name', 'Name')}</span>
|
||||
{renderSortIcon('file_name')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('action')}>
|
||||
<span>{t('files.header.type', 'Type')}</span>
|
||||
{renderSortIcon('action')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('size')}>
|
||||
<span>{t('files.header.size', 'Size')}</span>
|
||||
{renderSortIcon('size')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
|
||||
<span>{t('files.header.date', 'Date')}</span>
|
||||
{renderSortIcon('created_at')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Files List with AI-created indicator */}
|
||||
<motion.ul
|
||||
className={`${styles.filesList} ${styles.aiCreated}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4, delay: 0.1 }}
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{sortedFiles.map((file: UserFile) => (
|
||||
<motion.div
|
||||
key={file.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
layout
|
||||
>
|
||||
<DateienItem
|
||||
file={file}
|
||||
onDelete={onFileDeleted}
|
||||
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.ul>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateienCreated;
|
||||
|
|
@ -1,349 +0,0 @@
|
|||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--color-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
width: 900px;
|
||||
height: 700px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
background-color: var(--color-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: transparent;
|
||||
color: var(--color-gray);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
background-color: var(--color-gray-disabled);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabNavigation {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
background-color: var(--color-surface);
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.tabButton {
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-gray);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.tabButton:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tabButton.active {
|
||||
color: var(--color-secondary);
|
||||
border-bottom-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.actionBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.selectionControls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.selectAllButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.selectAllButton:hover {
|
||||
background-color: var(--color-surface);
|
||||
border-color: var(--color-gray);
|
||||
}
|
||||
|
||||
.selectionCount {
|
||||
font-size: 14px;
|
||||
color: var(--color-gray);
|
||||
font-weight: 500;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.uploadButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.uploadButton:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.fileListContainer {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.noFiles {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: var(--color-gray);
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.selectableFileList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selectableFileItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-bg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.selectableFileItem:hover {
|
||||
background-color: var(--color-surface);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.selectableFileItem.selected {
|
||||
background-color: var(--color-primary-disabled);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.fileCheckbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.checkedIcon {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.uncheckedIcon {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.fileIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.fileInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fileName {
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fileDetails {
|
||||
font-size: 12px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--color-gray-disabled);
|
||||
background-color: var(--color-bg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--color-red);
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cancelButton:hover {
|
||||
background-color: var(--color-red);
|
||||
border-color: var(--color-red);
|
||||
}
|
||||
|
||||
.confirmButton {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.confirmButton:hover:not(:disabled) {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.confirmButton:disabled {
|
||||
background-color: var(--color-secondary-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.modal {
|
||||
width: 95vw;
|
||||
height: 85vh;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.tabNavigation {
|
||||
padding: 0 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tabButton {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.actionBar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.selectionControls {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fileListContainer {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.selectableFileItem {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { IoClose, IoCheckbox, IoSquareOutline, IoCloudUploadOutline } from 'react-icons/io5';
|
||||
import { FaFile } from 'react-icons/fa';
|
||||
import { useUserFiles, UserFile, FileInfo } from '../../../hooks/useFiles';
|
||||
import DateienUploadTool from './DateienUploadTool';
|
||||
import DateienAll from '../DateienAll';
|
||||
import DateienUploads from '../DateienUploads';
|
||||
import DateienCreated from '../DateienCreated';
|
||||
import DateienShared from '../DateienShared';
|
||||
import { useLanguage } from '../../../contexts/LanguageContext';
|
||||
import styles from './DateienSelector.module.css';
|
||||
|
||||
type FileListType = 'all' | 'uploads' | 'created' | 'shared';
|
||||
|
||||
interface DateienSelectorProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onFilesSelected: (files: FileInfo[]) => void;
|
||||
allowMultiple?: boolean;
|
||||
}
|
||||
|
||||
const DateienSelector: React.FC<DateienSelectorProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onFilesSelected,
|
||||
allowMultiple = true
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
|
||||
const [activeTab, setActiveTab] = useState<FileListType>('all');
|
||||
const [showUploadTool, setShowUploadTool] = useState(false);
|
||||
const { files, loading, error, refetch } = useUserFiles();
|
||||
|
||||
// Filter files based on source
|
||||
const getFilteredFiles = (files: UserFile[], type: FileListType): UserFile[] => {
|
||||
switch (type) {
|
||||
case 'uploads':
|
||||
return files.filter(file => file.source === 'user_uploaded');
|
||||
case 'created':
|
||||
return files.filter(file => file.source === 'agent_created');
|
||||
case 'shared':
|
||||
return files.filter(file => file.source === 'shared_with_me');
|
||||
case 'all':
|
||||
default:
|
||||
return files;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredFiles = getFilteredFiles(files, activeTab);
|
||||
|
||||
// Reset selection when tab changes
|
||||
useEffect(() => {
|
||||
setSelectedFiles(new Set());
|
||||
}, [activeTab]);
|
||||
|
||||
// Reset state when modal closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setSelectedFiles(new Set());
|
||||
setShowUploadTool(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleFileSelect = (fileId: number) => {
|
||||
if (allowMultiple) {
|
||||
setSelectedFiles(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(fileId)) {
|
||||
newSet.delete(fileId);
|
||||
} else {
|
||||
newSet.add(fileId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
} else {
|
||||
setSelectedFiles(new Set([fileId]));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedFiles.size === filteredFiles.length) {
|
||||
setSelectedFiles(new Set());
|
||||
} else {
|
||||
setSelectedFiles(new Set(filteredFiles.map(file => file.id)));
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSelection = () => {
|
||||
const selectedFileObjects: FileInfo[] = files
|
||||
.filter(file => selectedFiles.has(file.id))
|
||||
.map(file => ({
|
||||
id: file.id,
|
||||
name: file.file_name,
|
||||
mimeType: deriveMimeTypeFromAction(file.action),
|
||||
size: file.size,
|
||||
creationDate: file.created_at,
|
||||
source: file.source
|
||||
}));
|
||||
|
||||
onFilesSelected(selectedFileObjects);
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Helper function to derive MIME type from action (reverse mapping)
|
||||
const deriveMimeTypeFromAction = (action: string): string => {
|
||||
switch (action.toLowerCase()) {
|
||||
case 'bild':
|
||||
return 'image/jpeg'; // Default image type
|
||||
case 'pdf':
|
||||
return 'application/pdf';
|
||||
case 'dokument':
|
||||
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
case 'tabelle':
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
case 'text':
|
||||
return 'text/plain';
|
||||
case 'video':
|
||||
return 'video/mp4'; // Default video type
|
||||
case 'audio':
|
||||
return 'audio/mpeg'; // Default audio type
|
||||
default:
|
||||
return 'application/octet-stream'; // Default binary type
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = (file: File) => {
|
||||
// Refresh file list after upload
|
||||
refetch();
|
||||
setShowUploadTool(false);
|
||||
};
|
||||
|
||||
const renderFileListComponent = () => {
|
||||
const commonProps = {
|
||||
files: filteredFiles,
|
||||
onFileDeleted: refetch
|
||||
};
|
||||
|
||||
switch (activeTab) {
|
||||
case 'uploads':
|
||||
return <DateienUploads {...commonProps} />;
|
||||
case 'created':
|
||||
return <DateienCreated {...commonProps} />;
|
||||
case 'shared':
|
||||
return <DateienShared {...commonProps} />;
|
||||
case 'all':
|
||||
default:
|
||||
return <DateienAll {...commonProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getTabLabel = (type: FileListType) => {
|
||||
const counts = {
|
||||
all: files.length,
|
||||
uploads: files.filter(f => f.source === 'user_uploaded').length,
|
||||
created: files.filter(f => f.source === 'agent_created').length,
|
||||
shared: files.filter(f => f.source === 'shared_with_me').length
|
||||
};
|
||||
|
||||
const labels = {
|
||||
all: t('files.selector.tab.all', 'All files'),
|
||||
uploads: t('files.selector.tab.uploads', 'Uploaded'),
|
||||
created: t('files.selector.tab.created', 'AI-created'),
|
||||
shared: t('files.selector.tab.shared', 'Shared')
|
||||
};
|
||||
|
||||
return `${labels[type]} (${counts[type]})`;
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
if (showUploadTool) {
|
||||
return (
|
||||
<DateienUploadTool
|
||||
isOpen={true}
|
||||
onClose={() => setShowUploadTool(false)}
|
||||
onFileUpload={handleFileUpload}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.overlay}>
|
||||
<motion.div
|
||||
className={styles.modal}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('files.selector.title', 'Select files')}</h2>
|
||||
<button className={styles.closeButton} onClick={onClose}>
|
||||
<IoClose />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
{/* Tab Navigation */}
|
||||
<div className={styles.tabNavigation}>
|
||||
{(['all', 'uploads', 'created', 'shared'] as FileListType[]).map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`${styles.tabButton} ${activeTab === tab ? styles.active : ''}`}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
>
|
||||
{getTabLabel(tab)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Action Bar */}
|
||||
<div className={styles.actionBar}>
|
||||
<div className={styles.selectionControls}>
|
||||
{allowMultiple && filteredFiles.length > 0 && (
|
||||
<button
|
||||
className={styles.selectAllButton}
|
||||
onClick={handleSelectAll}
|
||||
>
|
||||
{selectedFiles.size === filteredFiles.length ? (
|
||||
<>
|
||||
<IoCheckbox /> {t('files.selector.deselect_all', 'Deselect all')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IoSquareOutline /> {t('files.selector.select_all', 'Select all')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{selectedFiles.size > 0 && (
|
||||
<span className={styles.selectionCount}>
|
||||
{selectedFiles.size} {selectedFiles.size === 1 ? t('files.selector.file_selected', 'File') : t('files.selector.files_selected', 'Files')} {t('files.selector.selected_suffix', 'selected')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={styles.uploadButton}
|
||||
onClick={() => setShowUploadTool(true)}
|
||||
>
|
||||
<IoCloudUploadOutline />
|
||||
{t('files.selector.upload_new', 'Upload new file')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* File List */}
|
||||
<div className={styles.fileListContainer}>
|
||||
{loading ? (
|
||||
<div className={styles.loading}>{t('files.selector.loading', 'Loading files...')}</div>
|
||||
) : error ? (
|
||||
<div className={styles.error}>{t('files.selector.error_loading', 'Error loading files:')} {error}</div>
|
||||
) : filteredFiles.length === 0 ? (
|
||||
<div className={styles.noFiles}>{t('files.no_files', 'No files found.')}</div>
|
||||
) : (
|
||||
<div className={styles.selectableFileList}>
|
||||
{filteredFiles.map(file => (
|
||||
<div
|
||||
key={file.id}
|
||||
className={`${styles.selectableFileItem} ${
|
||||
selectedFiles.has(file.id) ? styles.selected : ''
|
||||
}`}
|
||||
onClick={() => handleFileSelect(file.id)}
|
||||
>
|
||||
<div className={styles.fileCheckbox}>
|
||||
{selectedFiles.has(file.id) ? (
|
||||
<IoCheckbox className={styles.checkedIcon} />
|
||||
) : (
|
||||
<IoSquareOutline className={styles.uncheckedIcon} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.fileIcon}>
|
||||
<FaFile />
|
||||
</div>
|
||||
<div className={styles.fileInfo}>
|
||||
<div className={styles.fileName}>{file.file_name}</div>
|
||||
<div className={styles.fileDetails}>
|
||||
{file.action} • {file.size ? `${Math.round(file.size / 1024)} KB` : t('files.unknown_size', 'Unknown Size')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className={styles.footer}>
|
||||
<button
|
||||
className={styles.cancelButton}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</button>
|
||||
<button
|
||||
className={styles.confirmButton}
|
||||
onClick={handleConfirmSelection}
|
||||
disabled={selectedFiles.size === 0}
|
||||
>
|
||||
{selectedFiles.size === 0
|
||||
? t('files.selector.title', 'Select files')
|
||||
: `${selectedFiles.size} ${selectedFiles.size === 1 ? t('files.selector.file_selected', 'File') : t('files.selector.files_selected', 'Files')} ${t('common.save', 'Save')}`
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateienSelector;
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--color-bg);
|
||||
padding: 35px 40px 30px 40px;
|
||||
border-radius: 30px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modalHeader h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-gray);
|
||||
padding: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.closeButton:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.uploadStatus {
|
||||
padding: 1rem;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.uploadStatus.success {
|
||||
background-color: var(--color-secondary-disabled);
|
||||
color: var(--color-secondary);
|
||||
border: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
.uploadStatus.error {
|
||||
background-color: var(--color-red-disabled);
|
||||
color: var(--color-red);
|
||||
border: 1px solid var(--color-red);
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
border: 2px dashed var(--color-gray-disabled);
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
margin: 1rem 0;
|
||||
transition: all 0.3s ease;
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.dropzone.active {
|
||||
border-color: var(--color-secondary);
|
||||
background-color: var(--color-secondary-disabled);
|
||||
}
|
||||
|
||||
.dropzone.uploading {
|
||||
border-color: var(--color-secondary);
|
||||
background-color: var(--color-secondary-disabled);
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.uploadIcon {
|
||||
font-size: 3rem;
|
||||
color: var(--color-gray);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dropzoneText {
|
||||
color: var(--color-gray);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.dropzoneText p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.browseButton {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.browseButton:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.browseButton:disabled {
|
||||
background-color: var(--color-gray-disabled);
|
||||
color: var(--color-gray);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.uploadButton {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.uploadButton:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.uploadButton:disabled {
|
||||
background-color: var(--color-gray-disabled);
|
||||
color: var(--color-gray);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import styles from './DateienUploadTool.module.css';
|
||||
import { IoCloudUploadOutline } from "react-icons/io5";
|
||||
import { IoClose } from "react-icons/io5";
|
||||
import { useFileOperations } from '../../../hooks/useFiles';
|
||||
import { useLanguage } from '../../../contexts/LanguageContext';
|
||||
|
||||
interface DateienUploadToolProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onFileUpload: (file: File) => void;
|
||||
}
|
||||
|
||||
function DateienUploadTool({ isOpen, onClose, onFileUpload }: DateienUploadToolProps) {
|
||||
const { t } = useLanguage();
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadStatus, setUploadStatus] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const { handleFileUpload, uploadError } = useFileOperations();
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
if (acceptedFiles.length > 0) {
|
||||
setSelectedFile(acceptedFiles[0]);
|
||||
// Clear previous upload status when selecting a new file
|
||||
setUploadStatus(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (selectedFile) {
|
||||
setIsUploading(true);
|
||||
setUploadStatus(null);
|
||||
|
||||
try {
|
||||
const result = await handleFileUpload(selectedFile);
|
||||
|
||||
if (result.success) {
|
||||
setUploadStatus({
|
||||
success: true,
|
||||
message: t('files.upload.success', 'File uploaded successfully!')
|
||||
});
|
||||
onFileUpload(selectedFile);
|
||||
setSelectedFile(null);
|
||||
// Close modal after brief success message
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 1500);
|
||||
} else {
|
||||
setUploadStatus({
|
||||
success: false,
|
||||
message: uploadError || t('files.upload.error', 'An error occurred while uploading.')
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setUploadStatus({
|
||||
success: false,
|
||||
message: t('files.upload.unexpected_error', 'An unexpected error occurred while uploading.')
|
||||
});
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2>{t('files.upload.title', 'Upload file')}</h2>
|
||||
<button className={styles.closeButton} onClick={onClose} disabled={isUploading}>
|
||||
<IoClose />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{uploadStatus && (
|
||||
<div className={`${styles.uploadStatus} ${uploadStatus.success ? styles.success : styles.error}`}>
|
||||
{uploadStatus.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`${styles.dropzone} ${isDragActive ? styles.active : ''} ${isUploading ? styles.uploading : ''}`}
|
||||
>
|
||||
<input {...getInputProps()} disabled={isUploading} />
|
||||
<IoCloudUploadOutline className={styles.uploadIcon} />
|
||||
{isDragActive ? (
|
||||
<p>{t('files.upload.drop_here', 'Drop file here...')}</p>
|
||||
) : isUploading ? (
|
||||
<p>{t('files.upload.uploading', 'Uploading...')}</p>
|
||||
) : (
|
||||
<div className={styles.dropzoneText}>
|
||||
<p>{t('files.upload.drag_files', 'Drag files here')}</p>
|
||||
<p>{t('files.upload.or', 'or')}</p>
|
||||
<button className={styles.browseButton} disabled={isUploading}>
|
||||
{t('files.upload.browse', 'Browse')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedFile && !isUploading && !uploadStatus?.success && (
|
||||
<div className={styles.selectedFile}>
|
||||
<p>{t('files.upload.selected_file', 'Selected file:')} {selectedFile.name}</p>
|
||||
<button
|
||||
className={styles.uploadButton}
|
||||
onClick={handleUpload}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{isUploading ? t('files.upload.uploading_button', 'Uploading...') : t('files.upload.upload_button', 'Upload')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DateienUploadTool;
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
.fileItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
padding: 0px 16px;
|
||||
justify-content: space-between;
|
||||
color: var(--color-text);
|
||||
transition: background-color 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.fileItem:hover {
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
/* Column layout matching the header structure */
|
||||
.fileName {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
padding-left: 14px; /* Align with table header */
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.fileName span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.fileType {
|
||||
font-size: 14px;
|
||||
color: var(--color-gray);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.fileSource {
|
||||
font-size: 12px;
|
||||
color: var(--color-gray-hover);
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.fileSize {
|
||||
font-size: 14px;
|
||||
color: var(--color-gray);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.fileDateWithActions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fileDate {
|
||||
font-size: 14px;
|
||||
color: var(--color-gray);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 8px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
color: var(--color-gray);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.downloadButton,
|
||||
.deleteButton,
|
||||
.previewButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
color: var(--color-gray);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 32px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.downloadButton:hover:not(:disabled),
|
||||
.deleteButton:hover:not(:disabled),
|
||||
.previewButton:hover:not(:disabled) {
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.deleteButton:hover:not(:disabled) {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.previewButton:hover:not(:disabled) {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.deleteButton.confirm {
|
||||
background-color: var(--color-red-disabled);
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.deleteButton.confirm:hover:not(:disabled) {
|
||||
background-color: var(--color-red-hover);
|
||||
}
|
||||
|
||||
.downloadButton:disabled,
|
||||
.deleteButton:disabled,
|
||||
.previewButton:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.downloadButton.downloading,
|
||||
.deleteButton.deleting {
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.actionText {
|
||||
font-size: 12px;
|
||||
color: var(--color-gray);
|
||||
animation: pulse 1.5s infinite;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.deleteButton.confirm .actionText {
|
||||
color: var(--color-red);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
import { FaFile, FaDownload, FaTrash } from "react-icons/fa";
|
||||
import { MdOutlineRemoveRedEye } from "react-icons/md";
|
||||
import styles from "./DateienItem.module.css";
|
||||
import { useState } from "react";
|
||||
import { useFileOperations } from "../../hooks/useFiles";
|
||||
import FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatAreaFilePreview";
|
||||
import { Document } from "../Dashboard/DashboardChat/dashboardChatAreaTypes";
|
||||
import { useLanguage } from "../../contexts/LanguageContext";
|
||||
|
||||
type DateienItemProps = {
|
||||
file: {
|
||||
id: number;
|
||||
file_name: string;
|
||||
action: string;
|
||||
created_at: string;
|
||||
size?: number;
|
||||
source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me'
|
||||
};
|
||||
onDelete?: () => void;
|
||||
onOptimisticDelete?: () => void; // New prop for immediate UI update
|
||||
};
|
||||
|
||||
const DateienItem = ({ file, onDelete, onOptimisticDelete }: DateienItemProps) => {
|
||||
const { t } = useLanguage();
|
||||
const { downloadingFiles, deletingFiles, handleFileDownload, handleFileDelete } = useFileOperations();
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
const isDownloading = downloadingFiles.has(file.id);
|
||||
const isDeleting = deletingFiles.has(file.id);
|
||||
|
||||
/**
|
||||
* Formats a file size in bytes to a human-readable string (KB, MB, etc.)
|
||||
*/
|
||||
const formatFileSize = (bytes?: number): string => {
|
||||
if (bytes === undefined || bytes === null) return t('files.unknown_size', 'Unknown Size');
|
||||
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
if (i === 0) return `${bytes} ${sizes[i]}`;
|
||||
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the file source for display
|
||||
*/
|
||||
const formatFileSource = (source?: string): string => {
|
||||
switch (source) {
|
||||
case 'user_uploaded':
|
||||
return t('files.source.uploaded', 'Uploaded');
|
||||
case 'agent_created':
|
||||
return t('files.source.ai_created', 'AI-created');
|
||||
case 'shared_with_me':
|
||||
return t('files.source.shared', 'Shared');
|
||||
default:
|
||||
return t('files.source.unknown', 'Unknown');
|
||||
}
|
||||
};
|
||||
|
||||
// Format the date properly
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
// Check if date is valid
|
||||
if (isNaN(date.getTime())) {
|
||||
return t('files.unknown_date', 'Unknown Date');
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error formatting date:', e);
|
||||
return t('files.unknown_date', 'Unknown Date');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = async () => {
|
||||
if (showDeleteConfirm) {
|
||||
const success = await handleFileDelete(file.id, onOptimisticDelete);
|
||||
if (!success) {
|
||||
// If deletion failed, refresh the file list to restore the file
|
||||
if (onDelete) {
|
||||
onDelete();
|
||||
}
|
||||
}
|
||||
setShowDeleteConfirm(false);
|
||||
} else {
|
||||
setShowDeleteConfirm(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelDelete = () => {
|
||||
setShowDeleteConfirm(false);
|
||||
};
|
||||
|
||||
const handlePreview = () => {
|
||||
// Split filename to get name and extension
|
||||
const nameParts = file.file_name.split('.');
|
||||
const extension = nameParts.length > 1 ? nameParts.pop() : undefined;
|
||||
const fileName = nameParts.join('.');
|
||||
|
||||
// Create a Document object compatible with FilePreviewPopup
|
||||
const document: Document = {
|
||||
id: String(file.id),
|
||||
fileId: file.id,
|
||||
name: fileName,
|
||||
ext: extension,
|
||||
size: file.size
|
||||
};
|
||||
|
||||
setPreviewDocument(document);
|
||||
setIsPreviewOpen(true);
|
||||
};
|
||||
|
||||
const handleClosePreview = () => {
|
||||
setIsPreviewOpen(false);
|
||||
setPreviewDocument(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<li>
|
||||
{/* 1st column: Name with icon */}
|
||||
<div className={styles.fileName}>
|
||||
<FaFile className={styles.icon} />
|
||||
<span>{file.file_name}</span>
|
||||
</div>
|
||||
|
||||
{/* 2nd column: Type with source */}
|
||||
<div className={styles.fileType}>
|
||||
<div>{file.action}</div>
|
||||
<div className={styles.fileSource}>{formatFileSource(file.source)}</div>
|
||||
</div>
|
||||
|
||||
{/* 3rd column: Size */}
|
||||
<div className={styles.fileSize}>
|
||||
{formatFileSize(file.size)}
|
||||
</div>
|
||||
|
||||
{/* 4th column: Date and action buttons */}
|
||||
<div className={styles.fileDateWithActions}>
|
||||
<span className={styles.fileDate}>
|
||||
{formatDate(file.created_at)}
|
||||
</span>
|
||||
|
||||
<div className={styles.actionButtons}>
|
||||
<button
|
||||
className={styles.previewButton}
|
||||
onClick={handlePreview}
|
||||
disabled={isDownloading || isDeleting}
|
||||
title={t('files.preview_tooltip', 'Preview file')}
|
||||
>
|
||||
<MdOutlineRemoveRedEye className={styles.actionIcon} />
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.downloadButton} ${isDownloading ? styles.downloading : ''}`}
|
||||
onClick={() => handleFileDownload(file.id, file.file_name)}
|
||||
disabled={isDownloading || isDeleting}
|
||||
title={t('files.download_tooltip', 'Download file')}
|
||||
>
|
||||
<FaDownload className={styles.actionIcon} />
|
||||
{isDownloading && <span className={styles.actionText}>{t('files.downloading', 'Downloading...')}</span>}
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.deleteButton} ${isDeleting ? styles.deleting : ''} ${showDeleteConfirm ? styles.confirm : ''}`}
|
||||
onClick={handleDeleteClick}
|
||||
disabled={isDownloading || isDeleting}
|
||||
title={showDeleteConfirm ? t('files.delete_confirm_tooltip', 'Click again to confirm deletion') : t('files.delete_tooltip', 'Delete file')}
|
||||
onBlur={handleCancelDelete}
|
||||
>
|
||||
<FaTrash className={styles.actionIcon} />
|
||||
{isDeleting && <span className={styles.actionText}>{t('files.deleting', 'Deleting...')}</span>}
|
||||
{showDeleteConfirm && <span className={styles.actionText}>{t('files.delete_confirm', 'Click to confirm...')}</span>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{/* File Preview Popup */}
|
||||
{previewDocument && (
|
||||
<FilePreviewPopup
|
||||
document={previewDocument}
|
||||
isOpen={isPreviewOpen}
|
||||
onClose={handleClosePreview}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateienItem;
|
||||
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
/* No files message */
|
||||
.noFilesMessage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--color-gray);
|
||||
font-style: italic;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
/* Files table container */
|
||||
.filesTable {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Table header with exact grid positioning */
|
||||
.tableHeader {
|
||||
display: grid;
|
||||
grid-template-columns: 45% 15% 15% 25%;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0px 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: var(--color-bg);
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
margin-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Header cells with exact positioning */
|
||||
.headerCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
padding-left: 0;
|
||||
transition: color 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.headerCell:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Adjust first column for icon space */
|
||||
.headerCell:nth-child(1) {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
/* Simple sort icon styling */
|
||||
.sortIcon {
|
||||
margin-left: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--color-gray);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.headerCell:hover .sortIcon {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* File list styling */
|
||||
.filesList {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Override the flex layout in DateienItem to force matching the header */
|
||||
.filesList li {
|
||||
display: grid !important;
|
||||
grid-template-columns: 45% 15% 15% 25%;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
height: 60px;
|
||||
padding: 0 16px;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filesList li:hover {
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import DateienItem from './DateienItem';
|
||||
import { UserFile } from '../../hooks/useFiles';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import styles from './DateienLists.module.css';
|
||||
|
||||
// Sort types
|
||||
type SortField = 'file_name' | 'action' | 'size' | 'created_at';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
interface DateienSharedProps {
|
||||
files: UserFile[];
|
||||
onFileDeleted: () => void;
|
||||
onOptimisticDelete?: (fileId: number) => void;
|
||||
}
|
||||
|
||||
const DateienShared: React.FC<DateienSharedProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
|
||||
const { t } = useLanguage();
|
||||
const [sortField, setSortField] = useState<SortField>('created_at');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||
|
||||
// Filter files for shared (shared_with_me)
|
||||
const sharedFiles = files.filter(file => file.source === 'shared_with_me');
|
||||
|
||||
// Handle sorting
|
||||
const handleSort = (field: SortField) => {
|
||||
if (field === sortField) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
|
||||
}
|
||||
};
|
||||
|
||||
// Sort files
|
||||
const sortedFiles = [...sharedFiles].sort((a, b) => {
|
||||
let result = 0;
|
||||
|
||||
switch (sortField) {
|
||||
case 'file_name':
|
||||
result = a.file_name.localeCompare(b.file_name);
|
||||
break;
|
||||
case 'action':
|
||||
result = a.action.localeCompare(b.action);
|
||||
break;
|
||||
case 'size':
|
||||
const sizeA = a.size ?? 0;
|
||||
const sizeB = b.size ?? 0;
|
||||
result = sizeA - sizeB;
|
||||
break;
|
||||
case 'created_at':
|
||||
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
||||
break;
|
||||
}
|
||||
|
||||
return sortDirection === 'asc' ? result : -result;
|
||||
});
|
||||
|
||||
// Helper to render sort icon
|
||||
const renderSortIcon = (field: SortField) => {
|
||||
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
|
||||
return sortDirection === 'asc' ?
|
||||
<FaSortUp className={styles.sortIcon} /> :
|
||||
<FaSortDown className={styles.sortIcon} />;
|
||||
};
|
||||
|
||||
if (sortedFiles.length === 0) {
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.noFilesMessage}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<p>{t('files.no_shared_files', 'No shared files found.')}</p>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.filesTable}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{/* Table Headers */}
|
||||
<div className={styles.tableHeader}>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
|
||||
<span>{t('files.header.name', 'Name')}</span>
|
||||
{renderSortIcon('file_name')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('action')}>
|
||||
<span>{t('files.header.type', 'Type')}</span>
|
||||
{renderSortIcon('action')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('size')}>
|
||||
<span>{t('files.header.size', 'Size')}</span>
|
||||
{renderSortIcon('size')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
|
||||
<span>{t('files.header.date', 'Date')}</span>
|
||||
{renderSortIcon('created_at')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Files List with shared indicator */}
|
||||
<motion.ul
|
||||
className={`${styles.filesList} ${styles.shared}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4, delay: 0.1 }}
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{sortedFiles.map((file: UserFile) => (
|
||||
<motion.div
|
||||
key={file.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
layout
|
||||
>
|
||||
<DateienItem
|
||||
file={file}
|
||||
onDelete={onFileDeleted}
|
||||
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.ul>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateienShared;
|
||||
249
src/components/Dateien/DateienTable.module.css
Normal file
249
src/components/Dateien/DateienTable.module.css
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
.dateienTable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dateienFormGenerator {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Error state styling */
|
||||
.errorState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--color-error, #dc3545);
|
||||
background-color: var(--color-error-bg, #f8d7da);
|
||||
border: 1px solid var(--color-error-border, #f5c6cb);
|
||||
border-radius: 8px;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.retryButton {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: var(--color-primary, #007bff);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.retryButton:hover {
|
||||
background-color: var(--color-primary, #0056b3);
|
||||
}
|
||||
|
||||
/* Table cell styling */
|
||||
.fileName {
|
||||
font-weight: 500;
|
||||
color: var(--color-text) !important;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fileTypeBadge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
text-align: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.type-bild,
|
||||
.type-image {
|
||||
background-color: #e3f2fd;
|
||||
color: #1565c0;
|
||||
border: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.type-pdf {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
border: 1px solid #ffcdd2;
|
||||
}
|
||||
|
||||
.type-dokument,
|
||||
.type-document {
|
||||
background-color: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
border: 1px solid #c8e6c9;
|
||||
}
|
||||
|
||||
.type-tabelle,
|
||||
.type-spreadsheet {
|
||||
background-color: #fff3e0;
|
||||
color: #ef6c00;
|
||||
border: 1px solid #ffe0b2;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
background-color: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
border: 1px solid #e1bee7;
|
||||
}
|
||||
|
||||
.type-video {
|
||||
background-color: #fce4ec;
|
||||
color: #ad1457;
|
||||
border: 1px solid #f8bbd9;
|
||||
}
|
||||
|
||||
.type-audio {
|
||||
background-color: #e0f2f1;
|
||||
color: #00695c;
|
||||
border: 1px solid #b2dfdb;
|
||||
}
|
||||
|
||||
.type-datei,
|
||||
.type-file {
|
||||
background-color: #f5f5f5;
|
||||
color: #495057;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.fileSize {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary, #666);
|
||||
background-color: var(--color-bg-secondary, #f8f9fa);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.sourceBadge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.source-user-uploaded {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.source-agent-created {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.source-shared-with-me {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.fileName {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.fileTypeBadge,
|
||||
.sourceBadge {
|
||||
font-size: 0.8em;
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
|
||||
.fileSize {
|
||||
font-size: 0.8em;
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.fileName {
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.fileSize {
|
||||
background-color: #374151;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.type-bild,
|
||||
.type-image {
|
||||
background-color: #1e3a8a;
|
||||
color: #bfdbfe;
|
||||
}
|
||||
|
||||
.type-pdf {
|
||||
background-color: #7f1d1d;
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.type-dokument,
|
||||
.type-document {
|
||||
background-color: #14532d;
|
||||
color: #bbf7d0;
|
||||
}
|
||||
|
||||
.type-tabelle,
|
||||
.type-spreadsheet {
|
||||
background-color: #9a3412;
|
||||
color: #fed7aa;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
background-color: #581c87;
|
||||
color: #e9d5ff;
|
||||
}
|
||||
|
||||
.type-video {
|
||||
background-color: #831843;
|
||||
color: #fbcfe8;
|
||||
}
|
||||
|
||||
.type-audio {
|
||||
background-color: #0f766e;
|
||||
color: #99f6e4;
|
||||
}
|
||||
|
||||
.type-datei,
|
||||
.type-file {
|
||||
background-color: #374151;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.source-user-uploaded {
|
||||
background-color: #14532d;
|
||||
color: #bbf7d0;
|
||||
}
|
||||
|
||||
.source-agent-created {
|
||||
background-color: #0c4a6e;
|
||||
color: #bae6fd;
|
||||
}
|
||||
|
||||
.source-shared-with-me {
|
||||
background-color: #92400e;
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
.errorState {
|
||||
background-color: #2d1b1e;
|
||||
border-color: #5c2b33;
|
||||
color: #f5c6cb;
|
||||
}
|
||||
}
|
||||
80
src/components/Dateien/DateienTable.tsx
Normal file
80
src/components/Dateien/DateienTable.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { FormGenerator } from '../FormGenerator';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { Popup, EditForm } from '../Popup';
|
||||
import styles from './DateienTable.module.css';
|
||||
import { useDateienLogic } from './dateienLogic.tsx';
|
||||
import type { DateienTableProps } from './dateienInterfaces';
|
||||
|
||||
export function DateienTable({ className = '' }: DateienTableProps) {
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Use the custom hook for all business logic
|
||||
const {
|
||||
files,
|
||||
loading,
|
||||
error,
|
||||
columns,
|
||||
actions,
|
||||
editModalOpen,
|
||||
editingFile,
|
||||
editFileFields,
|
||||
handleSaveFile,
|
||||
handleCancelEdit
|
||||
} = useDateienLogic();
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
return (
|
||||
<div className={`${styles.dateienTable} ${className}`}>
|
||||
<div className={styles.errorState}>
|
||||
<p>{t('files.error.loading')} {error}</p>
|
||||
<button onClick={() => window.location.reload()} className={styles.retryButton}>
|
||||
{t('files.button.retry')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.dateienTable} ${className}`}>
|
||||
<FormGenerator
|
||||
data={files}
|
||||
columns={columns}
|
||||
title={t('files.table.title')}
|
||||
loading={loading}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
resizable={true}
|
||||
pagination={true}
|
||||
pageSize={10}
|
||||
selectable={false}
|
||||
onRowClick={undefined}
|
||||
actions={actions}
|
||||
className={styles.dateienFormGenerator}
|
||||
/>
|
||||
|
||||
{/* Edit File Modal */}
|
||||
<Popup
|
||||
isOpen={editModalOpen}
|
||||
title={t('files.edit.title', 'Edit File')}
|
||||
onClose={handleCancelEdit}
|
||||
size="small"
|
||||
>
|
||||
{editingFile && (
|
||||
<EditForm
|
||||
data={editingFile}
|
||||
fields={editFileFields}
|
||||
onSave={handleSaveFile}
|
||||
onCancel={handleCancelEdit}
|
||||
saveButtonText={t('common.save', 'Save')}
|
||||
cancelButtonText={t('common.cancel', 'Cancel')}
|
||||
/>
|
||||
)}
|
||||
</Popup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DateienTable;
|
||||
0
src/components/Dateien/DateienUploadModal.module.css
Normal file
0
src/components/Dateien/DateienUploadModal.module.css
Normal file
0
src/components/Dateien/DateienUploadModal.tsx
Normal file
0
src/components/Dateien/DateienUploadModal.tsx
Normal file
|
|
@ -1,139 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import DateienItem from './DateienItem';
|
||||
import { UserFile } from '../../hooks/useFiles';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import styles from './DateienLists.module.css';
|
||||
|
||||
// Sort types
|
||||
type SortField = 'file_name' | 'action' | 'size' | 'created_at';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
interface DateienUploadsProps {
|
||||
files: UserFile[];
|
||||
onFileDeleted: () => void;
|
||||
onOptimisticDelete?: (fileId: number) => void;
|
||||
}
|
||||
|
||||
const DateienUploads: React.FC<DateienUploadsProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
|
||||
const { t } = useLanguage();
|
||||
const [sortField, setSortField] = useState<SortField>('created_at');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||
|
||||
// Filter files for uploads (user_uploaded)
|
||||
const uploadedFiles = files.filter(file => file.source === 'user_uploaded');
|
||||
|
||||
// Handle sorting
|
||||
const handleSort = (field: SortField) => {
|
||||
if (field === sortField) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
|
||||
}
|
||||
};
|
||||
|
||||
// Sort files
|
||||
const sortedFiles = [...uploadedFiles].sort((a, b) => {
|
||||
let result = 0;
|
||||
|
||||
switch (sortField) {
|
||||
case 'file_name':
|
||||
result = a.file_name.localeCompare(b.file_name);
|
||||
break;
|
||||
case 'action':
|
||||
result = a.action.localeCompare(b.action);
|
||||
break;
|
||||
case 'size':
|
||||
const sizeA = a.size ?? 0;
|
||||
const sizeB = b.size ?? 0;
|
||||
result = sizeA - sizeB;
|
||||
break;
|
||||
case 'created_at':
|
||||
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
||||
break;
|
||||
}
|
||||
|
||||
return sortDirection === 'asc' ? result : -result;
|
||||
});
|
||||
|
||||
// Helper to render sort icon
|
||||
const renderSortIcon = (field: SortField) => {
|
||||
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
|
||||
return sortDirection === 'asc' ?
|
||||
<FaSortUp className={styles.sortIcon} /> :
|
||||
<FaSortDown className={styles.sortIcon} />;
|
||||
};
|
||||
|
||||
if (sortedFiles.length === 0) {
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.noFilesMessage}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<p>{t('files.no_uploaded_files', 'No uploaded files found.')}</p>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.filesTable}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{/* Table Headers */}
|
||||
<div className={styles.tableHeader}>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
|
||||
<span>{t('files.header.name', 'Name')}</span>
|
||||
{renderSortIcon('file_name')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('action')}>
|
||||
<span>{t('files.header.type', 'Type')}</span>
|
||||
{renderSortIcon('action')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('size')}>
|
||||
<span>{t('files.header.size', 'Size')}</span>
|
||||
{renderSortIcon('size')}
|
||||
</div>
|
||||
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
|
||||
<span>{t('files.header.date', 'Date')}</span>
|
||||
{renderSortIcon('created_at')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Files List */}
|
||||
<motion.ul
|
||||
className={styles.filesList}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4, delay: 0.1 }}
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{sortedFiles.map((file: UserFile) => (
|
||||
<motion.div
|
||||
key={file.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
layout
|
||||
>
|
||||
<DateienItem
|
||||
file={file}
|
||||
onDelete={onFileDeleted}
|
||||
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.ul>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateienUploads;
|
||||
90
src/components/Dateien/dateienInterfaces.ts
Normal file
90
src/components/Dateien/dateienInterfaces.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { ColumnConfig } from '../FormGenerator';
|
||||
import React from 'react';
|
||||
import { EditFieldConfig } from '../Popup/EditForm';
|
||||
|
||||
// Re-export file-related interfaces from hooks
|
||||
export type { UserFile, FileInfo } from '../../hooks/useFiles';
|
||||
|
||||
// Import for local use
|
||||
import type { UserFile } from '../../hooks/useFiles';
|
||||
|
||||
// Component Props Interfaces
|
||||
export interface DateienTableProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Table Action Interface
|
||||
export interface TableAction {
|
||||
label: string;
|
||||
onClick: (file: UserFile) => Promise<void> | void;
|
||||
icon: React.ReactNode | ((file: UserFile) => React.ReactNode);
|
||||
}
|
||||
|
||||
// File Operation Handler Types
|
||||
export interface FileHandlers {
|
||||
handleFileDownload: (fileId: string, fileName: string) => Promise<boolean>;
|
||||
handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>;
|
||||
handleFileUpload: (file: globalThis.File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>;
|
||||
handleFileUpdate: (fileId: string, updateData: Partial<{ filename: string }>) => Promise<{ success: boolean; fileData?: any; error?: string }>;
|
||||
}
|
||||
|
||||
// Hook Return Types for File Operations
|
||||
export interface FileOperationsReturn extends FileHandlers {
|
||||
downloadingFiles: Set<string>;
|
||||
deletingFiles: Set<string>;
|
||||
uploadingFile: boolean;
|
||||
downloadError: string | null;
|
||||
deleteError: string | null;
|
||||
uploadError: string | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
// Hook Return Types for User Files
|
||||
export interface UserFilesReturn {
|
||||
files: UserFile[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
removeFileOptimistically: (fileId: string) => void;
|
||||
addFileOptimistically: (newFile: UserFile) => void;
|
||||
}
|
||||
|
||||
// File Table Configuration
|
||||
export interface FileTableConfig {
|
||||
columns: ColumnConfig[];
|
||||
actions: TableAction[];
|
||||
pageSize: number;
|
||||
searchable: boolean;
|
||||
filterable: boolean;
|
||||
sortable: boolean;
|
||||
resizable: boolean;
|
||||
pagination: boolean;
|
||||
}
|
||||
|
||||
// File Size Formatter Function Type
|
||||
export type FileSizeFormatter = (sizeInBytes?: number) => string;
|
||||
|
||||
// Date Formatter Function Type
|
||||
export type DateFormatter = (value?: string) => string;
|
||||
|
||||
// Hook Return Type for Dateien Logic
|
||||
export interface DateienLogicReturn {
|
||||
files: UserFile[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
columns: ColumnConfig[];
|
||||
actions: TableAction[];
|
||||
downloadingFiles: Set<string>;
|
||||
deletingFiles: Set<string>;
|
||||
downloadError: string | null;
|
||||
deleteError: string | null;
|
||||
editModalOpen: boolean;
|
||||
editingFile: UserFile | null;
|
||||
editFileFields: EditFieldConfig[];
|
||||
handleEditFile: (file: UserFile) => void;
|
||||
handleSaveFile: (updatedFile: UserFile) => Promise<void>;
|
||||
handleCancelEdit: () => void;
|
||||
}
|
||||
|
||||
|
||||
274
src/components/Dateien/dateienLogic.tsx
Normal file
274
src/components/Dateien/dateienLogic.tsx
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { IoIosTrash, IoIosDownload } from 'react-icons/io';
|
||||
import { MdModeEdit } from 'react-icons/md';
|
||||
|
||||
import { ColumnConfig } from '../FormGenerator';
|
||||
import { useUserFiles, useFileOperations } from '../../hooks/useFiles';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { EditFieldConfig } from '../Popup/EditForm';
|
||||
import type {
|
||||
TableAction,
|
||||
FileSizeFormatter,
|
||||
DateFormatter,
|
||||
UserFile,
|
||||
DateienLogicReturn
|
||||
} from './dateienInterfaces';
|
||||
|
||||
export function useDateienLogic(): DateienLogicReturn {
|
||||
const { files, loading, error, refetch } = useUserFiles();
|
||||
const {
|
||||
handleFileDownload,
|
||||
handleFileDelete,
|
||||
handleFileUpdate,
|
||||
downloadingFiles,
|
||||
deletingFiles,
|
||||
downloadError,
|
||||
deleteError
|
||||
} = useFileOperations();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Edit modal state
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [editingFile, setEditingFile] = useState<UserFile | null>(null);
|
||||
|
||||
// Configure edit fields for filename editing
|
||||
const editFileFields: EditFieldConfig[] = useMemo(() => [
|
||||
{
|
||||
key: 'file_name',
|
||||
label: t('files.field.filename', 'Filename'),
|
||||
type: 'string',
|
||||
editable: true,
|
||||
required: true,
|
||||
validator: (value: string) => {
|
||||
if (!value || value.trim() === '') {
|
||||
return 'Filename cannot be empty';
|
||||
}
|
||||
if (value.includes('/') || value.includes('\\')) {
|
||||
return 'Filename cannot contain / or \\ characters';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
], [t]);
|
||||
|
||||
// Handle edit file
|
||||
const handleEditFile = (file: UserFile) => {
|
||||
setEditingFile(file);
|
||||
setEditModalOpen(true);
|
||||
};
|
||||
|
||||
// Handle save file
|
||||
const handleSaveFile = async (updatedFile: UserFile) => {
|
||||
if (!editingFile) return;
|
||||
|
||||
try {
|
||||
// Call API to update filename
|
||||
const result = await handleFileUpdate(editingFile.id, {
|
||||
filename: updatedFile.file_name
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// Close modal
|
||||
setEditModalOpen(false);
|
||||
setEditingFile(null);
|
||||
|
||||
// Refresh file list
|
||||
await refetch();
|
||||
} else {
|
||||
console.error('Failed to update file:', result.error);
|
||||
// TODO: Show error message to user
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update file:', error);
|
||||
// TODO: Show error message to user
|
||||
}
|
||||
};
|
||||
|
||||
// Handle cancel edit
|
||||
const handleCancelEdit = () => {
|
||||
setEditModalOpen(false);
|
||||
setEditingFile(null);
|
||||
};
|
||||
|
||||
// Helper function to format file size
|
||||
const formatFileSize: FileSizeFormatter = (sizeInBytes) => {
|
||||
if (!sizeInBytes || sizeInBytes === 0) return '-';
|
||||
|
||||
const units = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
let size = sizeInBytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
// Helper function to format date
|
||||
const formatDate: DateFormatter = (value) => {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
const date = new Date(value);
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = pad(date.getMonth() + 1);
|
||||
const dd = pad(date.getDate());
|
||||
const hh = pad(date.getHours());
|
||||
const mi = pad(date.getMinutes());
|
||||
const ss = pad(date.getSeconds());
|
||||
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
// Configure columns for the files table
|
||||
const columns: ColumnConfig[] = useMemo(() => [
|
||||
{
|
||||
key: 'file_name',
|
||||
label: 'Filename',
|
||||
type: 'string',
|
||||
width: 300,
|
||||
minWidth: 200,
|
||||
maxWidth: 400,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
formatter: (value: string) => (
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--color-text)',
|
||||
fontWeight: 500,
|
||||
display: 'block',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
paddingLeft: '8px'
|
||||
}}
|
||||
title={value}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'mime_type',
|
||||
label: 'mime type',
|
||||
type: 'string',
|
||||
width: 200,
|
||||
minWidth: 150,
|
||||
maxWidth: 300,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
key: 'size',
|
||||
label: 'filesize',
|
||||
type: 'number',
|
||||
width: 140,
|
||||
minWidth: 120,
|
||||
maxWidth: 180,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
formatter: (value: number | string | undefined) => (
|
||||
<span style={{ fontWeight: 500, color: 'var(--color-text)' }}>
|
||||
{formatFileSize(typeof value === 'string' ? parseInt(value, 10) : value)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
label: 'creation date',
|
||||
type: 'date',
|
||||
width: 200,
|
||||
minWidth: 180,
|
||||
maxWidth: 240,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
formatter: (value: string | undefined) => formatDate(value)
|
||||
},
|
||||
], []);
|
||||
|
||||
// Handle file download
|
||||
const handleDownload = async (file: UserFile) => {
|
||||
const success = await handleFileDownload(file.id, file.file_name);
|
||||
if (!success && downloadError) {
|
||||
console.error('Download failed:', downloadError);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle file deletion
|
||||
const handleDelete = async (file: UserFile) => {
|
||||
if (window.confirm(t('files.delete.confirm').replace('{name}', file.file_name))) {
|
||||
const success = await handleFileDelete(file.id, () => {
|
||||
// Optimistic update - this will be called immediately
|
||||
refetch();
|
||||
});
|
||||
|
||||
if (!success && deleteError) {
|
||||
console.error('Delete failed:', deleteError);
|
||||
// Refetch to restore the file in case of failure
|
||||
refetch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Configure action buttons
|
||||
const actions: TableAction[] = useMemo(() => [
|
||||
{
|
||||
label: t('files.action.edit', 'Edit'),
|
||||
icon: <MdModeEdit />,
|
||||
onClick: (row: UserFile) => {
|
||||
handleEditFile(row);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('files.action.download'),
|
||||
icon: (row: UserFile) => {
|
||||
const isDownloadingThis = downloadingFiles.has(row.id);
|
||||
if (isDownloadingThis) return '⏳';
|
||||
return <IoIosDownload />;
|
||||
},
|
||||
onClick: (row: UserFile) => {
|
||||
if (!downloadingFiles.has(row.id)) {
|
||||
handleDownload(row);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('files.action.delete'),
|
||||
icon: (row: UserFile) => {
|
||||
const isDeletingThis = deletingFiles.has(row.id);
|
||||
if (isDeletingThis) return '⏳';
|
||||
return <IoIosTrash />;
|
||||
},
|
||||
onClick: (row: UserFile) => {
|
||||
if (!deletingFiles.has(row.id)) {
|
||||
handleDelete(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
], [t, downloadingFiles, deletingFiles, handleDownload, handleDelete]);
|
||||
|
||||
return {
|
||||
files,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
columns,
|
||||
actions,
|
||||
downloadingFiles,
|
||||
deletingFiles,
|
||||
downloadError,
|
||||
deleteError,
|
||||
editModalOpen,
|
||||
editingFile,
|
||||
editFileFields,
|
||||
handleEditFile,
|
||||
handleSaveFile,
|
||||
handleCancelEdit
|
||||
};
|
||||
}
|
||||
3
src/components/Dateien/index.ts
Normal file
3
src/components/Dateien/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { default as DateienTable } from './DateienTable';
|
||||
export { useDateienLogic } from './dateienLogic.tsx';
|
||||
export * from './dateienInterfaces';
|
||||
|
|
@ -328,35 +328,45 @@
|
|||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Actions Column */
|
||||
/* Actions Column - Resizable like other columns */
|
||||
.actionsColumn {
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
padding: 12px 8px !important;
|
||||
text-align: left;
|
||||
padding: 8px !important;
|
||||
font-weight: 400;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Actions Column header */
|
||||
thead .actionsColumn {
|
||||
text-align: center;
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Actions Column border only on body cells, not header */
|
||||
tbody .actionsColumn {
|
||||
border-top: 1px solid var(--color-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
padding: 6px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
|
|
@ -365,16 +375,14 @@ tbody .actionsColumn {
|
|||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
.actionButton:hover {
|
||||
background: var(--color-secondary-hover);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
|
|
@ -386,43 +394,6 @@ tbody .actionsColumn {
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Custom Tooltip */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
bottom: 120%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-text);
|
||||
color: var(--color-bg);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Tooltip arrow */
|
||||
.tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Show tooltip on button hover */
|
||||
.actionButton:hover .tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
display: flex;
|
||||
|
|
@ -575,4 +546,30 @@ tbody .actionsColumn {
|
|||
|
||||
.tableContainer::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-secondary);
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loadingState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary, #666);
|
||||
}
|
||||
|
||||
.loadingSpinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid var(--color-bg-secondary, #e9ecef);
|
||||
border-top: 3px solid var(--color-primary, #007bff);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import styles from './FormGenerator.module.css';
|
||||
|
||||
// Types for the FormGenerator
|
||||
|
|
@ -55,6 +56,7 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
actions = [],
|
||||
className = ''
|
||||
}: FormGeneratorProps<T>) {
|
||||
const { t } = useLanguage();
|
||||
// Auto-detect columns if not provided
|
||||
const detectedColumns = useMemo((): ColumnConfig[] => {
|
||||
if (providedColumns) return providedColumns;
|
||||
|
|
@ -108,12 +110,18 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
// Initialize column widths
|
||||
useEffect(() => {
|
||||
const initialWidths: Record<string, number> = {};
|
||||
|
||||
// Add actions column if present
|
||||
if (actions.length > 0) {
|
||||
initialWidths['actions'] = 120; // Default width for actions column
|
||||
}
|
||||
|
||||
detectedColumns.forEach(col => {
|
||||
// Set a default width if none specified to ensure all columns have explicit widths
|
||||
initialWidths[col.key] = col.width || 150;
|
||||
});
|
||||
setColumnWidths(initialWidths);
|
||||
}, [detectedColumns]);
|
||||
}, [detectedColumns, actions]);
|
||||
|
||||
// Filter and search data
|
||||
const filteredData = useMemo(() => {
|
||||
|
|
@ -143,15 +151,6 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
} else if (column?.type === 'number') {
|
||||
return Number(value) === Number(filterValue);
|
||||
} else if (column?.type === 'date') {
|
||||
// Convert DD.MM.YYYY to comparable format
|
||||
const parseDate = (dateStr: string) => {
|
||||
if (dateStr.includes('.')) {
|
||||
const [day, month, year] = dateStr.split('.');
|
||||
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||
}
|
||||
return new Date(dateStr);
|
||||
};
|
||||
|
||||
// Convert row value to DD.MM.YYYY format for comparison
|
||||
const rowDate = new Date(value);
|
||||
const rowFormatted = `${rowDate.getDate().toString().padStart(2, '0')}.${(rowDate.getMonth() + 1).toString().padStart(2, '0')}.${rowDate.getFullYear()}`;
|
||||
|
|
@ -281,7 +280,7 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
const tableContainer = tableRef.current?.parentElement;
|
||||
if (tableContainer) {
|
||||
const containerWidth = tableContainer.clientWidth;
|
||||
const actionsColumnWidth = actions.length > 0 ? 120 : 0;
|
||||
const actionsColumnWidth = 0; // Actions column is now resizable like other columns
|
||||
const selectColumnWidth = selectable ? 40 : 0;
|
||||
const fixedWidth = actionsColumnWidth + selectColumnWidth;
|
||||
|
||||
|
|
@ -334,6 +333,8 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
|
||||
return (
|
||||
<div className={`${styles.formGenerator} ${className}`}>
|
||||
|
||||
|
||||
{(searchable || filterable) && (
|
||||
<div className={styles.controls}>
|
||||
{searchable && (
|
||||
|
|
@ -348,7 +349,7 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
onBlur={() => setSearchFocused(false)}
|
||||
className={`${styles.searchInput} ${searchFocused || searchTerm ? styles.focused : ''}`}
|
||||
/>
|
||||
<label className={searchFocused || searchTerm ? styles.focusedLabel : styles.label}>Search...</label>
|
||||
<label className={searchFocused || searchTerm ? styles.focusedLabel : styles.label}>{t('formgen.search.placeholder')}</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -365,15 +366,15 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
className={`${styles.filterSelect} ${filters[column.key] ? styles.hasValue : ''}`}
|
||||
>
|
||||
<option value="" disabled hidden>{column.label}</option>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
<option value="true">{t('formgen.filter.yes')}</option>
|
||||
<option value="false">{t('formgen.filter.no')}</option>
|
||||
</select>
|
||||
{filters[column.key] && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleFilter(column.key, '')}
|
||||
className={styles.clearFilterButton}
|
||||
title="Clear filter"
|
||||
title={t('formgen.filter.clear')}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
|
@ -397,7 +398,7 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
type="button"
|
||||
onClick={() => handleFilter(column.key, '')}
|
||||
className={styles.clearFilterButton}
|
||||
title="Clear filter"
|
||||
title={t('formgen.filter.clear')}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
|
@ -490,7 +491,7 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
className={`${styles.filterInput} ${filterFocused[column.key] || filters[column.key] ? styles.focused : ''}`}
|
||||
/>
|
||||
<label className={filterFocused[column.key] || filters[column.key] ? styles.focusedLabel : styles.label}>
|
||||
Filter {column.label}
|
||||
{t('formgen.filter.placeholder').replace('{column}', column.label)}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -503,10 +504,33 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
|
||||
{/* Table */}
|
||||
<div className={styles.tableContainer}>
|
||||
{(
|
||||
{loading ? (
|
||||
<div className={styles.loadingState}>
|
||||
<div className={styles.loadingSpinner}></div>
|
||||
<p>{t('common.loading', 'Loading...')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<table ref={tableRef} className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
{actions.length > 0 && (
|
||||
<th
|
||||
className={`${styles.actionsColumn} ${styles.th} ${resizable ? styles.sortable : ''}`}
|
||||
style={{
|
||||
width: columnWidths['actions'] || 120,
|
||||
minWidth: columnWidths['actions'] || 120,
|
||||
maxWidth: columnWidths['actions'] || 120
|
||||
}}
|
||||
>
|
||||
|
||||
{resizable && (
|
||||
<div
|
||||
className={styles.resizeHandle}
|
||||
onMouseDown={(e) => handleMouseDown(e, 'actions')}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
)}
|
||||
{selectable && (
|
||||
<th className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
|
||||
<input
|
||||
|
|
@ -516,12 +540,6 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
/>
|
||||
</th>
|
||||
)}
|
||||
{actions.length > 0 && (
|
||||
|
||||
<th className={styles.actionsColumn} style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}>
|
||||
Actions
|
||||
</th>
|
||||
)}
|
||||
{detectedColumns.map(column => (
|
||||
<th
|
||||
key={column.key}
|
||||
|
|
@ -560,18 +578,15 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
className={`${styles.tr} ${selectedRows.has(index) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
|
||||
onClick={() => onRowClick?.(row, index)}
|
||||
>
|
||||
{selectable && (
|
||||
<td className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRows.has(index)}
|
||||
onChange={() => handleRowSelect(index)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{actions.length > 0 && (
|
||||
<td className={styles.actionsColumn} style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}>
|
||||
{actions.length > 0 && (
|
||||
<td
|
||||
className={styles.actionsColumn}
|
||||
style={{
|
||||
width: columnWidths['actions'] || 120,
|
||||
minWidth: columnWidths['actions'] || 120,
|
||||
maxWidth: columnWidths['actions'] || 120
|
||||
}}
|
||||
>
|
||||
<div className={styles.actionButtons}>
|
||||
{actions.map((action, actionIndex) => (
|
||||
<button
|
||||
|
|
@ -588,12 +603,21 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
{typeof action.icon === 'function' ? action.icon(row) : action.icon}
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.tooltip}>{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
{selectable && (
|
||||
<td className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRows.has(index)}
|
||||
onChange={() => handleRowSelect(index)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{detectedColumns.map(column => (
|
||||
<td
|
||||
key={column.key}
|
||||
|
|
@ -622,6 +646,7 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
className={styles.paginationButton}
|
||||
title={t('formgen.pagination.first')}
|
||||
>
|
||||
««
|
||||
</button>
|
||||
|
|
@ -629,18 +654,23 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className={styles.paginationButton}
|
||||
title={t('formgen.pagination.prev')}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
|
||||
<span className={styles.paginationInfo}>
|
||||
Page {currentPage} of {totalPages} ({filteredData.length} items)
|
||||
{t('formgen.pagination.info')
|
||||
.replace('{page}', currentPage.toString())
|
||||
.replace('{total}', totalPages.toString())
|
||||
.replace('{count}', filteredData.length.toString())}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className={styles.paginationButton}
|
||||
title={t('formgen.pagination.next')}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
|
|
@ -648,6 +678,7 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
className={styles.paginationButton}
|
||||
title={t('formgen.pagination.last')}
|
||||
>
|
||||
»»
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
.sidebarContainer {
|
||||
border-radius: 0px;
|
||||
background: var(--color-bg);
|
||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
||||
/*background-image: url('../../../assets/styles/bg.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);*/
|
||||
width: 240px;
|
||||
padding-bottom: 1px;
|
||||
display: flex;
|
||||
|
|
|
|||
189
src/components/Workflows/WorkflowsTable.module.css
Normal file
189
src/components/Workflows/WorkflowsTable.module.css
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
.workflowsTable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.workflowsFormGenerator {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Error state styling */
|
||||
.errorState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--color-error, #dc3545);
|
||||
background-color: var(--color-error-bg, #f8d7da);
|
||||
border: 1px solid var(--color-error-border, #f5c6cb);
|
||||
border-radius: 8px;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.retryButton {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: var(--color-primary, #007bff);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.retryButton:hover {
|
||||
background-color: var(--color-primary-dark, #0056b3);
|
||||
}
|
||||
|
||||
/* Table cell styling */
|
||||
.workflowId {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--color-text-secondary, #666);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.workflowName {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary, #333);
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
text-align: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.status-stopped {
|
||||
background-color: #f5f5f5;
|
||||
color: #495057;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.roundNumber {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary, #007bff);
|
||||
background-color: var(--color-primary-light, #e3f2fd);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9em;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.messageCount {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary, #666);
|
||||
background-color: var(--color-bg-secondary, #f8f9fa);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9em;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.workflowId {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
font-size: 0.8em;
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
|
||||
.roundNumber,
|
||||
.messageCount {
|
||||
font-size: 0.8em;
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.workflowId {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.workflowName {
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background-color: #155724;
|
||||
color: #d4edda;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #0c5460;
|
||||
color: #d1ecf1;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: #721c24;
|
||||
color: #f8d7da;
|
||||
}
|
||||
|
||||
.status-stopped {
|
||||
background-color: #495057;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #856404;
|
||||
color: #fff3cd;
|
||||
}
|
||||
|
||||
.roundNumber {
|
||||
background-color: #1e3a8a;
|
||||
color: #bfdbfe;
|
||||
}
|
||||
|
||||
.messageCount {
|
||||
background-color: #374151;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.errorState {
|
||||
background-color: #2d1b1e;
|
||||
border-color: #5c2b33;
|
||||
color: #f5c6cb;
|
||||
}
|
||||
}
|
||||
227
src/components/Workflows/WorkflowsTable.tsx
Normal file
227
src/components/Workflows/WorkflowsTable.tsx
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import { useMemo } from 'react';
|
||||
import { FormGenerator, ColumnConfig } from '../FormGenerator/FormGenerator';
|
||||
import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import styles from './WorkflowsTable.module.css';
|
||||
|
||||
interface WorkflowsTableProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
|
||||
const { workflows, loading, error, refetch } = useWorkflows();
|
||||
const {
|
||||
stopWorkflow,
|
||||
deleteWorkflow,
|
||||
stoppingWorkflows,
|
||||
deletingWorkflows
|
||||
} = useWorkflowOperations();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Configure columns for the workflows table
|
||||
const columns: ColumnConfig[] = useMemo(() => [
|
||||
{
|
||||
key: 'id',
|
||||
label: t('workflows.column.id'),
|
||||
type: 'string',
|
||||
width: 180,
|
||||
minWidth: 150,
|
||||
maxWidth: 250,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
formatter: (value: string) => (
|
||||
<span className={styles.workflowId} title={value}>
|
||||
{value.length > 8 ? `${value.substring(0, 8)}...` : value}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: t('workflows.column.name'),
|
||||
type: 'string',
|
||||
width: 200,
|
||||
minWidth: 150,
|
||||
maxWidth: 300,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
formatter: (value: string | undefined, row: Workflow) => (
|
||||
<span className={styles.workflowName}>
|
||||
{value || row.title || t('workflows.unnamed')}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: t('workflows.column.status'),
|
||||
type: 'enum',
|
||||
width: 120,
|
||||
minWidth: 100,
|
||||
maxWidth: 150,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterOptions: ['running', 'completed', 'failed', 'stopped', 'pending'],
|
||||
formatter: (value: string) => (
|
||||
<span className={`${styles.statusBadge} ${styles[`status-${value}`]}`}>
|
||||
{t(`workflows.status.${value}`, value)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'currentRound',
|
||||
label: t('workflows.column.round'),
|
||||
type: 'number',
|
||||
width: 80,
|
||||
minWidth: 60,
|
||||
maxWidth: 100,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
formatter: (value: number | undefined) => (
|
||||
<span className={styles.roundNumber}>
|
||||
{value || 1}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'startedAt',
|
||||
label: t('workflows.column.started'),
|
||||
type: 'date',
|
||||
width: 140,
|
||||
minWidth: 120,
|
||||
maxWidth: 180,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
formatter: (value: string | undefined) => {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'lastActivity',
|
||||
label: t('workflows.column.lastActivity'),
|
||||
type: 'date',
|
||||
width: 140,
|
||||
minWidth: 120,
|
||||
maxWidth: 180,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
formatter: (value: string | undefined) => {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'messages',
|
||||
label: t('workflows.column.messages'),
|
||||
type: 'number',
|
||||
width: 100,
|
||||
minWidth: 80,
|
||||
maxWidth: 120,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
formatter: (value: any[] | undefined) => (
|
||||
<span className={styles.messageCount}>
|
||||
{value?.length || 0}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
], [t]);
|
||||
|
||||
// Handle workflow actions
|
||||
const handleStopWorkflow = async (workflow: Workflow) => {
|
||||
const success = await stopWorkflow(workflow.id);
|
||||
if (success) {
|
||||
refetch(); // Refresh the workflows list
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteWorkflow = async (workflow: Workflow) => {
|
||||
const workflowName = workflow.name || workflow.title || workflow.id;
|
||||
if (window.confirm(t('workflows.delete.confirm').replace('{name}', workflowName))) {
|
||||
const success = await deleteWorkflow(workflow.id);
|
||||
if (success) {
|
||||
refetch(); // Refresh the workflows list
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Configure action buttons
|
||||
const actions = useMemo(() => [
|
||||
{
|
||||
label: t('workflows.action.stop'),
|
||||
icon: (row: Workflow) => {
|
||||
const isStoppingThis = stoppingWorkflows.has(row.id);
|
||||
if (isStoppingThis) return '⏳';
|
||||
return '⏹️';
|
||||
},
|
||||
onClick: (row: Workflow) => {
|
||||
if (row.status === 'running' && !stoppingWorkflows.has(row.id)) {
|
||||
handleStopWorkflow(row);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('workflows.action.delete'),
|
||||
icon: (row: Workflow) => {
|
||||
const isDeletingThis = deletingWorkflows.has(row.id);
|
||||
if (isDeletingThis) return '⏳';
|
||||
return '🗑️';
|
||||
},
|
||||
onClick: (row: Workflow) => {
|
||||
if (!deletingWorkflows.has(row.id)) {
|
||||
handleDeleteWorkflow(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
], [t, stoppingWorkflows, deletingWorkflows, handleStopWorkflow, handleDeleteWorkflow]);
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
return (
|
||||
<div className={`${styles.workflowsTable} ${className}`}>
|
||||
<div className={styles.errorState}>
|
||||
<p>{t('workflows.error.loading')} {error}</p>
|
||||
<button onClick={refetch} className={styles.retryButton}>
|
||||
{t('workflows.button.retry')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.workflowsTable} ${className}`}>
|
||||
<FormGenerator
|
||||
data={workflows}
|
||||
columns={columns}
|
||||
title={t('workflows.table.title')}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
resizable={true}
|
||||
pagination={true}
|
||||
pageSize={10}
|
||||
loading={loading}
|
||||
actions={actions}
|
||||
className={styles.workflowsFormGenerator}
|
||||
onRowClick={(workflow: Workflow) => {
|
||||
// TODO: Navigate to workflow detail view
|
||||
console.log('Clicked workflow:', workflow);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkflowsTable;
|
||||
1
src/components/Workflows/index.ts
Normal file
1
src/components/Workflows/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as WorkflowsTable } from './WorkflowsTable';
|
||||
|
|
@ -3,21 +3,21 @@ import { useApiRequest } from './useApi';
|
|||
|
||||
// File interfaces
|
||||
export interface FileInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
id: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
size?: number;
|
||||
fileSize: number;
|
||||
creationDate: string;
|
||||
fileHash?: string;
|
||||
mandateId?: number;
|
||||
userId?: number;
|
||||
mandateId?: string;
|
||||
workflowId?: string;
|
||||
source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me'
|
||||
}
|
||||
|
||||
export interface UserFile {
|
||||
id: number;
|
||||
id: string;
|
||||
file_name: string;
|
||||
mime_type?: string;
|
||||
action: string;
|
||||
created_at: string;
|
||||
size?: number;
|
||||
|
|
@ -32,7 +32,7 @@ export function useUserFiles() {
|
|||
const fetchFiles = async () => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: '/api/files',
|
||||
url: '/api/files/list',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
|
|
@ -73,10 +73,11 @@ export function useUserFiles() {
|
|||
|
||||
return {
|
||||
id: apiFile.id,
|
||||
file_name: apiFile.name,
|
||||
file_name: apiFile.filename,
|
||||
mime_type: apiFile.mimeType,
|
||||
action: action,
|
||||
created_at: apiFile.creationDate,
|
||||
size: apiFile.size,
|
||||
size: apiFile.fileSize,
|
||||
source: apiFile.source
|
||||
};
|
||||
});
|
||||
|
|
@ -88,7 +89,7 @@ export function useUserFiles() {
|
|||
};
|
||||
|
||||
// Optimistically remove a file from the local state
|
||||
const removeFileOptimistically = (fileId: number) => {
|
||||
const removeFileOptimistically = (fileId: string) => {
|
||||
setFiles(prevFiles => prevFiles.filter(file => file.id !== fileId));
|
||||
};
|
||||
|
||||
|
|
@ -113,21 +114,21 @@ export function useUserFiles() {
|
|||
|
||||
// File operations hook
|
||||
export function useFileOperations() {
|
||||
const [downloadingFiles, setDownloadingFiles] = useState<Set<number>>(new Set());
|
||||
const [deletingFiles, setDeletingFiles] = useState<Set<number>>(new Set());
|
||||
const [downloadingFiles, setDownloadingFiles] = useState<Set<string>>(new Set());
|
||||
const [deletingFiles, setDeletingFiles] = useState<Set<string>>(new Set());
|
||||
const [uploadingFile, setUploadingFile] = useState(false);
|
||||
const { request, error: apiError, isLoading } = useApiRequest();
|
||||
const { request, isLoading } = useApiRequest();
|
||||
const [downloadError, setDownloadError] = useState<string | null>(null);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
|
||||
const handleFileDownload = async (fileId: number, fileName: string) => {
|
||||
const handleFileDownload = async (fileId: string, fileName: string) => {
|
||||
setDownloadError(null);
|
||||
setDownloadingFiles(prev => new Set(prev).add(fileId));
|
||||
|
||||
try {
|
||||
const blob = await request({
|
||||
url: `/api/files/${fileId}`,
|
||||
url: `/api/files/${fileId}/download`,
|
||||
method: 'get',
|
||||
// Override axios config for blob response
|
||||
additionalConfig: { responseType: 'blob' }
|
||||
|
|
@ -156,7 +157,7 @@ export function useFileOperations() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleFileDelete = async (fileId: number, onOptimisticDelete?: () => void) => {
|
||||
const handleFileDelete = async (fileId: string, onOptimisticDelete?: () => void) => {
|
||||
setDeleteError(null);
|
||||
setDeletingFiles(prev => new Set(prev).add(fileId));
|
||||
|
||||
|
|
@ -220,6 +221,23 @@ export function useFileOperations() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleFileUpdate = async (fileId: string, updateData: Partial<{ filename: string }>) => {
|
||||
setUploadError(null); // Reuse upload error state for update operations
|
||||
|
||||
try {
|
||||
const updatedFile = await request({
|
||||
url: `/api/files/${fileId}`,
|
||||
method: 'put',
|
||||
data: updateData
|
||||
});
|
||||
|
||||
return { success: true, fileData: updatedFile };
|
||||
} catch (error: any) {
|
||||
setUploadError(error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
downloadingFiles,
|
||||
deletingFiles,
|
||||
|
|
@ -230,6 +248,7 @@ export function useFileOperations() {
|
|||
handleFileDownload,
|
||||
handleFileDelete,
|
||||
handleFileUpload,
|
||||
handleFileUpdate,
|
||||
isLoading
|
||||
};
|
||||
}
|
||||
|
|
@ -1,18 +1,23 @@
|
|||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useApiRequest } from './useApi';
|
||||
|
||||
// Workflow interfaces
|
||||
// Workflow interfaces (matching backend ChatWorkflow model)
|
||||
export interface Workflow {
|
||||
id: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
mandateId: string;
|
||||
status: string;
|
||||
startedAt?: string;
|
||||
lastActivity?: string;
|
||||
currentRound?: number;
|
||||
dataStats?: Record<string, any>;
|
||||
userId?: number;
|
||||
messageIds?: string[];
|
||||
name?: string;
|
||||
title?: string; // Keep for backward compatibility
|
||||
currentRound: number;
|
||||
lastActivity: string;
|
||||
startedAt: string;
|
||||
logs?: any[];
|
||||
messages?: any[];
|
||||
stats?: Record<string, any>;
|
||||
tasks?: any[];
|
||||
dataStats?: Record<string, any>; // Keep for backward compatibility
|
||||
userId?: number; // Keep for backward compatibility
|
||||
messageIds?: string[]; // Keep for backward compatibility
|
||||
}
|
||||
|
||||
export interface WorkflowMessage {
|
||||
|
|
@ -43,7 +48,7 @@ export function useWorkflows() {
|
|||
const fetchWorkflows = async () => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: '/api/workflows',
|
||||
url: '/api/workflows/',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
|
|
@ -65,7 +70,7 @@ export function useWorkflowOperations() {
|
|||
const [startingWorkflow, setStartingWorkflow] = useState(false);
|
||||
const [stoppingWorkflows, setStoppingWorkflows] = useState<Set<string>>(new Set());
|
||||
const [deletingWorkflows, setDeletingWorkflows] = useState<Set<string>>(new Set());
|
||||
const { request, error: apiError, isLoading } = useApiRequest();
|
||||
const { request, isLoading } = useApiRequest();
|
||||
const [startError, setStartError] = useState<string | null>(null);
|
||||
const [stopError, setStopError] = useState<string | null>(null);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
|
|
@ -274,10 +279,10 @@ export function useFilePreview() {
|
|||
throw new Error(`Invalid file ID format: "${fileId}" (type: ${typeof fileId}). Expected a numeric file ID, but got a document UUID. Make sure the document object has a 'fileId' property with the numeric file ID.`);
|
||||
}
|
||||
|
||||
const response = await request({
|
||||
url: `/api/workflows/files/${numericFileId}/preview`,
|
||||
method: 'get'
|
||||
});
|
||||
const response = await request({
|
||||
url: `/api/files/${numericFileId}/preview`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
// Handle response as object with metadata and preview content
|
||||
if (typeof response === 'object' && response !== null) {
|
||||
|
|
@ -315,7 +320,7 @@ export function useFilePreview() {
|
|||
try {
|
||||
// Try to fetch the raw file content using download endpoint
|
||||
const rawResponse = await request({
|
||||
url: `/api/workflows/files/${numericFileId}/download`,
|
||||
url: `/api/files/${numericFileId}/download`,
|
||||
method: 'get',
|
||||
additionalConfig: { responseType: 'text' }
|
||||
});
|
||||
|
|
@ -389,7 +394,7 @@ export function useFileDownload() {
|
|||
|
||||
// Use the same approach as useFiles.ts - use request with blob response type
|
||||
const blob = await request({
|
||||
url: `/api/workflows/files/${numericFileId}/download`,
|
||||
url: `/api/files/${numericFileId}/download`,
|
||||
method: 'get',
|
||||
// Override axios config for blob response
|
||||
additionalConfig: { responseType: 'blob' }
|
||||
|
|
|
|||
|
|
@ -253,6 +253,10 @@ export default {
|
|||
'files.upload.unexpected_error': 'Beim Hochladen ist ein unerwarteter Fehler aufgetreten.',
|
||||
|
||||
// Files Page
|
||||
'files.title': 'Dateien',
|
||||
'files.table.title': 'Dateien',
|
||||
'files.error.loading': 'Fehler beim Laden der Dateien:',
|
||||
'files.button.retry': 'Wiederholen',
|
||||
'files.page.tab.all': 'Alle Dateien',
|
||||
'files.page.tab.uploads': 'Meine Uploads',
|
||||
'files.page.tab.created': 'Erstellte Dateien',
|
||||
|
|
@ -260,4 +264,78 @@ export default {
|
|||
'files.page.add_file': 'Datei hinzufügen',
|
||||
'files.page.loading': 'Dateien werden geladen...',
|
||||
'files.page.error': 'Fehler:',
|
||||
|
||||
// File Table Columns
|
||||
'files.column.name': 'Name',
|
||||
'files.column.type': 'Typ',
|
||||
'files.column.size': 'Größe',
|
||||
'files.column.created': 'Erstellt',
|
||||
'files.column.source': 'Quelle',
|
||||
|
||||
// File Types
|
||||
'files.type.image': 'Bild',
|
||||
'files.type.pdf': 'PDF',
|
||||
'files.type.document': 'Dokument',
|
||||
'files.type.spreadsheet': 'Tabelle',
|
||||
'files.type.text': 'Text',
|
||||
'files.type.video': 'Video',
|
||||
'files.type.audio': 'Audio',
|
||||
'files.type.file': 'Datei',
|
||||
|
||||
// File Sources
|
||||
'files.source.uploaded': 'Hochgeladen',
|
||||
'files.source.created': 'KI Erstellt',
|
||||
'files.source.shared': 'Geteilt',
|
||||
|
||||
// File Actions
|
||||
'files.action.download': 'Herunterladen',
|
||||
'files.action.delete': 'Löschen',
|
||||
'files.delete.confirm': 'Sind Sie sicher, dass Sie die Datei "{name}" löschen möchten?',
|
||||
|
||||
// Workflows Page
|
||||
'workflows.title': 'Workflows',
|
||||
'workflows.table.title': 'Workflows',
|
||||
'workflows.error.loading': 'Fehler beim Laden der Workflows:',
|
||||
'workflows.button.retry': 'Wiederholen',
|
||||
'workflows.table.empty': 'Keine Workflows gefunden',
|
||||
|
||||
// Workflow Table Columns
|
||||
'workflows.column.id': 'ID',
|
||||
'workflows.column.name': 'Name',
|
||||
'workflows.column.status': 'Status',
|
||||
'workflows.column.round': 'Runde',
|
||||
'workflows.column.started': 'Gestartet',
|
||||
'workflows.column.lastActivity': 'Letzte Aktivität',
|
||||
'workflows.column.messages': 'Nachrichten',
|
||||
|
||||
// Workflow Status
|
||||
'workflows.status.running': 'Läuft',
|
||||
'workflows.status.completed': 'Abgeschlossen',
|
||||
'workflows.status.failed': 'Fehlgeschlagen',
|
||||
'workflows.status.stopped': 'Gestoppt',
|
||||
'workflows.status.pending': 'Wartend',
|
||||
|
||||
// Workflow Actions
|
||||
'workflows.action.stop': 'Stoppen',
|
||||
'workflows.action.delete': 'Löschen',
|
||||
'workflows.action.stop.tooltip': 'Workflow stoppen',
|
||||
'workflows.action.delete.tooltip': 'Workflow löschen',
|
||||
|
||||
// Workflow Messages
|
||||
'workflows.unnamed': 'Unbenannter Workflow',
|
||||
'workflows.delete.confirm': 'Sind Sie sicher, dass Sie den Workflow "{name}" löschen möchten?',
|
||||
'workflows.loading': 'Workflows werden geladen...',
|
||||
|
||||
// FormGenerator
|
||||
'formgen.search.placeholder': 'Suchen...',
|
||||
'formgen.filter.yes': 'Ja',
|
||||
'formgen.filter.no': 'Nein',
|
||||
'formgen.filter.clear': 'Filter löschen',
|
||||
'formgen.filter.placeholder': '{column} filtern',
|
||||
'formgen.actions.column': 'Aktionen',
|
||||
'formgen.pagination.info': 'Seite {page} von {total} ({count} Einträge)',
|
||||
'formgen.pagination.first': 'Erste Seite',
|
||||
'formgen.pagination.prev': 'Vorherige Seite',
|
||||
'formgen.pagination.next': 'Nächste Seite',
|
||||
'formgen.pagination.last': 'Letzte Seite',
|
||||
};
|
||||
|
|
@ -254,6 +254,10 @@ export default {
|
|||
'files.upload.unexpected_error': 'An unexpected error occurred while uploading.',
|
||||
|
||||
// Files Page
|
||||
'files.title': 'Files',
|
||||
'files.table.title': 'Files',
|
||||
'files.error.loading': 'Error loading files:',
|
||||
'files.button.retry': 'Retry',
|
||||
'files.page.tab.all': 'All Files',
|
||||
'files.page.tab.uploads': 'My Uploads',
|
||||
'files.page.tab.created': 'Created Files',
|
||||
|
|
@ -261,4 +265,78 @@ export default {
|
|||
'files.page.add_file': 'Add File',
|
||||
'files.page.loading': 'Loading files...',
|
||||
'files.page.error': 'Error:',
|
||||
|
||||
// File Table Columns
|
||||
'files.column.name': 'Name',
|
||||
'files.column.type': 'Type',
|
||||
'files.column.size': 'Size',
|
||||
'files.column.created': 'Created',
|
||||
'files.column.source': 'Source',
|
||||
|
||||
// File Types
|
||||
'files.type.image': 'Image',
|
||||
'files.type.pdf': 'PDF',
|
||||
'files.type.document': 'Document',
|
||||
'files.type.spreadsheet': 'Spreadsheet',
|
||||
'files.type.text': 'Text',
|
||||
'files.type.video': 'Video',
|
||||
'files.type.audio': 'Audio',
|
||||
'files.type.file': 'File',
|
||||
|
||||
// File Sources
|
||||
'files.source.uploaded': 'Uploaded',
|
||||
'files.source.created': 'AI Created',
|
||||
'files.source.shared': 'Shared',
|
||||
|
||||
// File Actions
|
||||
'files.action.download': 'Download',
|
||||
'files.action.delete': 'Delete',
|
||||
'files.delete.confirm': 'Are you sure you want to delete the file "{name}"?',
|
||||
|
||||
// Workflows Page
|
||||
'workflows.title': 'Workflows',
|
||||
'workflows.table.title': 'Workflows',
|
||||
'workflows.error.loading': 'Error loading workflows:',
|
||||
'workflows.button.retry': 'Retry',
|
||||
'workflows.table.empty': 'No workflows found',
|
||||
|
||||
// Workflow Table Columns
|
||||
'workflows.column.id': 'ID',
|
||||
'workflows.column.name': 'Name',
|
||||
'workflows.column.status': 'Status',
|
||||
'workflows.column.round': 'Round',
|
||||
'workflows.column.started': 'Started',
|
||||
'workflows.column.lastActivity': 'Last Activity',
|
||||
'workflows.column.messages': 'Messages',
|
||||
|
||||
// Workflow Status
|
||||
'workflows.status.running': 'Running',
|
||||
'workflows.status.completed': 'Completed',
|
||||
'workflows.status.failed': 'Failed',
|
||||
'workflows.status.stopped': 'Stopped',
|
||||
'workflows.status.pending': 'Pending',
|
||||
|
||||
// Workflow Actions
|
||||
'workflows.action.stop': 'Stop',
|
||||
'workflows.action.delete': 'Delete',
|
||||
'workflows.action.stop.tooltip': 'Stop workflow',
|
||||
'workflows.action.delete.tooltip': 'Delete workflow',
|
||||
|
||||
// Workflow Messages
|
||||
'workflows.unnamed': 'Unnamed Workflow',
|
||||
'workflows.delete.confirm': 'Are you sure you want to delete workflow "{name}"?',
|
||||
'workflows.loading': 'Loading workflows...',
|
||||
|
||||
// FormGenerator
|
||||
'formgen.search.placeholder': 'Search...',
|
||||
'formgen.filter.yes': 'Yes',
|
||||
'formgen.filter.no': 'No',
|
||||
'formgen.filter.clear': 'Clear filter',
|
||||
'formgen.filter.placeholder': 'Filter {column}',
|
||||
'formgen.actions.column': 'Actions',
|
||||
'formgen.pagination.info': 'Page {page} of {total} ({count} items)',
|
||||
'formgen.pagination.first': 'First page',
|
||||
'formgen.pagination.prev': 'Previous page',
|
||||
'formgen.pagination.next': 'Next page',
|
||||
'formgen.pagination.last': 'Last page',
|
||||
};
|
||||
|
|
@ -253,6 +253,10 @@ export default {
|
|||
'files.upload.unexpected_error': 'Une erreur inattendue s\'est produite lors du téléchargement.',
|
||||
|
||||
// Files Page
|
||||
'files.title': 'Fichiers',
|
||||
'files.table.title': 'Fichiers',
|
||||
'files.error.loading': 'Erreur lors du chargement des fichiers:',
|
||||
'files.button.retry': 'Réessayer',
|
||||
'files.page.tab.all': 'Tous les fichiers',
|
||||
'files.page.tab.uploads': 'Mes téléchargements',
|
||||
'files.page.tab.created': 'Fichiers créés',
|
||||
|
|
@ -260,4 +264,78 @@ export default {
|
|||
'files.page.add_file': 'Ajouter un fichier',
|
||||
'files.page.loading': 'Chargement des fichiers...',
|
||||
'files.page.error': 'Erreur:',
|
||||
|
||||
// File Table Columns
|
||||
'files.column.name': 'Nom',
|
||||
'files.column.type': 'Type',
|
||||
'files.column.size': 'Taille',
|
||||
'files.column.created': 'Créé',
|
||||
'files.column.source': 'Source',
|
||||
|
||||
// File Types
|
||||
'files.type.image': 'Image',
|
||||
'files.type.pdf': 'PDF',
|
||||
'files.type.document': 'Document',
|
||||
'files.type.spreadsheet': 'Feuille de calcul',
|
||||
'files.type.text': 'Texte',
|
||||
'files.type.video': 'Vidéo',
|
||||
'files.type.audio': 'Audio',
|
||||
'files.type.file': 'Fichier',
|
||||
|
||||
// File Sources
|
||||
'files.source.uploaded': 'Téléchargé',
|
||||
'files.source.created': 'Créé par IA',
|
||||
'files.source.shared': 'Partagé',
|
||||
|
||||
// File Actions
|
||||
'files.action.download': 'Télécharger',
|
||||
'files.action.delete': 'Supprimer',
|
||||
'files.delete.confirm': 'Êtes-vous sûr de vouloir supprimer le fichier "{name}"?',
|
||||
|
||||
// Workflows Page
|
||||
'workflows.title': 'Workflows',
|
||||
'workflows.table.title': 'Workflows',
|
||||
'workflows.error.loading': 'Erreur lors du chargement des workflows:',
|
||||
'workflows.button.retry': 'Réessayer',
|
||||
'workflows.table.empty': 'Aucun workflow trouvé',
|
||||
|
||||
// Workflow Table Columns
|
||||
'workflows.column.id': 'ID',
|
||||
'workflows.column.name': 'Nom',
|
||||
'workflows.column.status': 'Statut',
|
||||
'workflows.column.round': 'Tour',
|
||||
'workflows.column.started': 'Démarré',
|
||||
'workflows.column.lastActivity': 'Dernière activité',
|
||||
'workflows.column.messages': 'Messages',
|
||||
|
||||
// Workflow Status
|
||||
'workflows.status.running': 'En cours',
|
||||
'workflows.status.completed': 'Terminé',
|
||||
'workflows.status.failed': 'Échoué',
|
||||
'workflows.status.stopped': 'Arrêté',
|
||||
'workflows.status.pending': 'En attente',
|
||||
|
||||
// Workflow Actions
|
||||
'workflows.action.stop': 'Arrêter',
|
||||
'workflows.action.delete': 'Supprimer',
|
||||
'workflows.action.stop.tooltip': 'Arrêter le workflow',
|
||||
'workflows.action.delete.tooltip': 'Supprimer le workflow',
|
||||
|
||||
// Workflow Messages
|
||||
'workflows.unnamed': 'Workflow sans nom',
|
||||
'workflows.delete.confirm': 'Êtes-vous sûr de vouloir supprimer le workflow "{name}"?',
|
||||
'workflows.loading': 'Chargement des workflows...',
|
||||
|
||||
// FormGenerator
|
||||
'formgen.search.placeholder': 'Rechercher...',
|
||||
'formgen.filter.yes': 'Oui',
|
||||
'formgen.filter.no': 'Non',
|
||||
'formgen.filter.clear': 'Effacer le filtre',
|
||||
'formgen.filter.placeholder': 'Filtrer {column}',
|
||||
'formgen.actions.column': 'Actions',
|
||||
'formgen.pagination.info': 'Page {page} sur {total} ({count} éléments)',
|
||||
'formgen.pagination.first': 'Première page',
|
||||
'formgen.pagination.prev': 'Page précédente',
|
||||
'formgen.pagination.next': 'Page suivante',
|
||||
'formgen.pagination.last': 'Dernière page',
|
||||
};
|
||||
|
|
@ -1,156 +1,104 @@
|
|||
import styles from './HomeStyles/Dateien.module.css'
|
||||
import sharedStyles from './HomeStyles/pages.module.css'
|
||||
import { IoAddCircleOutline } from "react-icons/io5";
|
||||
import DateienUpload from '../../components/Dateien/DateienHinzufügen/DateienUploadTool';
|
||||
import DateienAll from '../../components/Dateien/DateienAll';
|
||||
import DateienUploads from '../../components/Dateien/DateienUploads';
|
||||
import DateienCreated from '../../components/Dateien/DateienCreated';
|
||||
import DateienShared from '../../components/Dateien/DateienShared';
|
||||
import { useState } from 'react';
|
||||
import { useUserFiles, useFileOperations } from '../../hooks/useFiles';
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
// Tab types
|
||||
type TabType = 'alle' | 'uploads' | 'erstellt' | 'geteilt';
|
||||
import { useRef, useState } from 'react';
|
||||
import { IoMdCloudUpload } from 'react-icons/io';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import sharedStyles from './HomeStyles/pages.module.css'
|
||||
import styles from './HomeStyles/Dateien.module.css'
|
||||
import { DateienTable } from '../../components/Dateien'
|
||||
import { useFileOperations } from '../../hooks/useFiles';
|
||||
|
||||
function Dateien() {
|
||||
const { t } = useLanguage();
|
||||
const { files, loading, error, refetch, removeFileOptimistically } = useUserFiles();
|
||||
const [isUploadOpen, setIsUploadOpen] = useState(false);
|
||||
const { uploadError, downloadError, deleteError } = useFileOperations();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('alle');
|
||||
const { handleFileUpload, uploadingFile } = useFileOperations();
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [tableRefreshKey, setTableRefreshKey] = useState(0);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
// Tab configuration with translations
|
||||
const tabs = [
|
||||
{ key: 'alle' as TabType, label: t('files.page.tab.all', 'All Files') },
|
||||
{ key: 'uploads' as TabType, label: t('files.page.tab.uploads', 'My Uploads') },
|
||||
{ key: 'erstellt' as TabType, label: t('files.page.tab.created', 'Created Files') },
|
||||
{ key: 'geteilt' as TabType, label: t('files.page.tab.shared', 'Shared Files') }
|
||||
];
|
||||
|
||||
// Single function to handle file refresh
|
||||
const refreshFiles = () => {
|
||||
console.log('Refreshing files list');
|
||||
refetch();
|
||||
const triggerFilePicker = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileUpload = async (file: File) => {
|
||||
console.log('File upload completed:', file.name);
|
||||
};
|
||||
const onFilesSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const handleUploadClose = () => {
|
||||
setIsUploadOpen(false);
|
||||
// Refresh files when upload modal is closed
|
||||
setTimeout(() => {
|
||||
refreshFiles();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleFileDeleted = () => {
|
||||
refreshFiles();
|
||||
};
|
||||
|
||||
// Render the appropriate component based on active tab
|
||||
const renderActiveTabContent = () => {
|
||||
const commonProps = {
|
||||
files,
|
||||
onFileDeleted: handleFileDeleted,
|
||||
onOptimisticDelete: removeFileOptimistically
|
||||
};
|
||||
|
||||
switch (activeTab) {
|
||||
case 'alle':
|
||||
return <DateienAll {...commonProps} />;
|
||||
case 'uploads':
|
||||
return <DateienUploads {...commonProps} />;
|
||||
case 'erstellt':
|
||||
return <DateienCreated {...commonProps} />;
|
||||
case 'geteilt':
|
||||
return <DateienShared {...commonProps} />;
|
||||
default:
|
||||
return <DateienAll {...commonProps} />;
|
||||
// Upload files sequentially
|
||||
for (const file of Array.from(files)) {
|
||||
await handleFileUpload(file);
|
||||
}
|
||||
|
||||
// Force remount DateienTable to refetch
|
||||
setTableRefreshKey(prev => prev + 1);
|
||||
|
||||
// Reset input value to allow re-selecting the same file(s)
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
const files = event.dataTransfer.files;
|
||||
if (files.length === 0) return;
|
||||
|
||||
// Upload files sequentially
|
||||
for (const file of Array.from(files)) {
|
||||
await handleFileUpload(file);
|
||||
}
|
||||
|
||||
// Force remount DateienTable to refetch
|
||||
setTableRefreshKey(prev => prev + 1);
|
||||
};
|
||||
|
||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDragOver(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
<h1 className={sharedStyles.pageTitle}>Dateien</h1>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
|
||||
{/* Combined Header with Tabs and Add Button */}
|
||||
<motion.div
|
||||
className={`${sharedStyles.pageHeader} ${styles.combinedHeader}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className={styles.tabButtonDiv}>
|
||||
{tabs.map((tab) => (
|
||||
<div key={tab.key} className={styles.tabButtonWrapper}>
|
||||
<motion.button
|
||||
className={`${styles.tabButton} ${
|
||||
activeTab === tab.key ? styles.tabButtonActive : styles.tabButtonInactive
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{tab.label}
|
||||
</motion.button>
|
||||
<AnimatePresence>
|
||||
{activeTab === tab.key && (
|
||||
<motion.div
|
||||
className={styles.tabUnderline}
|
||||
initial={{ opacity: 0, width: "0%" }}
|
||||
animate={{ opacity: 1, width: "100%" }}
|
||||
exit={{ opacity: 0, width: "0%" }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
))}
|
||||
<div className={sharedStyles.pageHeader}>
|
||||
<h1 className={sharedStyles.pageTitle}>{t('files.title')}</h1>
|
||||
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
|
||||
<div
|
||||
className={`${styles.dropZone} ${isDragOver ? styles.dropZoneActive : ''}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={triggerFilePicker}
|
||||
>
|
||||
<span className={styles.dropZoneText}>
|
||||
{isDragOver ? 'Drop files here' : 'Drop files here or click to browse'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className={sharedStyles.primaryButton}
|
||||
onClick={triggerFilePicker}
|
||||
disabled={uploadingFile}
|
||||
aria-label="Upload files"
|
||||
>
|
||||
<span className={sharedStyles.buttonIcon}><IoMdCloudUpload /></span>
|
||||
{uploadingFile ? 'Uploading...' : 'Upload Files'}
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
style={{ display: 'none' }}
|
||||
onChange={onFilesSelected}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`${sharedStyles.primaryButton} ${styles.datei_hinzufügen_button}`}
|
||||
onClick={() => setIsUploadOpen(true)}
|
||||
>
|
||||
<IoAddCircleOutline className={sharedStyles.buttonIcon}/>
|
||||
{t('files.page.add_file', 'Add File')}
|
||||
</button>
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
|
||||
<div className={`${sharedStyles.contentArea} ${styles.contentArea}`}>
|
||||
<DateienUpload
|
||||
isOpen={isUploadOpen}
|
||||
onClose={handleUploadClose}
|
||||
onFileUpload={handleFileUpload}
|
||||
/>
|
||||
|
||||
{(uploadError || downloadError || deleteError) && (
|
||||
<p className={styles.error}>
|
||||
{uploadError || downloadError || deleteError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{loading && <p>{t('files.page.loading', 'Loading files...')}</p>}
|
||||
{error && <p>{t('files.page.error', 'Error:')} {error}</p>}
|
||||
|
||||
{!loading && !error && (
|
||||
<motion.div
|
||||
key={activeTab} // Force re-render when tab changes
|
||||
initial={{ opacity: 0}}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{renderActiveTabContent()}
|
||||
</motion.div>
|
||||
)}
|
||||
<div className={sharedStyles.contentArea}>
|
||||
<DateienTable key={tableRefreshKey} className={styles.dateienTableContainer} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -77,3 +77,49 @@
|
|||
/* Any Dateien-specific content area customizations go here */
|
||||
/* Base styling now comes from sharedStyles.contentArea */
|
||||
}
|
||||
|
||||
/* Dateien table container */
|
||||
.dateienTableContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 400px; /* Ensure minimum height for table functionality */
|
||||
}
|
||||
|
||||
/* Drop zone styles */
|
||||
.dropZone {
|
||||
border: 2px dashed var(--color-primary);
|
||||
border-radius: 30px;
|
||||
padding: 15px 20px;
|
||||
background: var(--color-bg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropZone:hover {
|
||||
border-color: var(--color-secondary);
|
||||
background: var(--color-gray-disabled);
|
||||
}
|
||||
|
||||
.dropZoneActive {
|
||||
border-color: var(--color-secondary);
|
||||
background: var(--color-gray-disabled);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.dropZoneText {
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.dropZoneActive .dropZoneText {
|
||||
opacity: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
.homeContainer {
|
||||
position: relative;
|
||||
background-color: var(--color-surface);
|
||||
background-color: var(--color-bg);
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
width: 100vw;
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background-image: radial-gradient(circle, var(--color-gray-disabled) 1px, transparent 1px);
|
||||
background-size: 8px 8px;
|
||||
opacity: 0.4;
|
||||
z-index: -1;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,17 @@
|
|||
|
||||
/* Remove old .workflowsContainer - now using sharedStyles.pageContainer + sharedStyles.pageCard */
|
||||
|
||||
/* Workflow-specific styles go here */
|
||||
/* Workflow table container */
|
||||
.workflowsTableContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 400px; /* Ensure minimum height for table functionality */
|
||||
}
|
||||
|
||||
/* Future workflow-specific styles go here */
|
||||
.workflowItem {
|
||||
/* Future workflow item styling */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import styles from './HomeStyles/Workflows.module.css'
|
||||
import sharedStyles from './HomeStyles/pages.module.css'
|
||||
import { WorkflowsTable } from '../../components/Workflows'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
|
||||
function Workflows () {
|
||||
const { t } = useLanguage()
|
||||
|
||||
return (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
<h1 className={sharedStyles.pageTitle}>Workflows</h1>
|
||||
<h1 className={sharedStyles.pageTitle}>{t('workflows.title')}</h1>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
|
||||
<div className={sharedStyles.contentArea}>
|
||||
{/* Workflow content will go here */}
|
||||
<p>Workflow management coming soon...</p>
|
||||
<WorkflowsTable className={styles.workflowsTableContainer} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue