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_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"
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue