fixed timestamps
This commit is contained in:
parent
5a945b1364
commit
0848e98d5e
17 changed files with 474 additions and 122 deletions
2
.env
2
.env
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue