fixed timestamps

This commit is contained in:
Ida Dittrich 2025-09-02 06:58:26 +02:00
parent 5a945b1364
commit 0848e98d5e
17 changed files with 474 additions and 122 deletions

2
.env
View file

@ -1,4 +1,4 @@
VITE_API_BASE_URL="https://gateway-int.poweron-center.net/"
VITE_API_BASE_URL="http://localhost:8000"
VITE_MICROSOFT_CLIENT_ID="24cd6c8a-b592-4905-a5ba-d5fa9f911154"
VITE_MICROSOFT_TENANT_ID="6a51aaeb-2467-4186-9504-2a05aedc591f"
VITE_ENTRA_CLIENT_SECRET="2iw8Q~jwqG1iacxHopBt5pstu6R45UC1gIQabcbD"

View file

@ -90,10 +90,11 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
label: t('connections.field.connected_at', 'Connected At'),
type: 'readonly',
editable: false,
formatter: (value: string) => {
formatter: (value: number) => {
if (!value) return t('connections.not_available', 'N/A');
try {
return new Date(value).toLocaleString();
// Convert from seconds to milliseconds for Date constructor
return new Date(value * 1000).toLocaleString();
} catch {
return t('connections.invalid_date', 'Invalid Date');
}
@ -104,10 +105,11 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
label: t('connections.field.last_checked', 'Last Checked'),
type: 'readonly',
editable: false,
formatter: (value: string) => {
formatter: (value: number) => {
if (!value) return t('connections.not_available', 'N/A');
try {
return new Date(value).toLocaleString();
// Convert from seconds to milliseconds for Date constructor
return new Date(value * 1000).toLocaleString();
} catch {
return t('connections.invalid_date', 'Invalid Date');
}
@ -199,24 +201,6 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
}, []);
// Handler functions
const handleCreateConnection = async (type: 'msft' | 'google') => {
console.log('Creating connection for type:', type);
try {
const connectionData: CreateConnectionData = {
type: type,
status: 'pending',
connectedAt: new Date().toISOString(),
lastChecked: new Date().toISOString()
};
console.log('Sending connection data to backend:', connectionData);
const newConnection = await createConnection(connectionData);
console.log('Connection created successfully:', newConnection);
await fetchConnections();
} catch (error) {
console.error('Error creating connection:', error);
}
};
const handleConnect = async (connection: Connection) => {
console.log('Connecting to service:', connection);
@ -228,6 +212,31 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
}
};
const handleCreateConnection = async (type: 'msft' | 'google') => {
console.log('Creating connection for type:', type);
try {
// Get current UTC timestamp in seconds (float) to match backend
const currentTimestamp = Math.floor(Date.now() / 1000);
const connectionData: CreateConnectionData = {
type: type,
status: 'pending',
connectedAt: currentTimestamp,
lastChecked: currentTimestamp
};
console.log('Sending connection data to backend:', connectionData);
const newConnection = await createConnection(connectionData);
console.log('Connection created successfully:', newConnection);
handleConnect(newConnection);
await fetchConnections();
} catch (error) {
console.error('Error creating connection:', error);
}
};
const handleDisconnect = async (connection: Connection) => {
console.log('Disconnecting from service:', connection);
try {

View file

@ -50,7 +50,7 @@ export interface WorkflowMessage {
role: 'user' | 'assistant' | 'system';
status: string;
sequenceNr: number;
publishedAt: string;
publishedAt: number; // UTC timestamp in seconds (float from backend)
timestamp?: string; // For backward compatibility
fileIds?: string[]; // For backward compatibility
stats?: WorkflowMessageStats;
@ -115,8 +115,8 @@ export interface Workflow {
status: string;
name?: string;
currentRound: number;
lastActivity: string;
startedAt: string;
lastActivity: number; // UTC timestamp in seconds (float from backend)
startedAt: number; // UTC timestamp in seconds (float from backend)
logs?: WorkflowLog[];
messages?: WorkflowMessage[];
stats?: WorkflowStats;

View file

@ -23,7 +23,8 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
// Helper to create optimistic user message
const createOptimisticMessage = useCallback((prompt: string, fileIds: string[] = []) => {
const timestamp = new Date().toISOString();
// Use UTC timestamp in seconds (float) to match backend expectation
const timestamp = Math.floor(Date.now() / 1000);
return {
id: `temp-${Date.now()}`,
workflowId: currentWorkflowId || 'pending',

View file

@ -25,7 +25,7 @@ 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 }>;
handleFileUpdate: (fileId: string, updateData: Partial<{ fileName: string }>) => Promise<{ success: boolean; fileData?: any; error?: string }>;
}
// Hook Return Types for File Operations

View file

@ -64,7 +64,7 @@ export function useDateienLogic(): DateienLogicReturn {
try {
// Call API to update filename
const result = await handleFileUpdate(editingFile.id, {
filename: updatedFile.file_name
fileName: updatedFile.file_name
});
if (result.success) {
@ -108,9 +108,12 @@ export function useDateienLogic(): DateienLogicReturn {
// Helper function to format date
const formatDate: DateFormatter = (value) => {
if (!value) return '-';
if (!value || value === 'null' || value === 'undefined') return '-';
try {
const date = new Date(value);
if (isNaN(date.getTime())) {
return '-';
}
const pad = (n: number) => n.toString().padStart(2, '0');
const yyyy = date.getFullYear();
const mm = pad(date.getMonth() + 1);
@ -120,10 +123,76 @@ export function useDateienLogic(): DateienLogicReturn {
const ss = pad(date.getSeconds());
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`;
} catch {
return value;
return '-';
}
};
// Helper function to format MIME type to user-friendly display
const formatMimeType = (mimeType?: string): string => {
if (!mimeType) return '-';
// Excel files
if (mimeType.includes('spreadsheet') ||
mimeType.includes('excel') ||
mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
mimeType === 'application/vnd.ms-excel') {
return 'Excel Table';
}
// Word documents
if (mimeType.includes('word') ||
mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
mimeType === 'application/msword') {
return 'Word Document';
}
// PowerPoint presentations
if (mimeType.includes('presentation') ||
mimeType === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ||
mimeType === 'application/vnd.ms-powerpoint') {
return 'PowerPoint Presentation';
}
// PDF files
if (mimeType === 'application/pdf') {
return 'PDF Document';
}
// Images
if (mimeType.startsWith('image/')) {
const subType = mimeType.split('/')[1]?.toUpperCase();
return `${subType} Image`;
}
// Text files
if (mimeType.startsWith('text/')) {
if (mimeType === 'text/plain') return 'Text File';
if (mimeType === 'text/csv') return 'CSV File';
if (mimeType === 'text/html') return 'HTML File';
return 'Text Document';
}
// Video files
if (mimeType.startsWith('video/')) {
const subType = mimeType.split('/')[1]?.toUpperCase();
return `${subType} Video`;
}
// Audio files
if (mimeType.startsWith('audio/')) {
const subType = mimeType.split('/')[1]?.toUpperCase();
return `${subType} Audio`;
}
// Archive files
if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('archive')) {
return 'Archive File';
}
// Fallback: return the MIME type as-is but shortened
return mimeType.length > 30 ? mimeType.substring(0, 30) + '...' : mimeType;
};
// Configure columns for the files table
const columns: ColumnConfig[] = useMemo(() => [
{
@ -162,6 +231,21 @@ export function useDateienLogic(): DateienLogicReturn {
sortable: true,
filterable: true,
searchable: true,
formatter: (value: string | undefined) => (
<span
style={{
color: 'var(--color-text)',
fontWeight: 500,
display: 'block',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
title={value} // Show original MIME type on hover
>
{formatMimeType(value)}
</span>
)
},
{
key: 'size',

View file

@ -32,9 +32,10 @@ export interface FormGeneratorProps<T = any> {
onRowClick?: (row: T, index: number) => void;
onRowSelect?: (selectedRows: T[]) => void;
selectable?: boolean;
isRowSelectable?: (row: T) => boolean;
loading?: boolean;
actions?: {
label: string;
label: string | ((row: T) => string);
onClick: (row: T) => void;
icon?: string | React.ReactNode | ((row: T) => React.ReactNode);
}[];
@ -57,6 +58,7 @@ export function FormGenerator<T extends Record<string, any>>({
onRowClick,
onRowSelect,
selectable = true, // Default to true for selection functionality
isRowSelectable,
loading = false,
actions = [],
onDelete,
@ -232,6 +234,9 @@ export function FormGenerator<T extends Record<string, any>>({
const handleRowSelect = (index: number) => {
if (!selectable) return;
const row = paginatedData[index];
if (isRowSelectable && !isRowSelectable(row)) return;
const newSelected = new Set(selectedRows);
if (newSelected.has(index)) {
newSelected.delete(index);
@ -250,13 +255,20 @@ export function FormGenerator<T extends Record<string, any>>({
const handleSelectAll = () => {
if (!selectable) return;
if (selectedRows.size === paginatedData.length) {
// Get only selectable rows
const selectableIndices = paginatedData
.map((row, index) => ({ row, index }))
.filter(({ row }) => !isRowSelectable || isRowSelectable(row))
.map(({ index }) => index);
if (selectedRows.size === selectableIndices.length) {
setSelectedRows(new Set());
onRowSelect?.([]);
} else {
const allIndices = new Set(paginatedData.map((_, index) => index));
setSelectedRows(allIndices);
onRowSelect?.(paginatedData);
const allSelectableIndices = new Set(selectableIndices);
setSelectedRows(allSelectableIndices);
const selectableData = selectableIndices.map(i => paginatedData[i]);
onRowSelect?.(selectableData);
}
};
@ -585,9 +597,15 @@ export function FormGenerator<T extends Record<string, any>>({
<th className={styles.selectColumn} style={{ width: '50px', minWidth: '50px', maxWidth: '50px' }}>
<input
type="checkbox"
checked={selectedRows.size === paginatedData.length && paginatedData.length > 0}
checked={(() => {
const selectableIndices = paginatedData
.map((row, index) => ({ row, index }))
.filter(({ row }) => !isRowSelectable || isRowSelectable(row))
.map(({ index }) => index);
return selectedRows.size === selectableIndices.length && selectableIndices.length > 0;
})()}
onChange={handleSelectAll}
title="Select all items"
title={t('formgen.select.all', 'Select all items')}
/>
</th>
)}
@ -644,7 +662,16 @@ export function FormGenerator<T extends Record<string, any>>({
checked={selectedRows.has(index)}
onChange={() => handleRowSelect(index)}
onClick={(e) => e.stopPropagation()}
title="Select this item"
disabled={isRowSelectable && !isRowSelectable(row)}
title={
isRowSelectable && !isRowSelectable(row)
? t('formgen.select.disabled', 'This item cannot be selected')
: t('formgen.select.item', 'Select this item')
}
style={{
opacity: isRowSelectable && !isRowSelectable(row) ? 0.4 : 1,
cursor: isRowSelectable && !isRowSelectable(row) ? 'not-allowed' : 'pointer'
}}
/>
</td>
)}
@ -654,23 +681,26 @@ export function FormGenerator<T extends Record<string, any>>({
style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}
>
<div className={styles.actionButtons}>
{actions.map((action, actionIndex) => (
<button
key={actionIndex}
onClick={(e) => {
e.stopPropagation();
action.onClick(row);
}}
className={styles.actionButton}
title={action.label}
>
{action.icon && (
<span className={styles.actionIcon}>
{typeof action.icon === 'function' ? action.icon(row) : action.icon}
</span>
)}
</button>
))}
{actions.map((action, actionIndex) => {
const actionLabel = typeof action.label === 'function' ? action.label(row) : action.label;
return (
<button
key={actionIndex}
onClick={(e) => {
e.stopPropagation();
action.onClick(row);
}}
className={styles.actionButton}
title={actionLabel}
>
{action.icon && (
<span className={styles.actionIcon}>
{typeof action.icon === 'function' ? action.icon(row) : action.icon}
</span>
)}
</button>
);
})}
</div>
</td>
)}

View file

@ -4,10 +4,31 @@ import { FormGenerator } from '../FormGenerator/FormGenerator';
import { Popup } from '../Popup/Popup';
import { EditForm } from '../Popup/EditForm';
import { usePromptsLogic } from './promptsLogic';
import { PromptsTableProps } from './promptsTypes';
import { PromptsTableProps, Prompt } from './promptsTypes';
import { useLanguage } from '../../contexts/LanguageContext';
import styles from './PromptsTable.module.css';
// Helper function to determine if a prompt can be selected/deleted
const isPromptSelectable = (prompt: Prompt): boolean => {
// Primary check: If backend explicitly sets _hideDelete, respect that
if (prompt._hideDelete === true) {
return false;
}
// Hardcoded list of the six default system prompt IDs that cannot be selected/deleted
const systemPromptIds = [
"097d34f0-9fcc-4233-bf1e-e64e13818464",
"17546dc6-b792-40e1-9aa1-0fcc4860d0c1",
"17c42519-2bf6-49b4-83f9-3cde7498310c",
"2bb85d1e-4e02-4de8-98ae-0e815267d864",
"93343a5b-49f0-4dbf-9513-2ab5f6938fd8",
"cfb51260-486f-4b42-96fe-ef03f406dcf1"
];
// Check if this prompt is one of the system default prompts
return !systemPromptIds.includes(prompt.id);
};
function PromptsTable({ className = '' }: PromptsTableProps) {
const { t } = useLanguage();
const {
@ -50,6 +71,7 @@ function PromptsTable({ className = '' }: PromptsTableProps) {
pagination={true}
pageSize={10}
selectable={true}
isRowSelectable={isPromptSelectable}
loading={loading}
actions={actions}
onDelete={handleDeleteSingle}

View file

@ -6,6 +6,27 @@ import { usePrompts, usePromptOperations, Prompt } from '../../hooks/usePrompts'
import { useLanguage } from '../../contexts/LanguageContext';
import type { EditFieldConfig } from '../Popup/EditForm';
// Helper function to determine if a prompt can be deleted
const isPromptDeletable = (prompt: Prompt): boolean => {
// Primary check: If backend explicitly sets _hideDelete, respect that
if (prompt._hideDelete === true) {
return false;
}
// Hardcoded list of the six default system prompt IDs that cannot be deleted
const systemPromptIds = [
"097d34f0-9fcc-4233-bf1e-e64e13818464",
"17546dc6-b792-40e1-9aa1-0fcc4860d0c1",
"17c42519-2bf6-49b4-83f9-3cde7498310c",
"2bb85d1e-4e02-4de8-98ae-0e815267d864",
"93343a5b-49f0-4dbf-9513-2ab5f6938fd8",
"cfb51260-486f-4b42-96fe-ef03f406dcf1"
];
// Check if this prompt is one of the system default prompts
return !systemPromptIds.includes(prompt.id);
};
import type {
PromptsLogicReturn,
PromptActionConfig,
@ -15,7 +36,6 @@ import type {
export function usePromptsLogic(): PromptsLogicReturn {
const { prompts, loading, error, refetch } = usePrompts();
const { t } = useLanguage();
const {
handlePromptDelete,
handlePromptUpdate,
@ -258,12 +278,26 @@ export function usePromptsLogic(): PromptsLogicReturn {
}
},
{
label: t('prompts.action.delete', 'Delete'),
icon: (_row: Prompt) => {
return <IoIosTrash />;
label: (row: Prompt) => {
const isDeletable = isPromptDeletable(row);
return isDeletable
? t('prompts.action.delete', 'Delete')
: t('prompts.action.delete.disabled', 'No permission to delete prompt');
},
icon: (row: Prompt) => {
const isDeletable = isPromptDeletable(row);
return (
<IoIosTrash
style={{
opacity: isDeletable ? 1 : 0.4,
cursor: isDeletable ? 'pointer' : 'not-allowed'
}}
/>
);
},
onClick: (row: Prompt) => {
if (!deletingPrompts.has(row.id)) {
const isDeletable = isPromptDeletable(row);
if (isDeletable && !deletingPrompts.has(row.id)) {
handleDeletePrompt(row);
}
}

View file

@ -6,6 +6,8 @@ export interface Prompt {
mandateId: string;
content: string;
name: string;
_createdBy?: string; // Optional field to track who created the prompt
_hideDelete?: boolean; // Backend access control flag
}
// Props for the PromptsTable component
@ -15,7 +17,7 @@ export interface PromptsTableProps {
// Action configuration for prompt actions
export interface PromptActionConfig {
label: string;
label: string | ((row: Prompt) => string);
icon: (row: Prompt) => React.ReactElement;
onClick: (row: Prompt) => void;
}

View file

@ -182,22 +182,11 @@ export function useWorkflowsLogic(): WorkflowsLogicReturn {
maxWidth: 180,
sortable: true,
filterable: true,
formatter: (value: string | number | undefined) => {
formatter: (value: number | undefined) => {
if (!value) return '-';
try {
let date: Date;
// Handle Unix timestamp (as string or number)
if (typeof value === 'string' && /^\d+$/.test(value)) {
// Unix timestamp as string
date = new Date(parseInt(value) * 1000);
} else if (typeof value === 'number') {
// Unix timestamp as number
date = new Date(value * 1000);
} else {
// ISO string or other date format
date = new Date(value);
}
// Backend sends UTC timestamp in seconds (float), convert to milliseconds for Date constructor
const date = new Date(value * 1000);
// Check if date is valid
if (isNaN(date.getTime())) {
@ -220,22 +209,11 @@ export function useWorkflowsLogic(): WorkflowsLogicReturn {
maxWidth: 180,
sortable: true,
filterable: true,
formatter: (value: string | number | undefined) => {
formatter: (value: number | undefined) => {
if (!value) return '-';
try {
let date: Date;
// Handle Unix timestamp (as string or number)
if (typeof value === 'string' && /^\d+$/.test(value)) {
// Unix timestamp as string
date = new Date(parseInt(value) * 1000);
} else if (typeof value === 'number') {
// Unix timestamp as number
date = new Date(value * 1000);
} else {
// ISO string or other date format
date = new Date(value);
}
// Backend sends UTC timestamp in seconds (float), convert to milliseconds for Date constructor
const date = new Date(value * 1000);
// Check if date is valid
if (isNaN(date.getTime())) {

View file

@ -1,7 +1,7 @@
import { useState } from 'react';
import { useApiRequest } from './useApi';
// Connection interfaces based on backend UserConnection model
// Connection interfaces - exactly matching backend UserConnection model
export interface Connection {
id: string;
userId: string;
@ -10,9 +10,9 @@ export interface Connection {
externalUsername: string;
externalEmail?: string;
status: 'active' | 'expired' | 'revoked' | 'pending';
connectedAt: string;
lastChecked: string;
expiresAt?: string;
connectedAt: number; // Backend uses float for UTC timestamp in seconds
lastChecked: number; // Backend uses float for UTC timestamp in seconds
expiresAt?: number; // Backend uses Optional[float] for UTC timestamp in seconds
}
export interface CreateConnectionData {
@ -24,9 +24,9 @@ export interface CreateConnectionData {
externalUsername?: string;
externalEmail?: string;
status?: 'active' | 'expired' | 'revoked' | 'pending';
connectedAt?: string;
lastChecked?: string;
expiresAt?: string;
connectedAt?: number; // Backend uses float for UTC timestamp in seconds
lastChecked?: number; // Backend uses float for UTC timestamp in seconds
expiresAt?: number; // Backend uses Optional[float] for UTC timestamp in seconds
}
export interface ConnectResponse {

View file

@ -1,17 +1,15 @@
import { useState, useEffect } from 'react';
import { useApiRequest } from './useApi';
// File interfaces
// File interfaces - exactly matching backend FileItem model
export interface FileInfo {
id: string;
filename: string;
mandateId: string; // Required in backend
fileName: string; // Required in backend
mimeType: string;
fileHash: string;
fileSize: number;
creationDate: string;
fileHash?: string;
mandateId?: string;
workflowId?: string;
source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me'
creationDate: number; // Backend uses float for UTC timestamp in seconds
}
export interface UserFile {
@ -19,7 +17,7 @@ export interface UserFile {
file_name: string;
mime_type?: string;
action: string;
created_at: string;
created_at: number |string;
size?: number;
source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me'
}
@ -31,13 +29,71 @@ export function useUserFiles() {
const fetchFiles = async () => {
try {
console.log('🔍 Fetching files from API...');
const data = await request({
url: '/api/files/list',
method: 'get'
});
// Map API response to our frontend model
const mappedFiles = data.map((apiFile: FileInfo): UserFile => {
console.log('📥 Raw API response:', data);
// Ensure data is an array, handle null/undefined responses
const fileList = Array.isArray(data) ? data : [];
console.log(`📋 Processing ${fileList.length} files from API`);
// Filter out invalid files and map API response to our frontend model
const validFiles = fileList.filter((apiFile: any): boolean => {
// Skip files with invalid data that would cause backend validation errors
// Backend FileItem requires: id, mandateId, fileName, mimeType, fileHash, fileSize, creationDate
const isValid = !!(
apiFile &&
typeof apiFile === 'object' &&
apiFile.id &&
typeof apiFile.id === 'string' &&
apiFile.mandateId &&
typeof apiFile.mandateId === 'string' &&
apiFile.fileName &&
typeof apiFile.fileName === 'string' &&
apiFile.fileName.trim() !== '' &&
apiFile.mimeType &&
typeof apiFile.mimeType === 'string' &&
apiFile.fileHash &&
typeof apiFile.fileHash === 'string' &&
typeof apiFile.fileSize === 'number' &&
apiFile.fileSize >= 0 &&
typeof apiFile.creationDate === 'number' &&
apiFile.creationDate > 0
);
if (!isValid) {
console.warn('❌ Filtering out invalid file record:', {
id: apiFile?.id,
mandateId: apiFile?.mandateId,
fileName: apiFile?.fileName,
mimeType: apiFile?.mimeType,
fileHash: apiFile?.fileHash,
fileSize: apiFile?.fileSize,
creationDate: apiFile?.creationDate,
creationDateType: typeof apiFile?.creationDate,
fullObject: apiFile
});
} else {
console.log('✅ Valid file:', {
id: apiFile.id,
fileName: apiFile.fileName,
creationDate: apiFile.creationDate
});
}
return isValid;
});
console.log(`✨ Filtered to ${validFiles.length} valid files`);
if (validFiles.length !== fileList.length) {
console.warn(`⚠️ Filtered out ${fileList.length - validFiles.length} invalid files`);
}
const mappedFiles = validFiles.map((apiFile: any): UserFile => {
// Derive a simplified action from the MIME type
let action = 'Document';
if (apiFile.mimeType) {
@ -71,20 +127,28 @@ export function useUserFiles() {
}
}
// Convert creationDate from backend float (seconds since epoch) to ISO string
// Backend always sends creationDate as float seconds since epoch (UTC)
const createdAt = new Date(apiFile.creationDate * 1000).toISOString();
return {
id: apiFile.id,
file_name: apiFile.filename,
mime_type: apiFile.mimeType,
file_name: apiFile.fileName, // Required field from backend
mime_type: apiFile.mimeType, // Required field from backend
action: action,
created_at: apiFile.creationDate,
size: apiFile.fileSize,
source: apiFile.source
created_at: createdAt,
size: apiFile.fileSize, // Required field from backend
source: 'user_uploaded' // Default source since workflowId is not part of FileItem model
};
});
console.log(`🎉 Successfully processed ${mappedFiles.length} files for display`);
setFiles(mappedFiles);
} catch (error) {
// Error is already handled by useApiRequest
console.error('❌ Error fetching files:', error);
// Set empty array on error to prevent UI issues
setFiles([]);
}
};
@ -127,15 +191,25 @@ export function useFileOperations() {
setDownloadingFiles(prev => new Set(prev).add(fileId));
try {
console.log(`📥 Starting download for file: ${fileName} (ID: ${fileId})`);
// Try to get the file download
const blob = await request({
url: `/api/files/${fileId}/download`,
method: 'get',
// Override axios config for blob response
additionalConfig: { responseType: 'blob' }
additionalConfig: {
responseType: 'blob',
// Better error handling for blob responses
validateStatus: function (status: number) {
return status >= 200 && status < 300; // default
}
}
});
console.log(`✅ Download successful for: ${fileName}`, { size: blob.size, type: blob.type });
// Create a download link and trigger the download
const url = window.URL.createObjectURL(new Blob([blob]));
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', fileName);
@ -146,7 +220,16 @@ export function useFileOperations() {
return true;
} catch (error: any) {
setDownloadError(error.message);
console.error(`❌ Download failed for ${fileName}:`, error);
let errorMessage = error.message;
if (error.response?.status === 404) {
errorMessage = `File "${fileName}" not found or has been deleted.`;
} else if (error.response?.status === 403) {
errorMessage = `No permission to download "${fileName}".`;
}
setDownloadError(errorMessage);
return false;
} finally {
setDownloadingFiles(prev => {
@ -167,16 +250,31 @@ export function useFileOperations() {
}
try {
console.log(`🗑️ Starting delete for file ID: ${fileId}`);
await request({
url: `/api/files/${fileId}`,
method: 'delete'
});
console.log(`✅ Delete successful for file ID: ${fileId}`);
// Add a small delay to ensure backend has time to process
await new Promise(resolve => setTimeout(resolve, 300));
return true;
} catch (error: any) {
setDeleteError(error.message);
console.error(`❌ Delete failed for file ID ${fileId}:`, error);
let errorMessage = error.message;
if (error.response?.status === 404) {
errorMessage = `File not found or has already been deleted.`;
// If file doesn't exist, consider it successfully "deleted"
return true;
} else if (error.response?.status === 403) {
errorMessage = `No permission to delete this file.`;
}
setDeleteError(errorMessage);
// If deletion failed and we optimistically removed it, we should refetch to restore the file
return false;
} finally {
@ -188,11 +286,35 @@ export function useFileOperations() {
}
};
/**
* File upload function - backend bug has been fixed!
*
* BACKEND FIXES APPLIED:
* - Fixed file.fileName file.filename in routeDataFiles.py
* - Removed workflowId from FileItem creation in interfaceComponentObjects.py
* - Upload should now work correctly
*/
const handleFileUpload = async (file: globalThis.File, workflowId?: string) => {
setUploadError(null);
setUploadingFile(true);
try {
console.log('📤 Starting file upload...', {
fileName: file.name,
fileSize: file.size,
fileType: file.type,
workflowId: workflowId
});
// Validate file before upload
if (!file || !file.name || file.name.trim() === '') {
throw new Error('Invalid file: File must have a valid name');
}
if (file.size === 0) {
throw new Error('Invalid file: File cannot be empty');
}
const formData = new FormData();
formData.append('file', file);
@ -200,6 +322,25 @@ export function useFileOperations() {
formData.append('workflowId', workflowId);
}
// FormData is now correctly configured for backend
console.log('📋 FormData prepared:', {
hasFile: formData.has('file'),
hasWorkflowId: formData.has('workflowId'),
workflowId: workflowId
});
// Log the actual file object being sent
console.log('📁 File object details:', {
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified,
constructor: file.constructor.name
});
console.log('🚀 Sending upload request...');
const fileData = await request({
url: '/api/files/upload',
method: 'post',
@ -212,29 +353,66 @@ export function useFileOperations() {
}
});
console.log('✅ Upload successful:', fileData);
return { success: true, fileData };
} catch (error: any) {
setUploadError(error.message);
return { success: false, error: error.message };
console.error('❌ Upload failed:', {
error: error,
message: error.message,
response: error.response,
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data
});
// Provide detailed error analysis for backend team
let errorMessage = error.message;
if (error.response?.status === 500) {
if (errorMessage.includes('validation')) {
errorMessage = 'File validation failed. Please ensure the file is valid and try again.';
} else {
errorMessage = 'Server error during upload. Please try again later.';
}
}
console.error('❌ Upload error details:', error);
setUploadError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setUploadingFile(false);
}
};
const handleFileUpdate = async (fileId: string, updateData: Partial<{ filename: string }>) => {
const handleFileUpdate = async (fileId: string, updateData: Partial<{ fileName: string }>) => {
setUploadError(null); // Reuse upload error state for update operations
try {
console.log(`✏️ Starting update for file ID: ${fileId}`, updateData);
const updatedFile = await request({
url: `/api/files/${fileId}`,
method: 'put',
data: updateData
});
console.log(`✅ Update successful for file ID: ${fileId}`);
return { success: true, fileData: updatedFile };
} catch (error: any) {
setUploadError(error.message);
return { success: false, error: error.message };
console.error(`❌ Update failed for file ID ${fileId}:`, error);
let errorMessage = error.message;
if (error.response?.status === 404) {
errorMessage = `File not found or has been deleted.`;
} else if (error.response?.status === 403) {
errorMessage = `No permission to update this file.`;
} else if (error.response?.status === 400) {
errorMessage = `Invalid file update data.`;
}
setUploadError(errorMessage);
return { success: false, error: errorMessage };
}
};

View file

@ -7,6 +7,8 @@ export interface Prompt {
mandateId: string;
content: string;
name: string;
_createdBy?: string; // Optional field to track who created the prompt
_hideDelete?: boolean; // Backend access control flag
}
// Prompts list hook

View file

@ -391,6 +391,9 @@ export default {
'formgen.pagination.prev': 'Vorherige Seite',
'formgen.pagination.next': 'Nächste Seite',
'formgen.pagination.last': 'Letzte Seite',
'formgen.select.all': 'Alle Elemente auswählen',
'formgen.select.item': 'Dieses Element auswählen',
'formgen.select.disabled': 'Dieses Element kann nicht ausgewählt werden',
'formgen.delete.multiple': 'Löschen ({count})',
'formgen.delete.single': 'Löschen',
'formgen.delete.confirm': 'Sind Sie sicher, dass Sie die {count} ausgewählten Elemente löschen möchten?',
@ -407,6 +410,7 @@ export default {
'prompts.action.edit': 'Bearbeiten',
'prompts.action.copy': 'Kopieren',
'prompts.action.delete': 'Löschen',
'prompts.action.delete.disabled': 'Keine Berechtigung zum Löschen des Prompts',
'prompts.delete.confirm': 'Sind Sie sicher, dass Sie "{name}" löschen möchten?',
'prompts.delete.confirmMultiple': 'Sind Sie sicher, dass Sie {count} Prompts löschen möchten?',
'prompts.field.name': 'Prompt-Name',

View file

@ -394,6 +394,9 @@ export default {
'formgen.pagination.prev': 'Previous page',
'formgen.pagination.next': 'Next page',
'formgen.pagination.last': 'Last page',
'formgen.select.all': 'Select all items',
'formgen.select.item': 'Select this item',
'formgen.select.disabled': 'This item cannot be selected',
'formgen.delete.multiple': 'Delete ({count})',
'formgen.delete.confirm_multiple': 'Are you sure you want to delete the {count} selected items?',
@ -407,6 +410,7 @@ export default {
'prompts.action.edit': 'Edit',
'prompts.action.copy': 'Copy',
'prompts.action.delete': 'Delete',
'prompts.action.delete.disabled': 'No permission to delete prompt',
'prompts.delete.confirm': 'Are you sure you want to delete "{name}"?',
'prompts.delete.confirmMultiple': 'Are you sure you want to delete {count} prompts?',
'prompts.field.name': 'Prompt Name',

View file

@ -394,6 +394,9 @@ export default {
'formgen.pagination.prev': 'Page précédente',
'formgen.pagination.next': 'Page suivante',
'formgen.pagination.last': 'Dernière page',
'formgen.select.all': 'Sélectionner tous les éléments',
'formgen.select.item': 'Sélectionner cet élément',
'formgen.select.disabled': 'Cet élément ne peut pas être sélectionné',
'formgen.delete.multiple': 'Supprimer ({count})',
'formgen.delete.confirm_multiple': 'Êtes-vous sûr de vouloir supprimer les {count} éléments sélectionnés ?',
@ -407,6 +410,7 @@ export default {
'prompts.action.edit': 'Modifier',
'prompts.action.copy': 'Copier',
'prompts.action.delete': 'Supprimer',
'prompts.action.delete.disabled': 'Aucune permission de supprimer l\'invite',
'prompts.delete.confirm': 'Êtes-vous sûr de vouloir supprimer "{name}" ?',
'prompts.delete.confirmMultiple': 'Êtes-vous sûr de vouloir supprimer {count} prompts ?',
'prompts.field.name': 'Nom du prompt',