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_CLIENT_ID="24cd6c8a-b592-4905-a5ba-d5fa9f911154"
VITE_MICROSOFT_TENANT_ID="6a51aaeb-2467-4186-9504-2a05aedc591f" VITE_MICROSOFT_TENANT_ID="6a51aaeb-2467-4186-9504-2a05aedc591f"
VITE_ENTRA_CLIENT_SECRET="2iw8Q~jwqG1iacxHopBt5pstu6R45UC1gIQabcbD" VITE_ENTRA_CLIENT_SECRET="2iw8Q~jwqG1iacxHopBt5pstu6R45UC1gIQabcbD"

View file

@ -90,10 +90,11 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
label: t('connections.field.connected_at', 'Connected At'), label: t('connections.field.connected_at', 'Connected At'),
type: 'readonly', type: 'readonly',
editable: false, editable: false,
formatter: (value: string) => { formatter: (value: number) => {
if (!value) return t('connections.not_available', 'N/A'); if (!value) return t('connections.not_available', 'N/A');
try { try {
return new Date(value).toLocaleString(); // Convert from seconds to milliseconds for Date constructor
return new Date(value * 1000).toLocaleString();
} catch { } catch {
return t('connections.invalid_date', 'Invalid Date'); return t('connections.invalid_date', 'Invalid Date');
} }
@ -104,10 +105,11 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
label: t('connections.field.last_checked', 'Last Checked'), label: t('connections.field.last_checked', 'Last Checked'),
type: 'readonly', type: 'readonly',
editable: false, editable: false,
formatter: (value: string) => { formatter: (value: number) => {
if (!value) return t('connections.not_available', 'N/A'); if (!value) return t('connections.not_available', 'N/A');
try { try {
return new Date(value).toLocaleString(); // Convert from seconds to milliseconds for Date constructor
return new Date(value * 1000).toLocaleString();
} catch { } catch {
return t('connections.invalid_date', 'Invalid Date'); return t('connections.invalid_date', 'Invalid Date');
} }
@ -199,24 +201,6 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
}, []); }, []);
// Handler functions // 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) => { const handleConnect = async (connection: Connection) => {
console.log('Connecting to service:', 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) => { const handleDisconnect = async (connection: Connection) => {
console.log('Disconnecting from service:', connection); console.log('Disconnecting from service:', connection);
try { try {

View file

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

View file

@ -23,7 +23,8 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
// Helper to create optimistic user message // Helper to create optimistic user message
const createOptimisticMessage = useCallback((prompt: string, fileIds: string[] = []) => { 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 { return {
id: `temp-${Date.now()}`, id: `temp-${Date.now()}`,
workflowId: currentWorkflowId || 'pending', workflowId: currentWorkflowId || 'pending',

View file

@ -25,7 +25,7 @@ export interface FileHandlers {
handleFileDownload: (fileId: string, fileName: string) => Promise<boolean>; handleFileDownload: (fileId: string, fileName: string) => Promise<boolean>;
handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>; handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>;
handleFileUpload: (file: globalThis.File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>; 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 // Hook Return Types for File Operations

View file

@ -64,7 +64,7 @@ export function useDateienLogic(): DateienLogicReturn {
try { try {
// Call API to update filename // Call API to update filename
const result = await handleFileUpdate(editingFile.id, { const result = await handleFileUpdate(editingFile.id, {
filename: updatedFile.file_name fileName: updatedFile.file_name
}); });
if (result.success) { if (result.success) {
@ -108,9 +108,12 @@ export function useDateienLogic(): DateienLogicReturn {
// Helper function to format date // Helper function to format date
const formatDate: DateFormatter = (value) => { const formatDate: DateFormatter = (value) => {
if (!value) return '-'; if (!value || value === 'null' || value === 'undefined') return '-';
try { try {
const date = new Date(value); const date = new Date(value);
if (isNaN(date.getTime())) {
return '-';
}
const pad = (n: number) => n.toString().padStart(2, '0'); const pad = (n: number) => n.toString().padStart(2, '0');
const yyyy = date.getFullYear(); const yyyy = date.getFullYear();
const mm = pad(date.getMonth() + 1); const mm = pad(date.getMonth() + 1);
@ -120,10 +123,76 @@ export function useDateienLogic(): DateienLogicReturn {
const ss = pad(date.getSeconds()); const ss = pad(date.getSeconds());
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`; return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`;
} catch { } 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 // Configure columns for the files table
const columns: ColumnConfig[] = useMemo(() => [ const columns: ColumnConfig[] = useMemo(() => [
{ {
@ -162,6 +231,21 @@ export function useDateienLogic(): DateienLogicReturn {
sortable: true, sortable: true,
filterable: true, filterable: true,
searchable: 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', key: 'size',

View file

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

View file

@ -4,10 +4,31 @@ import { FormGenerator } from '../FormGenerator/FormGenerator';
import { Popup } from '../Popup/Popup'; import { Popup } from '../Popup/Popup';
import { EditForm } from '../Popup/EditForm'; import { EditForm } from '../Popup/EditForm';
import { usePromptsLogic } from './promptsLogic'; import { usePromptsLogic } from './promptsLogic';
import { PromptsTableProps } from './promptsTypes'; import { PromptsTableProps, Prompt } from './promptsTypes';
import { useLanguage } from '../../contexts/LanguageContext'; import { useLanguage } from '../../contexts/LanguageContext';
import styles from './PromptsTable.module.css'; 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) { function PromptsTable({ className = '' }: PromptsTableProps) {
const { t } = useLanguage(); const { t } = useLanguage();
const { const {
@ -50,6 +71,7 @@ function PromptsTable({ className = '' }: PromptsTableProps) {
pagination={true} pagination={true}
pageSize={10} pageSize={10}
selectable={true} selectable={true}
isRowSelectable={isPromptSelectable}
loading={loading} loading={loading}
actions={actions} actions={actions}
onDelete={handleDeleteSingle} onDelete={handleDeleteSingle}

View file

@ -6,6 +6,27 @@ import { usePrompts, usePromptOperations, Prompt } from '../../hooks/usePrompts'
import { useLanguage } from '../../contexts/LanguageContext'; import { useLanguage } from '../../contexts/LanguageContext';
import type { EditFieldConfig } from '../Popup/EditForm'; import type { EditFieldConfig } from '../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 { import type {
PromptsLogicReturn, PromptsLogicReturn,
PromptActionConfig, PromptActionConfig,
@ -15,7 +36,6 @@ import type {
export function usePromptsLogic(): PromptsLogicReturn { export function usePromptsLogic(): PromptsLogicReturn {
const { prompts, loading, error, refetch } = usePrompts(); const { prompts, loading, error, refetch } = usePrompts();
const { t } = useLanguage(); const { t } = useLanguage();
const { const {
handlePromptDelete, handlePromptDelete,
handlePromptUpdate, handlePromptUpdate,
@ -258,12 +278,26 @@ export function usePromptsLogic(): PromptsLogicReturn {
} }
}, },
{ {
label: t('prompts.action.delete', 'Delete'), label: (row: Prompt) => {
icon: (_row: Prompt) => { const isDeletable = isPromptDeletable(row);
return <IoIosTrash />; 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) => { onClick: (row: Prompt) => {
if (!deletingPrompts.has(row.id)) { const isDeletable = isPromptDeletable(row);
if (isDeletable && !deletingPrompts.has(row.id)) {
handleDeletePrompt(row); handleDeletePrompt(row);
} }
} }

View file

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

View file

@ -182,22 +182,11 @@ export function useWorkflowsLogic(): WorkflowsLogicReturn {
maxWidth: 180, maxWidth: 180,
sortable: true, sortable: true,
filterable: true, filterable: true,
formatter: (value: string | number | undefined) => { formatter: (value: number | undefined) => {
if (!value) return '-'; if (!value) return '-';
try { try {
let date: Date; // Backend sends UTC timestamp in seconds (float), convert to milliseconds for Date constructor
const date = new Date(value * 1000);
// 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);
}
// Check if date is valid // Check if date is valid
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
@ -220,22 +209,11 @@ export function useWorkflowsLogic(): WorkflowsLogicReturn {
maxWidth: 180, maxWidth: 180,
sortable: true, sortable: true,
filterable: true, filterable: true,
formatter: (value: string | number | undefined) => { formatter: (value: number | undefined) => {
if (!value) return '-'; if (!value) return '-';
try { try {
let date: Date; // Backend sends UTC timestamp in seconds (float), convert to milliseconds for Date constructor
const date = new Date(value * 1000);
// 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);
}
// Check if date is valid // Check if date is valid
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {

View file

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

View file

@ -1,17 +1,15 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useApiRequest } from './useApi'; import { useApiRequest } from './useApi';
// File interfaces // File interfaces - exactly matching backend FileItem model
export interface FileInfo { export interface FileInfo {
id: string; id: string;
filename: string; mandateId: string; // Required in backend
fileName: string; // Required in backend
mimeType: string; mimeType: string;
fileHash: string;
fileSize: number; fileSize: number;
creationDate: string; creationDate: number; // Backend uses float for UTC timestamp in seconds
fileHash?: string;
mandateId?: string;
workflowId?: string;
source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me'
} }
export interface UserFile { export interface UserFile {
@ -19,7 +17,7 @@ export interface UserFile {
file_name: string; file_name: string;
mime_type?: string; mime_type?: string;
action: string; action: string;
created_at: string; created_at: number |string;
size?: number; size?: number;
source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me' source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me'
} }
@ -31,13 +29,71 @@ export function useUserFiles() {
const fetchFiles = async () => { const fetchFiles = async () => {
try { try {
console.log('🔍 Fetching files from API...');
const data = await request({ const data = await request({
url: '/api/files/list', url: '/api/files/list',
method: 'get' method: 'get'
}); });
// Map API response to our frontend model console.log('📥 Raw API response:', data);
const mappedFiles = data.map((apiFile: FileInfo): UserFile => {
// 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 // Derive a simplified action from the MIME type
let action = 'Document'; let action = 'Document';
if (apiFile.mimeType) { 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 { return {
id: apiFile.id, id: apiFile.id,
file_name: apiFile.filename, file_name: apiFile.fileName, // Required field from backend
mime_type: apiFile.mimeType, mime_type: apiFile.mimeType, // Required field from backend
action: action, action: action,
created_at: apiFile.creationDate, created_at: createdAt,
size: apiFile.fileSize, size: apiFile.fileSize, // Required field from backend
source: apiFile.source source: 'user_uploaded' // Default source since workflowId is not part of FileItem model
}; };
}); });
console.log(`🎉 Successfully processed ${mappedFiles.length} files for display`);
setFiles(mappedFiles); setFiles(mappedFiles);
} catch (error) { } catch (error) {
// Error is already handled by useApiRequest // 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)); setDownloadingFiles(prev => new Set(prev).add(fileId));
try { try {
console.log(`📥 Starting download for file: ${fileName} (ID: ${fileId})`);
// Try to get the file download
const blob = await request({ const blob = await request({
url: `/api/files/${fileId}/download`, url: `/api/files/${fileId}/download`,
method: 'get', method: 'get',
// Override axios config for blob response // 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 // 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'); const link = document.createElement('a');
link.href = url; link.href = url;
link.setAttribute('download', fileName); link.setAttribute('download', fileName);
@ -146,7 +220,16 @@ export function useFileOperations() {
return true; return true;
} catch (error: any) { } 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; return false;
} finally { } finally {
setDownloadingFiles(prev => { setDownloadingFiles(prev => {
@ -167,16 +250,31 @@ export function useFileOperations() {
} }
try { try {
console.log(`🗑️ Starting delete for file ID: ${fileId}`);
await request({ await request({
url: `/api/files/${fileId}`, url: `/api/files/${fileId}`,
method: 'delete' method: 'delete'
}); });
console.log(`✅ Delete successful for file ID: ${fileId}`);
// Add a small delay to ensure backend has time to process // Add a small delay to ensure backend has time to process
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise(resolve => setTimeout(resolve, 300));
return true; return true;
} catch (error: any) { } 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 // If deletion failed and we optimistically removed it, we should refetch to restore the file
return false; return false;
} finally { } 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) => { const handleFileUpload = async (file: globalThis.File, workflowId?: string) => {
setUploadError(null); setUploadError(null);
setUploadingFile(true); setUploadingFile(true);
try { try {
console.log('📤 Starting file upload...', {
fileName: file.name,
fileSize: file.size,
fileType: file.type,
workflowId: workflowId
});
// Validate file before upload
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(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
@ -200,6 +322,25 @@ export function useFileOperations() {
formData.append('workflowId', workflowId); 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({ const fileData = await request({
url: '/api/files/upload', url: '/api/files/upload',
method: 'post', method: 'post',
@ -212,29 +353,66 @@ export function useFileOperations() {
} }
}); });
console.log('✅ Upload successful:', fileData);
return { success: true, fileData }; return { success: true, fileData };
} catch (error: any) { } catch (error: any) {
setUploadError(error.message); console.error('❌ Upload failed:', {
return { success: false, error: error.message }; 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 { } finally {
setUploadingFile(false); 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 setUploadError(null); // Reuse upload error state for update operations
try { try {
console.log(`✏️ Starting update for file ID: ${fileId}`, updateData);
const updatedFile = await request({ const updatedFile = await request({
url: `/api/files/${fileId}`, url: `/api/files/${fileId}`,
method: 'put', method: 'put',
data: updateData data: updateData
}); });
console.log(`✅ Update successful for file ID: ${fileId}`);
return { success: true, fileData: updatedFile }; return { success: true, fileData: updatedFile };
} catch (error: any) { } catch (error: any) {
setUploadError(error.message); console.error(`❌ Update failed for file ID ${fileId}:`, error);
return { success: false, error: error.message }; 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; mandateId: string;
content: string; content: string;
name: string; name: string;
_createdBy?: string; // Optional field to track who created the prompt
_hideDelete?: boolean; // Backend access control flag
} }
// Prompts list hook // Prompts list hook

View file

@ -391,6 +391,9 @@ export default {
'formgen.pagination.prev': 'Vorherige Seite', 'formgen.pagination.prev': 'Vorherige Seite',
'formgen.pagination.next': 'Nächste Seite', 'formgen.pagination.next': 'Nächste Seite',
'formgen.pagination.last': 'Letzte 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.multiple': 'Löschen ({count})',
'formgen.delete.single': 'Löschen', 'formgen.delete.single': 'Löschen',
'formgen.delete.confirm': 'Sind Sie sicher, dass Sie die {count} ausgewählten Elemente löschen möchten?', '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.edit': 'Bearbeiten',
'prompts.action.copy': 'Kopieren', 'prompts.action.copy': 'Kopieren',
'prompts.action.delete': 'Löschen', '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.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.delete.confirmMultiple': 'Sind Sie sicher, dass Sie {count} Prompts löschen möchten?',
'prompts.field.name': 'Prompt-Name', 'prompts.field.name': 'Prompt-Name',

View file

@ -394,6 +394,9 @@ export default {
'formgen.pagination.prev': 'Previous page', 'formgen.pagination.prev': 'Previous page',
'formgen.pagination.next': 'Next page', 'formgen.pagination.next': 'Next page',
'formgen.pagination.last': 'Last 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.multiple': 'Delete ({count})',
'formgen.delete.confirm_multiple': 'Are you sure you want to delete the {count} selected items?', '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.edit': 'Edit',
'prompts.action.copy': 'Copy', 'prompts.action.copy': 'Copy',
'prompts.action.delete': 'Delete', '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.confirm': 'Are you sure you want to delete "{name}"?',
'prompts.delete.confirmMultiple': 'Are you sure you want to delete {count} prompts?', 'prompts.delete.confirmMultiple': 'Are you sure you want to delete {count} prompts?',
'prompts.field.name': 'Prompt Name', 'prompts.field.name': 'Prompt Name',

View file

@ -394,6 +394,9 @@ export default {
'formgen.pagination.prev': 'Page précédente', 'formgen.pagination.prev': 'Page précédente',
'formgen.pagination.next': 'Page suivante', 'formgen.pagination.next': 'Page suivante',
'formgen.pagination.last': 'Dernière page', '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.multiple': 'Supprimer ({count})',
'formgen.delete.confirm_multiple': 'Êtes-vous sûr de vouloir supprimer les {count} éléments sélectionnés ?', '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.edit': 'Modifier',
'prompts.action.copy': 'Copier', 'prompts.action.copy': 'Copier',
'prompts.action.delete': 'Supprimer', '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.confirm': 'Êtes-vous sûr de vouloir supprimer "{name}" ?',
'prompts.delete.confirmMultiple': 'Êtes-vous sûr de vouloir supprimer {count} prompts ?', 'prompts.delete.confirmMultiple': 'Êtes-vous sûr de vouloir supprimer {count} prompts ?',
'prompts.field.name': 'Nom du prompt', 'prompts.field.name': 'Nom du prompt',