working on new frontend and backend integration
This commit is contained in:
parent
84764f932b
commit
860fbd51f0
105 changed files with 8638 additions and 6859 deletions
2890
package-lock.json
generated
2890
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,6 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
<<<<<<< Updated upstream
|
||||
"@azure/msal-browser": "^4.12.0",
|
||||
"@azure/msal-react": "^3.0.12",
|
||||
"@xstate/react": "^5.0.0",
|
||||
|
|
@ -24,10 +23,12 @@
|
|||
"jwt-decode": "^4.0.0",
|
||||
"motion": "^12.7.3",
|
||||
"pg": "^8.8.0",
|
||||
=======
|
||||
>>>>>>> Stashed changes
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-dom": "^19.1.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.7.1",
|
||||
"xstate": "^5.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.30.1",
|
||||
|
|
|
|||
16
src/App.tsx
16
src/App.tsx
|
|
@ -10,13 +10,16 @@ import Register from './pages/Register';
|
|||
import { AuthProvider } from './auth/authProvider';
|
||||
import { ProtectedRoute } from './auth/ProtectedRoute';
|
||||
import { LanguageProvider } from './contexts/LanguageContext';
|
||||
import Home from './pages/Home';
|
||||
import Dateien from './pages/Dateien/Dateien';
|
||||
import TeamBereich from './pages/TeamBereich/TeamBereich';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Einstellungen from './pages/Einstellungen/Einstellungen';
|
||||
import Home from './pages/Home/Home';
|
||||
import Dateien from './pages/Home/Dateien';
|
||||
import TeamBereich from './pages/Home/TeamBereich';
|
||||
import Dashboard from './pages/Home/Dashboard';
|
||||
import Einstellungen from './pages/Home/Einstellungen';
|
||||
// Import the global light theme CSS variables as default
|
||||
import './assets/styles/light.css';
|
||||
import Connections from './pages/Home/Connections';
|
||||
import Workflows from './pages/Home/Workflows';
|
||||
import TestSharepoint from './pages/Home/TestSharepoint';
|
||||
|
||||
function App() {
|
||||
// Load saved theme preference on app mount
|
||||
|
|
@ -49,7 +52,10 @@ function App() {
|
|||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="dateien" element={<Dateien />} />
|
||||
<Route path="team-bereich" element={<TeamBereich />} />
|
||||
<Route path="connections" element={<Connections />} />
|
||||
<Route path="workflows" element={<Workflows />} />
|
||||
<Route path="einstellungen" element={<Einstellungen />} />
|
||||
<Route path="testSharepoint" element={<TestSharepoint />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@
|
|||
--color-secondary-hover: #FF6A55;
|
||||
--color-secondary-disabled: #F5B0A4;
|
||||
|
||||
--color-red: #D85B65;
|
||||
--color-red-hover: #E77A81;
|
||||
--color-red-disabled: #F3C0C4;
|
||||
--color-red: #dc3545;
|
||||
--color-red-hover: #f5c6cb;
|
||||
--color-red-disabled: #f8d7da;
|
||||
|
||||
--color-secondary-red: #B94A55;
|
||||
--color-secondary-red-hover: #D46872;
|
||||
|
|
@ -40,9 +40,9 @@
|
|||
--color-secondary-hover: #FF715C;
|
||||
--color-secondary-disabled: #6E3E36;
|
||||
|
||||
--color-red: #FF6F7A;
|
||||
--color-red-hover: #FF8B94;
|
||||
--color-red-disabled: #80383E;
|
||||
--color-red: #dc3545;
|
||||
--color-red-hover: #f5c6cb;
|
||||
--color-red-disabled: #f8d7da;
|
||||
|
||||
--color-secondary-red: #D65D6A;
|
||||
--color-secondary-red-hover: #E17683;
|
||||
|
|
|
|||
61
src/components/Connections/ConnectionEditModal.module.css
Normal file
61
src/components/Connections/ConnectionEditModal.module.css
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
.editModal {
|
||||
/* Custom styling for the edit modal */
|
||||
}
|
||||
|
||||
.editForm {
|
||||
/* Form-specific styling within the modal */
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
/* Ensure proper spacing for form elements */
|
||||
.editForm :global(.fieldGroup) {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.editForm :global(.floatingLabelInput) {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
/* Style the readonly fields to blend better with the modal */
|
||||
.editForm :global(.readonlyField) {
|
||||
background-color: var(--color-bg);
|
||||
opacity: 0.8;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Enhance button styling within the modal */
|
||||
.editForm :global(.buttonGroup) {
|
||||
margin-top: 32px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.editForm :global(.saveButton) {
|
||||
background-color: var(--color-secondary);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.editForm :global(.saveButton:hover) {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.editForm :global(.cancelButton) {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 640px) {
|
||||
.editForm {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.editForm :global(.buttonGroup) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.editForm :global(.saveButton),
|
||||
.editForm :global(.cancelButton) {
|
||||
flex: 1;
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
50
src/components/Connections/ConnectionEditModal.tsx
Normal file
50
src/components/Connections/ConnectionEditModal.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { Popup, EditForm } from '../Popup';
|
||||
import styles from './ConnectionEditModal.module.css';
|
||||
import { ConnectionEditModalProps } from './interfaces';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export function ConnectionEditModal({
|
||||
isOpen,
|
||||
connection,
|
||||
fields,
|
||||
onSave,
|
||||
onCancel
|
||||
}: ConnectionEditModalProps) {
|
||||
const { t, isLoading } = useLanguage();
|
||||
|
||||
if (!connection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const authorityName = connection.authority?.charAt(0).toUpperCase() + connection.authority?.slice(1);
|
||||
|
||||
// Simplified approach for the title
|
||||
const baseTitle = t('connections.edit_connection_title', `Edit {authority} Connection`);
|
||||
const modalTitle = baseTitle.includes('{authority}')
|
||||
? baseTitle.replace('{authority}', authorityName || '')
|
||||
: `Edit ${authorityName} Connection`;
|
||||
|
||||
return (
|
||||
<Popup
|
||||
isOpen={isOpen}
|
||||
title={modalTitle}
|
||||
onClose={onCancel}
|
||||
size="medium"
|
||||
className={styles.editModal}
|
||||
>
|
||||
<EditForm
|
||||
data={connection}
|
||||
fields={fields}
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
saveButtonText={t('connections.update_connection', 'Update Connection')}
|
||||
cancelButtonText={t('common.cancel', 'Cancel')}
|
||||
showButtons={true}
|
||||
className={styles.editForm}
|
||||
/>
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConnectionEditModal;
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
.errorContainer {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: var(--color-red, #ef4444);
|
||||
padding: 12px 16px;
|
||||
background-color: var(--color-red-hover, rgba(239, 68, 68, 0.1));
|
||||
border: 1px solid var(--color-red, #ef4444);
|
||||
border-radius: 25px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.errorMessage:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.errorMessage strong {
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* Animation for error appearance */
|
||||
.errorMessage {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 480px) {
|
||||
.errorMessage {
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
41
src/components/Connections/ConnectionsErrorDisplay.tsx
Normal file
41
src/components/Connections/ConnectionsErrorDisplay.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import React from 'react';
|
||||
import styles from './ConnectionsErrorDisplay.module.css';
|
||||
import { ConnectionsErrorDisplayProps } from './interfaces';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export function ConnectionsErrorDisplay({
|
||||
error,
|
||||
connectError,
|
||||
disconnectError
|
||||
}: ConnectionsErrorDisplayProps) {
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Don't render anything if no errors
|
||||
if (!error && !connectError && !disconnectError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.errorContainer}>
|
||||
{error && (
|
||||
<div className={styles.errorMessage}>
|
||||
<strong>{t('connections.error', 'Error')}:</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{connectError && (
|
||||
<div className={styles.errorMessage}>
|
||||
<strong>{t('connections.connection_error', 'Connection Error')}:</strong> {connectError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{disconnectError && (
|
||||
<div className={styles.errorMessage}>
|
||||
<strong>{t('connections.disconnect_error', 'Disconnect Error')}:</strong> {disconnectError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConnectionsErrorDisplay;
|
||||
50
src/components/Connections/ConnectionsTable.module.css
Normal file
50
src/components/Connections/ConnectionsTable.module.css
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
.tableContainer {
|
||||
width: 100%;
|
||||
background: var(--color-bg);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.connectionsTable {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Override FormGenerator styles for connections-specific styling */
|
||||
.connectionsTable :global(.table) {
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.connectionsTable :global(.th) {
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.connectionsTable :global(.td) {
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
border-bottom: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.connectionsTable :global(.tr:hover) {
|
||||
background-color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.tableContainer {
|
||||
border-radius: 4px;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.tableContainer {
|
||||
margin: 0 -16px;
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
42
src/components/Connections/ConnectionsTable.tsx
Normal file
42
src/components/Connections/ConnectionsTable.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import React from 'react';
|
||||
import { FormGenerator } from '../FormGenerator';
|
||||
import styles from './ConnectionsTable.module.css';
|
||||
import { ConnectionsTableProps } from './interfaces';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
export function ConnectionsTable({
|
||||
connections,
|
||||
columns,
|
||||
actions,
|
||||
isLoading = false,
|
||||
isConnecting = false,
|
||||
isDisconnecting = false,
|
||||
onRowSelect
|
||||
}: ConnectionsTableProps) {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const loading = isLoading || isConnecting || isDisconnecting;
|
||||
|
||||
return (
|
||||
<div className={styles.tableContainer}>
|
||||
<FormGenerator
|
||||
data={connections}
|
||||
columns={columns}
|
||||
title={t('connections.service_connections', 'Service Connections')}
|
||||
loading={loading}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
resizable={true}
|
||||
pagination={true}
|
||||
pageSize={10}
|
||||
selectable={false}
|
||||
onRowSelect={onRowSelect}
|
||||
actions={actions}
|
||||
className={styles.connectionsTable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConnectionsTable;
|
||||
10
src/components/Connections/index.ts
Normal file
10
src/components/Connections/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Export all components
|
||||
export { ConnectionsTable } from './ConnectionsTable';
|
||||
export { ConnectionEditModal } from './ConnectionEditModal';
|
||||
export { ConnectionsErrorDisplay } from './ConnectionsErrorDisplay';
|
||||
|
||||
// Export logic hook
|
||||
export { useConnectionsLogic } from './logic';
|
||||
|
||||
// Export all interfaces and types
|
||||
export * from './interfaces';
|
||||
72
src/components/Connections/interfaces.ts
Normal file
72
src/components/Connections/interfaces.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { ColumnConfig } from '../FormGenerator';
|
||||
import { EditFieldConfig } from '../Popup';
|
||||
|
||||
// Re-export connection-related interfaces from hooks
|
||||
export type { Connection, CreateConnectionData } from '../../hooks/useConnections';
|
||||
|
||||
// Import React for component types
|
||||
import React from 'react';
|
||||
|
||||
// Component Props Interfaces
|
||||
export interface ConnectionsTableProps {
|
||||
connections: Connection[];
|
||||
columns: ColumnConfig[];
|
||||
actions: TableAction[];
|
||||
isLoading?: boolean;
|
||||
isConnecting?: boolean;
|
||||
isDisconnecting?: boolean;
|
||||
onRowSelect?: (selectedRows: Connection[]) => void;
|
||||
}
|
||||
|
||||
export interface ConnectionEditModalProps {
|
||||
isOpen: boolean;
|
||||
connection: Connection | null;
|
||||
fields: EditFieldConfig[];
|
||||
onSave: (updatedConnection: Connection) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export interface ConnectionsErrorDisplayProps {
|
||||
error?: string | null;
|
||||
connectError?: string | null;
|
||||
disconnectError?: string | null;
|
||||
}
|
||||
|
||||
// Table Action Interface
|
||||
export interface TableAction {
|
||||
label: string;
|
||||
onClick: (connection: Connection) => Promise<void> | void;
|
||||
icon: React.ReactNode | ((connection: Connection) => React.ReactNode);
|
||||
}
|
||||
|
||||
// Connection Status Types
|
||||
export type ConnectionStatus = 'active' | 'pending' | 'expired' | 'revoked';
|
||||
export type ConnectionAuthority = 'local' | 'google' | 'msft';
|
||||
|
||||
// Handler Function Types
|
||||
export interface ConnectionHandlers {
|
||||
handleCreateConnection: (type: 'msft' | 'google') => Promise<void>;
|
||||
handleConnect: (connection: Connection) => Promise<void>;
|
||||
handleDisconnect: (connection: Connection) => Promise<void>;
|
||||
handleDelete: (connection: Connection) => Promise<void>;
|
||||
handleConnectOrDisconnect: (connection: Connection) => Promise<void>;
|
||||
handleEditConnection: (connection: Connection) => Promise<void>;
|
||||
handleSaveConnection: (updatedConnection: Connection) => Promise<void>;
|
||||
handleCancelEdit: () => void;
|
||||
}
|
||||
|
||||
// Hook Return Types
|
||||
export interface ConnectionsLogicReturn extends ConnectionHandlers {
|
||||
connections: Connection[];
|
||||
isLoading: boolean;
|
||||
isConnecting: boolean;
|
||||
isDisconnecting: boolean;
|
||||
error: string | null;
|
||||
connectError: string | null;
|
||||
disconnectError: string | null;
|
||||
editPopupOpen: boolean;
|
||||
editingConnection: Connection | null;
|
||||
connectionColumns: ColumnConfig[];
|
||||
connectionEditFields: EditFieldConfig[];
|
||||
tableActions: TableAction[];
|
||||
}
|
||||
339
src/components/Connections/logic.tsx
Normal file
339
src/components/Connections/logic.tsx
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { IoIosLink, IoIosTrash } from 'react-icons/io';
|
||||
import { MdModeEdit } from 'react-icons/md';
|
||||
import { GoUnlink } from 'react-icons/go';
|
||||
import React from 'react';
|
||||
|
||||
import { useConnections, useOAuthConnect, useDisconnect } from '../../hooks/useConnections';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { ColumnConfig } from '../FormGenerator';
|
||||
import { EditFieldConfig } from '../Popup';
|
||||
import {
|
||||
Connection,
|
||||
CreateConnectionData,
|
||||
ConnectionsLogicReturn,
|
||||
TableAction
|
||||
} from './interfaces';
|
||||
|
||||
export function useConnectionsLogic(): ConnectionsLogicReturn {
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Hooks
|
||||
const {
|
||||
connections,
|
||||
fetchConnections,
|
||||
createConnection,
|
||||
updateConnection,
|
||||
connectService,
|
||||
disconnectService,
|
||||
deleteConnection,
|
||||
isLoading,
|
||||
error
|
||||
} = useConnections();
|
||||
|
||||
const {
|
||||
connectWithPopup,
|
||||
isConnecting,
|
||||
error: connectError
|
||||
} = useOAuthConnect();
|
||||
|
||||
const {
|
||||
disconnect,
|
||||
isDisconnecting,
|
||||
error: disconnectError
|
||||
} = useDisconnect();
|
||||
|
||||
// Local state
|
||||
const [editPopupOpen, setEditPopupOpen] = useState(false);
|
||||
const [editingConnection, setEditingConnection] = useState<Connection | null>(null);
|
||||
|
||||
// Define field configuration for editing connections
|
||||
const connectionEditFields: EditFieldConfig[] = [
|
||||
{
|
||||
key: 'authority',
|
||||
label: t('connections.field.service', 'Service'),
|
||||
type: 'readonly',
|
||||
editable: false,
|
||||
formatter: (value: string) => {
|
||||
if (!value) return t('connections.unknown', 'Unknown');
|
||||
const labels = {
|
||||
'google': t('connections.service.google', 'Google'),
|
||||
'msft': t('connections.service.microsoft', 'Microsoft'),
|
||||
'local': t('connections.service.local', 'Local')
|
||||
};
|
||||
return labels[value as keyof typeof labels] || value;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: t('connections.field.status', 'Status'),
|
||||
type: 'readonly',
|
||||
editable: false,
|
||||
formatter: (value: string) => value ? value.charAt(0).toUpperCase() + value.slice(1) : t('connections.unknown', 'Unknown')
|
||||
},
|
||||
{
|
||||
key: 'externalUsername',
|
||||
label: t('connections.field.external_username', 'External Username'),
|
||||
type: 'string',
|
||||
editable: true,
|
||||
required: false,
|
||||
placeholder: t('connections.placeholder.external_username', 'Enter external username')
|
||||
},
|
||||
{
|
||||
key: 'externalEmail',
|
||||
label: t('connections.field.external_email', 'External Email'),
|
||||
type: 'email',
|
||||
editable: true,
|
||||
required: false,
|
||||
placeholder: t('connections.placeholder.external_email', 'Enter external email address')
|
||||
},
|
||||
{
|
||||
key: 'connectedAt',
|
||||
label: t('connections.field.connected_at', 'Connected At'),
|
||||
type: 'readonly',
|
||||
editable: false,
|
||||
formatter: (value: string) => {
|
||||
if (!value) return t('connections.not_available', 'N/A');
|
||||
try {
|
||||
return new Date(value).toLocaleString();
|
||||
} catch {
|
||||
return t('connections.invalid_date', 'Invalid Date');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'lastChecked',
|
||||
label: t('connections.field.last_checked', 'Last Checked'),
|
||||
type: 'readonly',
|
||||
editable: false,
|
||||
formatter: (value: string) => {
|
||||
if (!value) return t('connections.not_available', 'N/A');
|
||||
try {
|
||||
return new Date(value).toLocaleString();
|
||||
} catch {
|
||||
return t('connections.invalid_date', 'Invalid Date');
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Define custom columns for the connections table
|
||||
const connectionColumns: ColumnConfig[] = [
|
||||
{
|
||||
key: 'authority',
|
||||
label: t('connections.field.service', 'Service'),
|
||||
type: 'enum',
|
||||
filterOptions: ['google', 'msft', 'local'],
|
||||
formatter: (value: string) => {
|
||||
if (!value) return t('connections.unknown', 'Unknown');
|
||||
const labels = {
|
||||
'google': t('connections.service.google', 'Google'),
|
||||
'msft': t('connections.service.microsoft', 'Microsoft'),
|
||||
'local': t('connections.service.local', 'Local')
|
||||
};
|
||||
return labels[value as keyof typeof labels] || value;
|
||||
},
|
||||
width: 150,
|
||||
sortable: true,
|
||||
filterable: true
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: t('connections.field.status', 'Status'),
|
||||
type: 'enum',
|
||||
filterOptions: ['active', 'pending', 'expired', 'revoked'],
|
||||
formatter: (value: string) => {
|
||||
return value?.charAt(0).toUpperCase() + value?.slice(1) || t('connections.unknown', 'Unknown');
|
||||
},
|
||||
width: 120,
|
||||
sortable: true,
|
||||
filterable: true
|
||||
},
|
||||
{
|
||||
key: 'externalUsername',
|
||||
label: t('connections.field.external_username', 'External Username'),
|
||||
type: 'string',
|
||||
width: 200,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
searchable: true
|
||||
},
|
||||
{
|
||||
key: 'externalEmail',
|
||||
label: t('connections.field.external_email', 'External Email'),
|
||||
type: 'string',
|
||||
width: 250,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
searchable: true
|
||||
},
|
||||
{
|
||||
key: 'connectedAt',
|
||||
label: t('connections.field.connected_at', 'Connected At'),
|
||||
type: 'date',
|
||||
width: 150,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
searchable: true
|
||||
},
|
||||
{
|
||||
key: 'lastChecked',
|
||||
label: t('connections.field.last_checked', 'Last Checked'),
|
||||
type: 'date',
|
||||
width: 150,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
searchable: true
|
||||
},
|
||||
{
|
||||
key: 'expiresAt',
|
||||
label: t('connections.field.expires_at', 'Expires At'),
|
||||
type: 'date',
|
||||
width: 150,
|
||||
sortable: true,
|
||||
filterable: true
|
||||
}
|
||||
];
|
||||
|
||||
// Fetch connections on mount
|
||||
useEffect(() => {
|
||||
fetchConnections();
|
||||
}, []);
|
||||
|
||||
// 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);
|
||||
try {
|
||||
await connectWithPopup(connection.id);
|
||||
await fetchConnections();
|
||||
} catch (error) {
|
||||
console.error('Error connecting to service:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = async (connection: Connection) => {
|
||||
console.log('Disconnecting from service:', connection);
|
||||
try {
|
||||
await disconnect(connection.id);
|
||||
await fetchConnections();
|
||||
} catch (error) {
|
||||
console.error('Error disconnecting from service:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (connection: Connection) => {
|
||||
const serviceName = connection.authority?.charAt(0).toUpperCase() + connection.authority?.slice(1) || t('connections.unknown', 'Unknown');
|
||||
const confirmMessage = t('connections.confirm_delete', 'Are you sure you want to delete the {service} connection?').replace('{service}', serviceName);
|
||||
|
||||
if (window.confirm(confirmMessage)) {
|
||||
try {
|
||||
await deleteConnection(connection.id);
|
||||
await fetchConnections();
|
||||
} catch (error) {
|
||||
console.error('Error deleting connection:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnectOrDisconnect = async (connection: Connection) => {
|
||||
if (connection.status === 'active') {
|
||||
await handleDisconnect(connection);
|
||||
} else {
|
||||
await handleConnect(connection);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditConnection = async (connection: Connection) => {
|
||||
console.log('Editing connection:', connection);
|
||||
setEditingConnection(connection);
|
||||
setEditPopupOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveConnection = async (updatedConnection: Connection) => {
|
||||
if (!editingConnection) return;
|
||||
|
||||
try {
|
||||
const updateData = {
|
||||
externalUsername: updatedConnection.externalUsername,
|
||||
externalEmail: updatedConnection.externalEmail
|
||||
};
|
||||
|
||||
await updateConnection(editingConnection.id, updateData);
|
||||
console.log('Connection updated successfully');
|
||||
await fetchConnections();
|
||||
setEditPopupOpen(false);
|
||||
setEditingConnection(null);
|
||||
} catch (error) {
|
||||
console.error('Error updating connection:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditPopupOpen(false);
|
||||
setEditingConnection(null);
|
||||
};
|
||||
|
||||
// Table actions
|
||||
const tableActions: TableAction[] = [
|
||||
{
|
||||
label: t('connections.action.edit', 'Edit'),
|
||||
onClick: handleEditConnection,
|
||||
icon: <MdModeEdit />
|
||||
},
|
||||
{
|
||||
label: t('connections.action.toggle_connection', 'Toggle Connection'),
|
||||
onClick: handleConnectOrDisconnect,
|
||||
icon: (connection: Connection) => connection.status === 'active' ? <GoUnlink /> : <IoIosLink />
|
||||
},
|
||||
{
|
||||
label: t('connections.action.delete', 'Delete'),
|
||||
onClick: handleDelete,
|
||||
icon: <IoIosTrash />
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
// Data
|
||||
connections,
|
||||
isLoading,
|
||||
isConnecting,
|
||||
isDisconnecting,
|
||||
error,
|
||||
connectError,
|
||||
disconnectError,
|
||||
editPopupOpen,
|
||||
editingConnection,
|
||||
connectionColumns,
|
||||
connectionEditFields,
|
||||
tableActions,
|
||||
|
||||
// Handlers
|
||||
handleCreateConnection,
|
||||
handleConnect,
|
||||
handleDisconnect,
|
||||
handleDelete,
|
||||
handleConnectOrDisconnect,
|
||||
handleEditConnection,
|
||||
handleSaveConnection,
|
||||
handleCancelEdit
|
||||
};
|
||||
}
|
||||
|
|
@ -1,13 +1,10 @@
|
|||
import React, { useState } from "react";
|
||||
import { BsArrowsAngleExpand, BsArrowsAngleContract } from "react-icons/bs";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Prompt } from "../../../hooks/usePrompts";
|
||||
import { useLanguage } from '../../../contexts/LanguageContext';
|
||||
|
||||
import DashboardChatArea from './DashboardChatArea/DashboardChatArea';
|
||||
import DashboardChatHistory from './DashboardChatHistory/DashboardChatHistory';
|
||||
import DashboardChatArea from './DashboardChatArea';
|
||||
|
||||
import styles from './DashboardChat.module.css';
|
||||
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
||||
|
||||
interface DashboardChatProps {
|
||||
isExpanded: boolean;
|
||||
|
|
@ -20,8 +17,6 @@ interface DashboardChatProps {
|
|||
}
|
||||
|
||||
const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
selectedPrompt,
|
||||
onPromptUsed,
|
||||
onWorkflowIdChange,
|
||||
|
|
|
|||
|
|
@ -25,13 +25,20 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
|||
// Workflow state
|
||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(resumeWorkflowId || null);
|
||||
|
||||
// Update current workflow ID when resumeWorkflowId changes
|
||||
React.useEffect(() => {
|
||||
if (resumeWorkflowId !== currentWorkflowId) {
|
||||
setCurrentWorkflowId(resumeWorkflowId);
|
||||
}
|
||||
}, [resumeWorkflowId, currentWorkflowId]);
|
||||
|
||||
// Handle workflow ID changes
|
||||
const handleWorkflowIdChange = (workflowId: string | null) => {
|
||||
const handleWorkflowIdChange = React.useCallback((workflowId: string | null) => {
|
||||
setCurrentWorkflowId(workflowId);
|
||||
if (onWorkflowIdChange) {
|
||||
onWorkflowIdChange(workflowId);
|
||||
}
|
||||
};
|
||||
}, [onWorkflowIdChange]);
|
||||
|
||||
// Handle resizing
|
||||
const handleMouseDown = (direction: 'horizontal' | 'vertical') => (e: React.MouseEvent) => {
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
import React from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { WorkflowStatusDisplayProps } from "./dashboardChatAreaTypes";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
|
||||
const WorkflowStatusDisplay: React.FC<WorkflowStatusDisplayProps> = ({
|
||||
currentWorkflowId,
|
||||
workflowStatus,
|
||||
workflowCompleted,
|
||||
onStartNewWorkflow,
|
||||
handleRetry,
|
||||
shouldShowRetryButton
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{currentWorkflowId && shouldShowRetryButton() && (
|
||||
<motion.div
|
||||
className={styles.workflow_status}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.retry_container}>
|
||||
<span className={styles.failed_message}>
|
||||
{t('chat.workflow_failed')}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
className={styles.retry_button}
|
||||
>
|
||||
{t('chat.retry_workflow')}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
{currentWorkflowId && !workflowCompleted && !shouldShowRetryButton() && (
|
||||
<motion.div
|
||||
className={styles.workflow_status}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div
|
||||
className={styles.dots_container}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '4px', height: '20px' }}
|
||||
>
|
||||
<motion.span
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
y: [0, -7, 0]
|
||||
}}
|
||||
exit={{ opacity: 0, scale: 0 }}
|
||||
transition={{
|
||||
opacity: { duration: 0.4, ease: "easeOut" },
|
||||
scale: { duration: 0.4, ease: "easeOut" },
|
||||
y: {
|
||||
duration: 1.2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
times: [0, 0.5, 1],
|
||||
delay: 0.2
|
||||
}
|
||||
}}
|
||||
style={{ fontSize: '20px', display: 'inline-block' }}
|
||||
>
|
||||
•
|
||||
</motion.span>
|
||||
<motion.span
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
y: [0, -7, 0]
|
||||
}}
|
||||
exit={{ opacity: 0, scale: 0 }}
|
||||
transition={{
|
||||
opacity: { duration: 0.4, ease: "easeOut", delay: 0.1 },
|
||||
scale: { duration: 0.4, ease: "easeOut", delay: 0.1 },
|
||||
y: {
|
||||
duration: 1.2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
times: [0, 0.5, 1],
|
||||
delay: 0.4
|
||||
}
|
||||
}}
|
||||
style={{ fontSize: '20px', display: 'inline-block' }}
|
||||
>
|
||||
•
|
||||
</motion.span>
|
||||
<motion.span
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
y: [0, -7, 0]
|
||||
}}
|
||||
exit={{ opacity: 0, scale: 0 }}
|
||||
transition={{
|
||||
opacity: { duration: 0.4, ease: "easeOut", delay: 0.2 },
|
||||
scale: { duration: 0.4, ease: "easeOut", delay: 0.2 },
|
||||
y: {
|
||||
duration: 1.2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
times: [0, 0.5, 1],
|
||||
delay: 0.6
|
||||
}
|
||||
}}
|
||||
style={{ fontSize: '20px', display: 'inline-block' }}
|
||||
>
|
||||
•
|
||||
</motion.span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowStatusDisplay;
|
||||
|
|
@ -1,536 +0,0 @@
|
|||
.chat_area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.chat_messages {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 15px;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.chat_messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat_messages::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.chat_messages::-webkit-scrollbar-thumb {
|
||||
background: var(--color-gray-disabled);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat_messages::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-gray);
|
||||
}
|
||||
|
||||
.messages_container {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
}
|
||||
|
||||
.messages_spacer {
|
||||
flex: 1;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.chat_input {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.chat_input.drag_over {
|
||||
background-color: var(--color-secondary-disabled);
|
||||
border: 2px dashed var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.input_row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message_input {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 12px;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.message_input:focus {
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.message_input:disabled {
|
||||
background-color: var(--color-surface);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.attachment_button {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
background-color: var(--color-secondary-disabled);
|
||||
color: var(--color-secondary);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.attachment_button:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
.attachment_button:disabled {
|
||||
background-color: var(--color-surface);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.attached_files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.attached_file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background-color: var(--color-secondary-disabled);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-secondary);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.attached_file_icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.attached_file_name {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.attached_file_remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-gray);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.attached_file_remove:hover {
|
||||
background-color: var(--color-gray-disabled);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.send_button {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.send_button:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.send_button_icon {
|
||||
height: 60%;
|
||||
width: 60%;
|
||||
margin: none;
|
||||
padding: none;
|
||||
}
|
||||
|
||||
.send_button:disabled {
|
||||
background-color: var(--color-gray-disabled);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
}
|
||||
|
||||
.stop_button {
|
||||
padding: 12px 12px;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
background-color: var(--color-red);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.stop_button:hover {
|
||||
background-color: var(--color-red-hover);
|
||||
}
|
||||
|
||||
.stop_button:disabled {
|
||||
background-color: var(--color-gray-disabled);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.loading_message {
|
||||
padding: 10px;
|
||||
background-color: var(--color-secondary-disabled);
|
||||
border-left: 4px solid var(--color-secondary);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.loading_message p {
|
||||
margin: 0;
|
||||
color: var(--color-secondary);
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.error_message {
|
||||
padding: 10px;
|
||||
background-color: var(--color-red-disabled);
|
||||
border-left: 4px solid var(--color-red);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.error_message p {
|
||||
margin: 0;
|
||||
color: var(--color-red);
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
max-width: 80%;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.message_user {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.message_assistant {
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message_system {
|
||||
background-color: var(--color-primary-disabled);
|
||||
color: var(--color-primary);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message_role {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.8;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.message_content {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.message_timestamp {
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
opacity: 0.6;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.placeholder_text {
|
||||
text-align: center;
|
||||
color: var(--color-gray);
|
||||
font-style: italic;
|
||||
margin: 20px 0;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.workflow_status {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.workflow_status p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--color-gray);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.retry_container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background-color: var(--color-red-disabled);
|
||||
border: 1px solid var(--color-red);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.failed_message {
|
||||
font-size: 14px;
|
||||
color: var(--color-red);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.retry_button {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family);
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.retry_button:hover {
|
||||
background-color: var(--color-primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.retry_button:disabled {
|
||||
background-color: var(--color-gray-disabled);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.completion_message {
|
||||
padding: 10px 12px;
|
||||
background-color: var(--color-secondary-disabled);
|
||||
border-left: 4px solid var(--color-secondary);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.completion_message p {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--color-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.new_workflow_button {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.new_workflow_button:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.message_documents {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.document_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.document_item:hover {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.message_assistant .document_item {
|
||||
background-color: var(--color-bg);
|
||||
border-color: var(--color-gray-disabled);
|
||||
}
|
||||
|
||||
.message_assistant .document_item:hover {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.document_icon {
|
||||
font-size: 16px;
|
||||
color: var(--color-secondary);
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.document_info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.document_name {
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.document_meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--color-gray);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.document_size {
|
||||
font-size: 11px;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.document_type {
|
||||
font-size: 11px;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.document_actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.document_item:hover .document_actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.document_action_button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-gray);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.document_action_button:hover {
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.message_assistant .document_action_button {
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.message_assistant .document_action_button:hover {
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
|
||||
import { useChatLogic } from "./dashboardChatAreaLogic";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import MessageList from "./DashboardChatAreaMessageList";
|
||||
import ChatInput from "./DashboardChatAreaInput";
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
|
||||
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||
selectedPrompt,
|
||||
onPromptUsed,
|
||||
onWorkflowIdChange,
|
||||
onWorkflowCompletedChange,
|
||||
resumeWorkflowId
|
||||
}) => {
|
||||
const {
|
||||
// State
|
||||
inputValue,
|
||||
setInputValue,
|
||||
currentWorkflowId,
|
||||
workflowCompleted,
|
||||
attachedFiles,
|
||||
|
||||
// Refs
|
||||
inputRef,
|
||||
messagesEndRef,
|
||||
|
||||
// Data from hooks
|
||||
messages,
|
||||
messagesLoading,
|
||||
messagesError,
|
||||
startingWorkflow,
|
||||
startError,
|
||||
workflowStatus,
|
||||
|
||||
// Handlers
|
||||
handleSend,
|
||||
handleKeyPress,
|
||||
startNewWorkflow,
|
||||
handleStopWorkflow,
|
||||
handleFileAttach,
|
||||
handleFileRemove,
|
||||
handleFilesSelect,
|
||||
handleRetry,
|
||||
|
||||
// Workflow state
|
||||
isWorkflowRunning,
|
||||
isStoppingWorkflow,
|
||||
shouldShowRetryButton
|
||||
} = useChatLogic({
|
||||
selectedPrompt,
|
||||
onPromptUsed,
|
||||
onWorkflowIdChange,
|
||||
resumeWorkflowId
|
||||
});
|
||||
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Notify parent component when workflow completion status changes
|
||||
useEffect(() => {
|
||||
if (onWorkflowCompletedChange) {
|
||||
onWorkflowCompletedChange(workflowCompleted);
|
||||
}
|
||||
}, [workflowCompleted, onWorkflowCompletedChange]);
|
||||
|
||||
const placeholder = workflowCompleted ? t('chat.continue_conversation') : t('chat.enter_message');
|
||||
|
||||
return (
|
||||
<div className={styles.chat_area}>
|
||||
<MessageList
|
||||
messages={messages}
|
||||
currentWorkflowId={currentWorkflowId}
|
||||
workflowStatus={workflowStatus}
|
||||
workflowCompleted={workflowCompleted}
|
||||
startingWorkflow={startingWorkflow}
|
||||
startError={startError}
|
||||
messagesError={messagesError}
|
||||
messagesLoading={messagesLoading}
|
||||
onStartNewWorkflow={startNewWorkflow}
|
||||
messagesEndRef={messagesEndRef}
|
||||
handleRetry={handleRetry}
|
||||
shouldShowRetryButton={shouldShowRetryButton}
|
||||
/>
|
||||
<ChatInput
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
onSend={handleSend}
|
||||
onKeyPress={handleKeyPress}
|
||||
isDisabled={startingWorkflow}
|
||||
placeholder={placeholder}
|
||||
inputRef={inputRef}
|
||||
isWorkflowRunning={isWorkflowRunning}
|
||||
onStopWorkflow={handleStopWorkflow}
|
||||
isStoppingWorkflow={isStoppingWorkflow}
|
||||
attachedFiles={attachedFiles}
|
||||
onFileAttach={handleFileAttach}
|
||||
onFileRemove={handleFileRemove}
|
||||
onFilesSelect={handleFilesSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardChatArea;
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { LuSendHorizontal } from "react-icons/lu";
|
||||
import { FaStop } from "react-icons/fa";
|
||||
import { IoAttach, IoClose } from "react-icons/io5";
|
||||
import { ChatInputProps } from "./dashboardChatAreaTypes";
|
||||
import { FileInfo } from "../../../../hooks/useFiles";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import DateienSelector from "../../../Dateien/DateienHinzufügen/DateienSelector";
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
|
||||
// Helper function to get file icon based on type
|
||||
const getFileIcon = (mimeType?: string): string => {
|
||||
if (!mimeType) return '📄';
|
||||
|
||||
const type = mimeType.toLowerCase();
|
||||
|
||||
if (type.includes('image')) return '🖼️';
|
||||
if (type.includes('video')) return '🎥';
|
||||
if (type.includes('audio')) return '🎵';
|
||||
if (type.includes('pdf')) return '📕';
|
||||
if (type.includes('word') || type.includes('document')) return '📘';
|
||||
if (type.includes('excel') || type.includes('spreadsheet')) return '📊';
|
||||
if (type.includes('powerpoint') || type.includes('presentation')) return '📋';
|
||||
if (type.includes('text')) return '📝';
|
||||
if (type.includes('zip') || type.includes('archive')) return '📦';
|
||||
if (type.includes('javascript') || type.includes('json') || type.includes('html') || type.includes('css')) return '💻';
|
||||
|
||||
return '📄';
|
||||
};
|
||||
|
||||
const ChatInput: React.FC<ChatInputProps> = ({
|
||||
inputValue,
|
||||
setInputValue,
|
||||
onSend,
|
||||
onKeyPress,
|
||||
isDisabled,
|
||||
placeholder,
|
||||
inputRef,
|
||||
isWorkflowRunning,
|
||||
onStopWorkflow,
|
||||
isStoppingWorkflow,
|
||||
attachedFiles,
|
||||
onFileAttach,
|
||||
onFileRemove,
|
||||
onFilesSelect
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
// Auto-resize textarea functionality
|
||||
useEffect(() => {
|
||||
if (inputRef?.current) {
|
||||
const textarea = inputRef.current;
|
||||
textarea.style.height = 'auto';
|
||||
|
||||
// Calculate line height - approximately 1.5em per line
|
||||
const lineHeight = 24; // Adjust this value based on your CSS line-height
|
||||
const maxHeight = lineHeight * 8; // 8 lines maximum
|
||||
|
||||
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
|
||||
// Enable/disable scroll based on content height
|
||||
if (textarea.scrollHeight > maxHeight) {
|
||||
textarea.style.overflowY = 'auto';
|
||||
} else {
|
||||
textarea.style.overflowY = 'hidden';
|
||||
}
|
||||
}
|
||||
}, [inputValue, inputRef]);
|
||||
|
||||
const handleAttachmentClick = () => {
|
||||
setIsUploadModalOpen(true);
|
||||
};
|
||||
|
||||
const handleFilesSelected = (files: FileInfo[]) => {
|
||||
onFilesSelect(files);
|
||||
setIsUploadModalOpen(false);
|
||||
};
|
||||
|
||||
const handleFileRemove = (fileId: number) => {
|
||||
onFileRemove(fileId);
|
||||
};
|
||||
|
||||
// Handle Enter key press for sending message (without Shift)
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (!isDisabled && (inputValue.trim() || attachedFiles.length > 0)) {
|
||||
onSend();
|
||||
}
|
||||
}
|
||||
// Call original onKeyPress if it exists (for backward compatibility)
|
||||
if (onKeyPress && e.key !== 'Enter') {
|
||||
onKeyPress(e as any);
|
||||
}
|
||||
};
|
||||
|
||||
// Drag and drop handlers
|
||||
const handleDragEnter = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isDisabled && !isWorkflowRunning) {
|
||||
setIsDragOver(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Only set drag over to false if we're leaving the entire input area
|
||||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||||
setIsDragOver(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
|
||||
if (isDisabled || isWorkflowRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length > 0) {
|
||||
// Convert File objects to FileInfo objects
|
||||
const fileInfos: FileInfo[] = files.map((file, index) => ({
|
||||
id: Date.now() + index, // Generate unique IDs
|
||||
name: file.name,
|
||||
mimeType: file.type,
|
||||
size: file.size,
|
||||
creationDate: new Date().toISOString(),
|
||||
source: 'user_uploaded'
|
||||
}));
|
||||
|
||||
onFilesSelect(fileInfos);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`${styles.chat_input} ${isDragOver ? styles.drag_over : ''}`}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{/* Show attached files if any */}
|
||||
{attachedFiles.length > 0 && (
|
||||
<div className={styles.attached_files}>
|
||||
{attachedFiles.map((file) => (
|
||||
<div key={file.id} className={styles.attached_file}>
|
||||
<span className={styles.attached_file_icon}>
|
||||
{getFileIcon(file.mimeType)}
|
||||
</span>
|
||||
<span className={styles.attached_file_name}>
|
||||
{file.name}
|
||||
</span>
|
||||
<button
|
||||
className={styles.attached_file_remove}
|
||||
onClick={() => handleFileRemove(file.id)}
|
||||
title={t('chat.remove_file')}
|
||||
>
|
||||
<IoClose size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input row with text input, attachment button, and send button */}
|
||||
<div className={styles.input_row}>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className={styles.message_input}
|
||||
disabled={isDisabled}
|
||||
rows={1}
|
||||
style={{
|
||||
resize: 'none',
|
||||
minHeight: '24px',
|
||||
lineHeight: '24px'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Attachment button */}
|
||||
<motion.button
|
||||
className={styles.attachment_button}
|
||||
onClick={handleAttachmentClick}
|
||||
disabled={isDisabled || isWorkflowRunning}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
title={t('chat.attach_file')}
|
||||
>
|
||||
<IoAttach size={26} />
|
||||
</motion.button>
|
||||
|
||||
{/* Send/Stop button */}
|
||||
<motion.button
|
||||
className={isWorkflowRunning ? styles.stop_button : styles.send_button}
|
||||
onClick={isWorkflowRunning ? onStopWorkflow : onSend}
|
||||
disabled={isWorkflowRunning ? isStoppingWorkflow : (isDisabled || (!inputValue.trim() && attachedFiles.length === 0))}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
>
|
||||
{isWorkflowRunning ? (
|
||||
<FaStop className={styles.send_button_icon}/>
|
||||
) : (
|
||||
<LuSendHorizontal className={styles.send_button_icon}/>
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{/* Upload Modal */}
|
||||
<DateienSelector
|
||||
isOpen={isUploadModalOpen}
|
||||
onClose={() => setIsUploadModalOpen(false)}
|
||||
onFilesSelected={handleFilesSelected}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatInput;
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import { FaDownload } from "react-icons/fa";
|
||||
import { MdOutlineRemoveRedEye } from "react-icons/md";
|
||||
import { Message, Document } from "./dashboardChatAreaTypes";
|
||||
import FilePreviewPopup from "./FilePreviewPopup";
|
||||
import { useFileDownload } from "../../../../hooks/useWorkflows";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
|
||||
interface MessageItemProps {
|
||||
message: Message;
|
||||
index: number;
|
||||
}
|
||||
|
||||
// Helper function to format file size
|
||||
const formatFileSize = (bytes?: number): string => {
|
||||
if (!bytes) return '';
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// Helper function to get file icon based on type or extension
|
||||
const getFileIcon = (type?: string, ext?: string): string => {
|
||||
// Use extension first if available, then fall back to MIME type
|
||||
const extension = ext?.toLowerCase();
|
||||
const mimeType = type?.toLowerCase();
|
||||
|
||||
// Check extension first
|
||||
if (extension) {
|
||||
// Images
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) return '🖼️';
|
||||
// Videos
|
||||
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) return '🎥';
|
||||
// Audio
|
||||
if (['mp3', 'wav', 'aac', 'flac', 'ogg', 'wma'].includes(extension)) return '🎵';
|
||||
// Documents
|
||||
if (extension === 'pdf') return '📕';
|
||||
if (['doc', 'docx'].includes(extension)) return '📘';
|
||||
if (['xls', 'xlsx'].includes(extension)) return '📊';
|
||||
if (['ppt', 'pptx'].includes(extension)) return '📋';
|
||||
if (['txt', 'md', 'rtf'].includes(extension)) return '📝';
|
||||
// Archives
|
||||
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) return '📦';
|
||||
// Code files
|
||||
if (['js', 'ts', 'jsx', 'tsx', 'html', 'css', 'py', 'java', 'cpp', 'c', 'php'].includes(extension)) return '💻';
|
||||
}
|
||||
|
||||
// Fall back to MIME type if extension didn't match
|
||||
if (mimeType) {
|
||||
if (mimeType.includes('image')) return '🖼️';
|
||||
if (mimeType.includes('video')) return '🎥';
|
||||
if (mimeType.includes('audio')) return '🎵';
|
||||
if (mimeType.includes('pdf')) return '📕';
|
||||
if (mimeType.includes('text')) return '📝';
|
||||
if (mimeType.includes('word') || mimeType.includes('document')) return '📘';
|
||||
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return '📊';
|
||||
if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return '📋';
|
||||
if (mimeType.includes('zip') || mimeType.includes('archive')) return '📦';
|
||||
}
|
||||
|
||||
return '📄';
|
||||
};
|
||||
|
||||
const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
|
||||
const { t } = useLanguage();
|
||||
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
const { downloadFile, isDownloading, error: downloadError } = useFileDownload();
|
||||
|
||||
|
||||
|
||||
const handleDocumentClick = (document: Document) => {
|
||||
// If there's a downloadUrl, use it; otherwise try the url
|
||||
const downloadLink = document.downloadUrl || document.url;
|
||||
|
||||
if (downloadLink) {
|
||||
// Open the document in a new tab
|
||||
window.open(downloadLink, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreview = (document: Document, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Use fileId if available, otherwise try to use id as fallback
|
||||
const fileId = document.fileId || document.id;
|
||||
|
||||
if (!fileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPreviewDocument(document);
|
||||
setIsPreviewOpen(true);
|
||||
};
|
||||
|
||||
const handleClosePreview = () => {
|
||||
setIsPreviewOpen(false);
|
||||
setPreviewDocument(null);
|
||||
};
|
||||
|
||||
const handleDownload = async (document: Document, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Use fileId if available, otherwise try to use id as fallback
|
||||
const fileId = document.fileId || document.id;
|
||||
|
||||
if (!fileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct filename with extension if available
|
||||
const fileName = document.ext ? `${document.name}.${document.ext}` : document.name;
|
||||
|
||||
await downloadFile(fileId, fileName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id || index}
|
||||
className={`${styles.message} ${styles[`message_${message.role}`]}`}
|
||||
>
|
||||
<div className={styles.message_role}>
|
||||
{message.role === 'user' ? t('chat.you') : message.agentName}
|
||||
</div>
|
||||
<div className={styles.message_content}>
|
||||
{message.content}
|
||||
</div>
|
||||
|
||||
{message.documents && message.documents.length > 0 && (
|
||||
<div className={styles.message_documents}>
|
||||
{message.documents.map((document, docIndex) => (
|
||||
<div
|
||||
key={document.id || docIndex}
|
||||
className={styles.document_item}
|
||||
onClick={() => handleDocumentClick(document)}
|
||||
title={`${t('chat.click_to_open')} ${document.name}`}
|
||||
>
|
||||
<span className={styles.document_icon}>
|
||||
{getFileIcon(document.type, document.ext)}
|
||||
</span>
|
||||
<div className={styles.document_info}>
|
||||
<div className={styles.document_name}>
|
||||
{document.ext ? `${document.name}.${document.ext}` : document.name}
|
||||
</div>
|
||||
<div className={styles.document_meta}>
|
||||
{document.size && (
|
||||
<span className={styles.document_size}>
|
||||
{formatFileSize(document.size)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.document_actions}>
|
||||
<button
|
||||
className={styles.document_action_button}
|
||||
onClick={(e) => handlePreview(document, e)}
|
||||
title={t('chat.preview_document')}
|
||||
>
|
||||
<MdOutlineRemoveRedEye />
|
||||
</button>
|
||||
<button
|
||||
className={styles.document_action_button}
|
||||
onClick={(e) => handleDownload(document, e)}
|
||||
title={t('chat.download_document')}
|
||||
>
|
||||
<FaDownload />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.timestamp && (
|
||||
<div className={styles.message_timestamp}>
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File Preview Popup */}
|
||||
{previewDocument && (
|
||||
<FilePreviewPopup
|
||||
document={previewDocument}
|
||||
isOpen={isPreviewOpen}
|
||||
onClose={handleClosePreview}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageItem;
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { MessageListProps } from "./dashboardChatAreaTypes";
|
||||
import MessageItem from "./DashboardChatAreaMessageItem";
|
||||
import WorkflowStatusDisplay from "./DashbaordChatAreaStatusDisplayold";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
|
||||
const MessageList: React.FC<MessageListProps> = ({
|
||||
messages,
|
||||
currentWorkflowId,
|
||||
workflowStatus,
|
||||
workflowCompleted,
|
||||
startingWorkflow,
|
||||
startError,
|
||||
messagesError,
|
||||
messagesLoading,
|
||||
onStartNewWorkflow,
|
||||
messagesEndRef,
|
||||
handleRetry,
|
||||
shouldShowRetryButton
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.chat_messages}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.messages_container}>
|
||||
{startingWorkflow && (
|
||||
<div className={styles.loading_message}>
|
||||
<p>{workflowCompleted && currentWorkflowId ? t('chat.sending_followup', 'Sending follow-up message...') : t('chat.sending_message', 'Sending message...')}</p>
|
||||
</div>
|
||||
)}
|
||||
{startError && (
|
||||
<div className={styles.error_message}>
|
||||
<p>{t('chat.error_prefix', 'Error:')} {startError}</p>
|
||||
</div>
|
||||
)}
|
||||
{messagesError && (
|
||||
<div className={styles.error_message}>
|
||||
<p>{t('chat.error_loading_messages', 'Error loading messages:')} {messagesError}</p>
|
||||
</div>
|
||||
)}
|
||||
{currentWorkflowId && messagesLoading && messages.length === 0 && (
|
||||
<div className={styles.loading_message}>
|
||||
<p>{t('chat.loading_workflow_messages', 'Loading workflow messages...')}</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.length > 0 ? (
|
||||
messages.map((message, index) => (
|
||||
<MessageItem
|
||||
key={message.id || index}
|
||||
message={message}
|
||||
index={index}
|
||||
/>
|
||||
))
|
||||
) : !currentWorkflowId ? (
|
||||
<p className={styles.placeholder_text}>{t('chat.start_conversation', 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...')}</p>
|
||||
) : null}
|
||||
|
||||
{/* Spacer to push workflow status to bottom when there are fewer messages */}
|
||||
{messages.length < 3 && <div className={styles.messages_spacer} />}
|
||||
|
||||
<WorkflowStatusDisplay
|
||||
currentWorkflowId={currentWorkflowId}
|
||||
workflowStatus={workflowStatus}
|
||||
workflowCompleted={workflowCompleted}
|
||||
onStartNewWorkflow={onStartNewWorkflow}
|
||||
handleRetry={handleRetry}
|
||||
shouldShowRetryButton={shouldShowRetryButton}
|
||||
/>
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageList;
|
||||
|
|
@ -1,455 +0,0 @@
|
|||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--color-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.popup {
|
||||
background: var(--color-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
max-width: 90vw;
|
||||
height: 90vh;
|
||||
width: 80vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
background-color: var(--color-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.close_button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: transparent;
|
||||
color: var(--color-gray);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close_button:hover {
|
||||
background-color: var(--color-gray-disabled);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-gray);
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-red);
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.no_preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-gray);
|
||||
font-size: 16px;
|
||||
font-style: italic;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.image_preview {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pdf_preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.text_preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text);
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.enhanced_text_preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 8px;
|
||||
padding: 32px;
|
||||
overflow: auto;
|
||||
font-family: var(--font-family);
|
||||
box-sizing: border-box;
|
||||
line-height: 1.7;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.text_line {
|
||||
margin-bottom: 4px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.text_line_break {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.text_header {
|
||||
font-weight: 600;
|
||||
font-size: 1.1em;
|
||||
color: var(--color-text);
|
||||
margin: 16px 0 8px 0;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.text_numbered {
|
||||
margin: 8px 0;
|
||||
padding-left: 8px;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.text_bullet {
|
||||
margin: 4px 0;
|
||||
padding-left: 8px;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.text_indented {
|
||||
background-color: var(--color-surface);
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
border-left: 3px solid var(--color-gray-disabled);
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.code_preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.python_code_preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.code_header {
|
||||
background-color: var(--color-gray-disabled);
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.code_language {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.code_filename {
|
||||
font-size: 12px;
|
||||
color: var(--color-gray);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.python_code_content {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
background-color: var(--color-bg);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text);
|
||||
white-space: pre;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.python_code_content code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text);
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown_preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 8px;
|
||||
padding: 32px;
|
||||
overflow: auto;
|
||||
font-family: var(--font-family);
|
||||
box-sizing: border-box;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.markdown_preview h1 {
|
||||
font-size: 2em;
|
||||
font-weight: 700;
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--color-text);
|
||||
border-bottom: 2px solid var(--color-gray-disabled);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown_preview h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
margin: 24px 0 12px 0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.markdown_preview h3 {
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
margin: 20px 0 10px 0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.markdown_preview h4,
|
||||
.markdown_preview h5,
|
||||
.markdown_preview h6 {
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
margin: 16px 0 8px 0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.markdown_preview p {
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.markdown_preview ul,
|
||||
.markdown_preview ol {
|
||||
margin: 0 0 16px 20px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.markdown_preview li {
|
||||
margin: 4px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown_preview blockquote {
|
||||
margin: 16px 0;
|
||||
padding: 12px 16px;
|
||||
border-left: 4px solid var(--color-primary);
|
||||
background-color: var(--color-surface);
|
||||
font-style: italic;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.markdown_preview code {
|
||||
background-color: var(--color-surface);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.markdown_preview pre {
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
overflow-x: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.markdown_preview pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.markdown_preview table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
}
|
||||
|
||||
.markdown_preview th,
|
||||
.markdown_preview td {
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown_preview th {
|
||||
background-color: var(--color-surface);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown_preview td {
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.markdown_preview a {
|
||||
color: var(--color-secondary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown_preview a:hover {
|
||||
color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.markdown_preview hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background-color: var(--color-gray-disabled);
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.markdown_preview strong {
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.markdown_preview em {
|
||||
font-style: italic;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.popup {
|
||||
width: 95vw;
|
||||
height: 85vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.pdf_preview {
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,247 +0,0 @@
|
|||
import React, { useEffect } from "react";
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { MdClose } from "react-icons/md";
|
||||
import { Document } from "./dashboardChatAreaTypes";
|
||||
import { useFilePreview } from "../../../../hooks/useWorkflows";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import styles from './FilePreviewPopup.module.css';
|
||||
|
||||
interface FilePreviewPopupProps {
|
||||
document: Document;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const FilePreviewPopup: React.FC<FilePreviewPopupProps> = ({ document, isOpen, onClose }) => {
|
||||
const { t } = useLanguage();
|
||||
const { previewContent, fileMetadata, isLoading, error, fetchPreview, clearPreview } = useFilePreview();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && document) {
|
||||
// Use fileId if available, otherwise try to use id as fallback
|
||||
const fileId = document.fileId || document.id;
|
||||
|
||||
if (fileId) {
|
||||
console.log('FilePreviewPopup: calling fetchPreview with fileId:', fileId);
|
||||
fetchPreview(String(fileId));
|
||||
} else {
|
||||
console.error('FilePreviewPopup: No fileId or id available on document:', document);
|
||||
}
|
||||
} else if (!isOpen) {
|
||||
clearPreview();
|
||||
}
|
||||
}, [isOpen, document.fileId, document.id]);
|
||||
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const getPreviewComponent = () => {
|
||||
if (isLoading) {
|
||||
return <div className={styles.loading}>{t('file_preview.loading')}</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<div>{t('file_preview.error')}: {error}</div>
|
||||
{fileMetadata && (
|
||||
<div style={{ marginTop: '10px', fontSize: '12px', opacity: 0.7 }}>
|
||||
Debug: {JSON.stringify(fileMetadata, null, 2)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!previewContent) {
|
||||
return (
|
||||
<div className={styles.no_preview}>
|
||||
<div>{t('file_preview.no_preview')}</div>
|
||||
{fileMetadata && (
|
||||
<div style={{ marginTop: '10px', fontSize: '12px', opacity: 0.7 }}>
|
||||
Available metadata: {Object.keys(fileMetadata).join(', ')}
|
||||
<br />
|
||||
Preview field: {fileMetadata.preview ? 'has data' : 'empty/null'}
|
||||
<br />
|
||||
Base64Encoded: {String(fileMetadata.base64Encoded)}
|
||||
<br />
|
||||
MimeType: {fileMetadata.mimeType}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Use metadata from backend response, but prioritize file extension over potentially incorrect MIME type
|
||||
const mimeType = fileMetadata?.mimeType;
|
||||
const isBase64Encoded = fileMetadata?.base64Encoded;
|
||||
const fileExtension = document.ext?.toLowerCase();
|
||||
|
||||
// Check if this is a markdown file by extension first (more reliable than backend MIME type)
|
||||
const isMarkdownByType = fileExtension === 'md' ||
|
||||
fileExtension === 'markdown' ||
|
||||
mimeType === 'text/markdown' ||
|
||||
mimeType === 'text/x-markdown';
|
||||
|
||||
// Content-based markdown detection for .txt files with markdown content
|
||||
// BUT NOT for specific code file types
|
||||
const isCodeFile = fileExtension === 'py' ||
|
||||
fileExtension === 'js' ||
|
||||
fileExtension === 'ts' ||
|
||||
fileExtension === 'jsx' ||
|
||||
fileExtension === 'tsx' ||
|
||||
fileExtension === 'java' ||
|
||||
fileExtension === 'cpp' ||
|
||||
fileExtension === 'c' ||
|
||||
fileExtension === 'php' ||
|
||||
fileExtension === 'html' ||
|
||||
fileExtension === 'css';
|
||||
|
||||
const hasMarkdownContent = !isCodeFile && previewContent && (
|
||||
previewContent.includes('# ') || // Headers
|
||||
previewContent.includes('## ') || // Headers
|
||||
previewContent.includes('**') || // Bold
|
||||
previewContent.includes('__') || // Bold
|
||||
previewContent.includes('- ') || // Lists
|
||||
previewContent.includes('* ') || // Lists
|
||||
previewContent.includes('1. ') || // Numbered lists
|
||||
previewContent.includes('```') || // Code blocks
|
||||
previewContent.includes('[') && previewContent.includes('](') // Links
|
||||
);
|
||||
|
||||
// For .txt files or text MIME types, check for markdown content
|
||||
const isTxtWithMarkdown = (fileExtension === 'txt' || mimeType?.startsWith('text/')) && hasMarkdownContent;
|
||||
const isMarkdown = isMarkdownByType || isTxtWithMarkdown;
|
||||
|
||||
// Debug logging
|
||||
console.log('FilePreviewPopup preview detection:', {
|
||||
fileExtension,
|
||||
mimeType,
|
||||
isMarkdownByType,
|
||||
hasMarkdownContent,
|
||||
isTxtWithMarkdown,
|
||||
isMarkdown,
|
||||
isCodeFile
|
||||
});
|
||||
|
||||
if (mimeType?.startsWith('image/')) {
|
||||
// Image preview
|
||||
const imageSrc = isBase64Encoded
|
||||
? `data:${mimeType};base64,${previewContent}`
|
||||
: previewContent;
|
||||
|
||||
return (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={document.name}
|
||||
className={styles.image_preview}
|
||||
/>
|
||||
);
|
||||
} else if (fileExtension === 'pdf' || mimeType === 'application/pdf') {
|
||||
// PDF preview
|
||||
const pdfSrc = isBase64Encoded
|
||||
? `data:application/pdf;base64,${previewContent}`
|
||||
: previewContent;
|
||||
|
||||
return (
|
||||
<iframe
|
||||
src={pdfSrc}
|
||||
className={styles.pdf_preview}
|
||||
title={document.name}
|
||||
/>
|
||||
);
|
||||
} else if (isMarkdown) {
|
||||
// Markdown preview
|
||||
console.log('Rendering markdown with ReactMarkdown:', previewContent?.substring(0, 200));
|
||||
|
||||
return (
|
||||
<div className={styles.markdown_preview}>
|
||||
<ReactMarkdown>{previewContent}</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
} else if (fileExtension === 'py') {
|
||||
// Python code preview
|
||||
return (
|
||||
<div className={styles.python_code_preview}>
|
||||
<div className={styles.code_header}>
|
||||
<span className={styles.code_language}>{t('file_preview.python')}</span>
|
||||
<span className={styles.code_filename}>
|
||||
{document.ext ? `${document.name}.${document.ext}` : document.name}
|
||||
</span>
|
||||
</div>
|
||||
<pre className={styles.python_code_content}>
|
||||
<code>{previewContent}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
} else if ((mimeType?.startsWith('text/') || fileExtension === 'txt') && !isMarkdown) {
|
||||
// Enhanced text preview for text files that are not markdown
|
||||
return (
|
||||
<div className={styles.enhanced_text_preview}>
|
||||
{previewContent?.split('\n').map((line, index) => {
|
||||
// Handle empty lines
|
||||
if (line.trim() === '') {
|
||||
return <div key={index} className={styles.text_line_break}></div>;
|
||||
}
|
||||
|
||||
// Check if line looks like a header (all caps, starts with numbers, etc.)
|
||||
const isHeader = line.match(/^[A-Z\s\d\.\-_]+:?\s*$/) && line.length < 80;
|
||||
const isNumberedItem = line.match(/^\s*\d+\.\s/);
|
||||
const isBulletItem = line.match(/^\s*[-*•]\s/);
|
||||
const isIndented = line.match(/^\s{4,}/);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`${styles.text_line} ${
|
||||
isHeader ? styles.text_header :
|
||||
isNumberedItem ? styles.text_numbered :
|
||||
isBulletItem ? styles.text_bullet :
|
||||
isIndented ? styles.text_indented : ''
|
||||
}`}
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// Code/raw text preview for non-text files
|
||||
return (
|
||||
<pre className={styles.code_preview}>
|
||||
{previewContent}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.overlay} onClick={handleBackdropClick}>
|
||||
<div className={styles.popup}>
|
||||
<div className={styles.header}>
|
||||
<h3 className={styles.title}>
|
||||
{document.ext ? `${document.name}.${document.ext}` : document.name}
|
||||
</h3>
|
||||
<button
|
||||
className={styles.close_button}
|
||||
onClick={onClose}
|
||||
title={t('file_preview.close_preview')}
|
||||
>
|
||||
<MdClose />
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{getPreviewComponent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilePreviewPopup;
|
||||
|
|
@ -1,316 +0,0 @@
|
|||
import { useState, useEffect, useRef } from "react";
|
||||
import { Prompt } from "../../../../hooks/usePrompts";
|
||||
import { FileInfo, useFileOperations } from "../../../../hooks/useFiles";
|
||||
import { useWorkflowOperations, useWorkflowMessages, useWorkflowStatus } from "../../../../hooks/useWorkflows";
|
||||
|
||||
interface UseChatLogicProps {
|
||||
selectedPrompt?: Prompt | null;
|
||||
onPromptUsed?: () => void;
|
||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
||||
resumeWorkflowId?: string | null;
|
||||
}
|
||||
|
||||
export const useChatLogic = ({
|
||||
selectedPrompt,
|
||||
onPromptUsed,
|
||||
onWorkflowIdChange,
|
||||
resumeWorkflowId
|
||||
}: UseChatLogicProps) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
||||
const [workflowCompleted, setWorkflowCompleted] = useState(false);
|
||||
const [attachedFiles, setAttachedFiles] = useState<FileInfo[]>([]);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { startWorkflow, startingWorkflow, startError, stopWorkflow, stoppingWorkflows } = useWorkflowOperations();
|
||||
const { messages, loading: messagesLoading, error: messagesError, refetch: refetchMessages } = useWorkflowMessages(currentWorkflowId);
|
||||
const { status: workflowStatus, refetch: refetchStatus } = useWorkflowStatus(currentWorkflowId);
|
||||
const { handleFileUpload } = useFileOperations();
|
||||
|
||||
// Update input value when a prompt is selected
|
||||
useEffect(() => {
|
||||
if (selectedPrompt) {
|
||||
setInputValue(selectedPrompt.content);
|
||||
// Focus the input field
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
}, [selectedPrompt]);
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive or workflow status changes
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [messages, workflowCompleted, workflowStatus]);
|
||||
|
||||
// Polling logic for fetching messages and status
|
||||
useEffect(() => {
|
||||
if (!currentWorkflowId || workflowCompleted) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
refetchMessages();
|
||||
refetchStatus();
|
||||
}, 1000); // Poll every second
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [currentWorkflowId, workflowCompleted, refetchMessages, refetchStatus]);
|
||||
|
||||
// Simple workflow completion detection
|
||||
useEffect(() => {
|
||||
const isCompleted = workflowStatus && (
|
||||
workflowStatus.status === 'completed' ||
|
||||
workflowStatus.status === 'finished' ||
|
||||
workflowStatus.status === 'done' ||
|
||||
workflowStatus.status === 'stopped' ||
|
||||
workflowStatus.status === 'error'
|
||||
);
|
||||
|
||||
setWorkflowCompleted(!!isCompleted);
|
||||
}, [workflowStatus]);
|
||||
|
||||
// Handle workflow ID changes
|
||||
useEffect(() => {
|
||||
if (currentWorkflowId && onWorkflowIdChange) {
|
||||
onWorkflowIdChange(currentWorkflowId);
|
||||
}
|
||||
}, [currentWorkflowId, onWorkflowIdChange]);
|
||||
|
||||
// Handle workflow resumption
|
||||
useEffect(() => {
|
||||
if (resumeWorkflowId && resumeWorkflowId !== currentWorkflowId) {
|
||||
setCurrentWorkflowId(resumeWorkflowId);
|
||||
setWorkflowCompleted(false);
|
||||
setInputValue("");
|
||||
setAttachedFiles([]);
|
||||
}
|
||||
}, [resumeWorkflowId, currentWorkflowId]);
|
||||
|
||||
const handleFileAttach = async (file: File) => {
|
||||
try {
|
||||
console.log('Uploading file:', file.name);
|
||||
const result = await handleFileUpload(file, currentWorkflowId || undefined);
|
||||
|
||||
if (result.success && result.fileData) {
|
||||
// Add the uploaded file to the attached files list
|
||||
const fileInfo: FileInfo = {
|
||||
id: result.fileData.id,
|
||||
name: result.fileData.name || file.name,
|
||||
mimeType: result.fileData.mimeType || file.type,
|
||||
size: result.fileData.size || file.size,
|
||||
creationDate: result.fileData.creationDate || new Date().toISOString(),
|
||||
workflowId: currentWorkflowId || undefined
|
||||
};
|
||||
|
||||
setAttachedFiles(prev => [...prev, fileInfo]);
|
||||
console.log('File uploaded successfully:', fileInfo);
|
||||
} else {
|
||||
console.error('Failed to upload file:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileRemove = (fileId: number) => {
|
||||
setAttachedFiles(prev => prev.filter(file => file.id !== fileId));
|
||||
};
|
||||
|
||||
const handleFilesSelect = (selectedFiles: FileInfo[]) => {
|
||||
// Add selected files to the attached files list, avoiding duplicates
|
||||
setAttachedFiles(prev => {
|
||||
const existingIds = new Set(prev.map(f => f.id));
|
||||
const newFiles = selectedFiles.filter(f => !existingIds.has(f.id));
|
||||
return [...prev, ...newFiles];
|
||||
});
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
if (inputValue.trim() || attachedFiles.length > 0) {
|
||||
console.log('Sending message:', inputValue, 'with files:', attachedFiles.map(f => f.id));
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
// Prepare file IDs for the request
|
||||
const listFileId = attachedFiles.map(file => file.id);
|
||||
|
||||
// If we have a completed workflow, send as follow-up using the existing workflow ID
|
||||
if (workflowCompleted && currentWorkflowId) {
|
||||
console.log('Sending follow-up message to workflow:', currentWorkflowId);
|
||||
result = await startWorkflow({
|
||||
prompt: inputValue || "Files attached", // Provide a default message if only files are sent
|
||||
listFileId: listFileId
|
||||
}, currentWorkflowId);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Follow-up message sent successfully');
|
||||
// Reset workflow completion state to resume polling for messages/status
|
||||
// but DON'T reset logPollingCompleted - logs should stay stopped
|
||||
setWorkflowCompleted(false);
|
||||
}
|
||||
} else {
|
||||
// Start a new workflow
|
||||
console.log('Starting new workflow');
|
||||
// Reset previous workflow state when starting a new one
|
||||
setCurrentWorkflowId(null);
|
||||
setWorkflowCompleted(false);
|
||||
|
||||
result = await startWorkflow({
|
||||
prompt: inputValue || "Files attached", // Provide a default message if only files are sent
|
||||
listFileId: listFileId
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
console.log('Workflow started successfully:', result.data);
|
||||
// Set the workflow ID to start polling for messages
|
||||
setCurrentWorkflowId(result.data.id);
|
||||
setWorkflowCompleted(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
// Clear the input and attached files after successful send
|
||||
setInputValue("");
|
||||
setAttachedFiles([]);
|
||||
// Call onPromptUsed if a prompt was used
|
||||
if (selectedPrompt && onPromptUsed) {
|
||||
onPromptUsed();
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to send message:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
const startNewWorkflow = () => {
|
||||
setCurrentWorkflowId(null);
|
||||
setWorkflowCompleted(false);
|
||||
setInputValue("");
|
||||
setAttachedFiles([]);
|
||||
if (onWorkflowIdChange) {
|
||||
onWorkflowIdChange(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStopWorkflow = async () => {
|
||||
if (currentWorkflowId) {
|
||||
console.log('Stopping workflow:', currentWorkflowId);
|
||||
const success = await stopWorkflow(currentWorkflowId);
|
||||
if (success) {
|
||||
console.log('Workflow stopped successfully');
|
||||
// Refresh status to get updated workflow state
|
||||
refetchStatus();
|
||||
} else {
|
||||
console.error('Failed to stop workflow');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Determine if workflow is currently running
|
||||
const isWorkflowRunning = !!(currentWorkflowId && !workflowCompleted && workflowStatus &&
|
||||
workflowStatus.status !== 'completed' &&
|
||||
workflowStatus.status !== 'finished' &&
|
||||
workflowStatus.status !== 'done' &&
|
||||
workflowStatus.status !== 'stopped' &&
|
||||
workflowStatus.status !== 'error');
|
||||
|
||||
const isStoppingWorkflow = currentWorkflowId ? stoppingWorkflows.has(currentWorkflowId) : false;
|
||||
|
||||
const handleRetry = async () => {
|
||||
if (!currentWorkflowId || !messages.length) {
|
||||
console.error('No workflow ID or messages available for retry');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the last user message to retry
|
||||
const userMessages = messages.filter(msg => msg.role === 'user');
|
||||
const lastUserMessage = userMessages[userMessages.length - 1];
|
||||
|
||||
if (!lastUserMessage) {
|
||||
console.error('No user message found to retry');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Retrying workflow with last user message:', lastUserMessage.content);
|
||||
|
||||
try {
|
||||
// Extract file IDs if available from the message
|
||||
const fileIds = lastUserMessage.fileIds || [];
|
||||
|
||||
// Start the workflow again with the same prompt and files
|
||||
const result = await startWorkflow({
|
||||
prompt: lastUserMessage.content,
|
||||
listFileId: fileIds
|
||||
}, currentWorkflowId);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Workflow retry started successfully');
|
||||
// Reset workflow completion state to resume polling
|
||||
setWorkflowCompleted(false);
|
||||
} else {
|
||||
console.error('Failed to retry workflow:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error retrying workflow:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const shouldShowRetryButton = () => {
|
||||
if (!workflowStatus) return false;
|
||||
|
||||
const statusLower = workflowStatus.status.toLowerCase();
|
||||
return statusLower === 'error' ||
|
||||
statusLower === 'failed' ||
|
||||
statusLower === 'stopped' ||
|
||||
statusLower === 'cancelled';
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
inputValue,
|
||||
setInputValue,
|
||||
currentWorkflowId,
|
||||
workflowCompleted,
|
||||
attachedFiles,
|
||||
|
||||
// Refs
|
||||
inputRef,
|
||||
messagesEndRef,
|
||||
|
||||
// Data from hooks
|
||||
messages,
|
||||
messagesLoading,
|
||||
messagesError,
|
||||
startingWorkflow,
|
||||
startError,
|
||||
workflowStatus,
|
||||
|
||||
// Handlers
|
||||
handleSend,
|
||||
handleKeyPress,
|
||||
startNewWorkflow,
|
||||
handleStopWorkflow,
|
||||
handleFileAttach,
|
||||
handleFileRemove,
|
||||
handleFilesSelect,
|
||||
handleRetry,
|
||||
|
||||
// Workflow state
|
||||
isWorkflowRunning,
|
||||
isStoppingWorkflow,
|
||||
shouldShowRetryButton
|
||||
};
|
||||
};
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import { Prompt } from "../../../../hooks/usePrompts";
|
||||
import { FileInfo } from "../../../../hooks/useFiles";
|
||||
|
||||
export interface DashboardChatAreaProps {
|
||||
selectedPrompt?: Prompt | null;
|
||||
onPromptUsed?: () => void;
|
||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
||||
onWorkflowCompletedChange?: (completed: boolean) => void;
|
||||
resumeWorkflowId?: string | null;
|
||||
}
|
||||
|
||||
export interface Document {
|
||||
id?: string;
|
||||
fileId?: number;
|
||||
name: string;
|
||||
url?: string;
|
||||
type?: string;
|
||||
size?: number;
|
||||
downloadUrl?: string;
|
||||
ext?: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id?: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
agentName: string;
|
||||
content: string;
|
||||
timestamp?: string;
|
||||
documents?: Document[];
|
||||
}
|
||||
|
||||
export interface WorkflowStatus {
|
||||
status: string;
|
||||
currentRound?: number;
|
||||
}
|
||||
|
||||
export interface ChatInputProps {
|
||||
inputValue: string;
|
||||
setInputValue: (value: string) => void;
|
||||
onSend: () => void;
|
||||
onKeyPress: (e: React.KeyboardEvent) => void;
|
||||
isDisabled: boolean;
|
||||
placeholder: string;
|
||||
inputRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||
isWorkflowRunning: boolean;
|
||||
onStopWorkflow: () => void;
|
||||
isStoppingWorkflow: boolean;
|
||||
attachedFiles: FileInfo[];
|
||||
onFileAttach: (file: File) => void;
|
||||
onFileRemove: (fileId: number) => void;
|
||||
onFilesSelect: (files: FileInfo[]) => void;
|
||||
}
|
||||
|
||||
export interface MessageListProps {
|
||||
messages: Message[];
|
||||
currentWorkflowId: string | null;
|
||||
workflowStatus: WorkflowStatus | null;
|
||||
workflowCompleted: boolean;
|
||||
startingWorkflow: boolean;
|
||||
startError: string | null;
|
||||
messagesError: string | null;
|
||||
messagesLoading: boolean;
|
||||
onStartNewWorkflow: () => void;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement | null>;
|
||||
handleRetry: () => Promise<void>;
|
||||
shouldShowRetryButton: () => boolean;
|
||||
}
|
||||
|
||||
export interface WorkflowStatusDisplayProps {
|
||||
currentWorkflowId: string | null;
|
||||
workflowStatus: WorkflowStatus | null;
|
||||
workflowCompleted: boolean;
|
||||
onStartNewWorkflow: () => void;
|
||||
handleRetry: () => Promise<void>;
|
||||
shouldShowRetryButton: () => boolean;
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
# DashboardChatArea - Modular Structure
|
||||
|
||||
This directory contains the refactored `DashboardChatArea` component, broken down into manageable modules for better maintainability and separation of concerns.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
DashboardChatArea/
|
||||
├── index.ts # Main export file
|
||||
├── types.ts # TypeScript interfaces and types
|
||||
├── DashboardChatArea.tsx # Main orchestrating component
|
||||
├── useChatLogic.ts # Custom hook with all business logic
|
||||
├── MessageList.tsx # Component for displaying messages
|
||||
├── MessageItem.tsx # Individual message component
|
||||
├── ChatInput.tsx # Input field and send button component
|
||||
├── WorkflowStatusDisplay.tsx # Workflow status and completion UI
|
||||
├── DashboardChatArea.module.css # Shared styles
|
||||
└── README.md # This documentation
|
||||
```
|
||||
|
||||
## Component Responsibilities
|
||||
|
||||
### `DashboardChatArea.tsx` (Main Component)
|
||||
- **Purpose**: Orchestrates all child components
|
||||
- **Responsibilities**:
|
||||
- Uses the `useChatLogic` hook
|
||||
- Renders `MessageList` and `ChatInput` components
|
||||
- Passes props between components
|
||||
- **Size**: ~73 lines (reduced from 278 lines)
|
||||
|
||||
### `useChatLogic.ts` (Custom Hook)
|
||||
- **Purpose**: Contains all business logic and state management
|
||||
- **Responsibilities**:
|
||||
- State management (input value, workflow ID, completion status)
|
||||
- Effects for polling, auto-scroll, prompt handling
|
||||
- Workflow operations (send messages, start workflows)
|
||||
- Event handlers
|
||||
- **Size**: ~196 lines
|
||||
|
||||
### `MessageList.tsx` (Message Display)
|
||||
- **Purpose**: Handles the display of all messages and status indicators
|
||||
- **Responsibilities**:
|
||||
- Renders loading and error states
|
||||
- Maps through messages using `MessageItem`
|
||||
- Includes `WorkflowStatusDisplay`
|
||||
- Handles auto-scroll reference
|
||||
- **Size**: ~73 lines
|
||||
|
||||
### `MessageItem.tsx` (Individual Message)
|
||||
- **Purpose**: Renders a single message
|
||||
- **Responsibilities**:
|
||||
- Message content display
|
||||
- Role-based styling
|
||||
- Timestamp formatting
|
||||
- **Size**: ~32 lines
|
||||
|
||||
### `ChatInput.tsx` (Input Interface)
|
||||
- **Purpose**: Handles user input and send functionality
|
||||
- **Responsibilities**:
|
||||
- Input field with ref handling
|
||||
- Send button with animations
|
||||
- Keyboard event handling
|
||||
- Disabled states
|
||||
- **Size**: ~46 lines
|
||||
|
||||
### `WorkflowStatusDisplay.tsx` (Status UI)
|
||||
- **Purpose**: Shows workflow status and completion states
|
||||
- **Responsibilities**:
|
||||
- Running workflow status
|
||||
- Completion message
|
||||
- "Start New Workflow" button
|
||||
- **Size**: ~38 lines
|
||||
|
||||
### `types.ts` (Type Definitions)
|
||||
- **Purpose**: Centralized TypeScript interfaces
|
||||
- **Responsibilities**:
|
||||
- Component prop interfaces
|
||||
- Data structure types
|
||||
- Shared type definitions
|
||||
- **Size**: ~50 lines
|
||||
|
||||
## Benefits of This Structure
|
||||
|
||||
1. **Separation of Concerns**: Each file has a single, clear responsibility
|
||||
2. **Reusability**: Components can be easily reused or tested independently
|
||||
3. **Maintainability**: Easier to locate and modify specific functionality
|
||||
4. **Readability**: Smaller files are easier to understand and navigate
|
||||
5. **Testing**: Individual components can be unit tested in isolation
|
||||
6. **Type Safety**: Centralized types ensure consistency across components
|
||||
|
||||
## Usage
|
||||
|
||||
Import the main component as before:
|
||||
|
||||
```typescript
|
||||
import DashboardChatArea from './DashboardChatArea';
|
||||
// or
|
||||
import DashboardChatArea, { DashboardChatAreaProps } from './DashboardChatArea';
|
||||
```
|
||||
|
||||
The API remains exactly the same - this refactoring is purely internal and doesn't affect how the component is used by parent components.
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
- **Adding new features**: Consider which component/file is most appropriate
|
||||
- **State changes**: Most state logic should go in `useChatLogic.ts`
|
||||
- **UI changes**: Modify the relevant component file
|
||||
- **New types**: Add to `types.ts`
|
||||
- **Styling**: All styles remain in `DashboardChatArea.module.css`
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from './DashboardChatArea';
|
||||
export type { DashboardChatAreaProps } from './dashboardChatAreaTypes';
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useFileDownload } from '../../../../hooks/useWorkflows';
|
||||
import { useFileDownload } from '../../../hooks/useWorkflows';
|
||||
import { FileInfo } from './dashboardChatAreaTypes';
|
||||
|
||||
interface AttachedFile {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { useFilePreview } from '../../../../hooks/useWorkflows';
|
||||
import { useFilePreview } from '../../../hooks/useWorkflows';
|
||||
import { FileInfo } from './dashboardChatAreaTypes';
|
||||
|
||||
interface AttachedFileWithData extends FileInfo {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useWorkflowOperations } from '../../../../hooks/useWorkflows';
|
||||
import { Prompt } from '../../../../hooks/usePrompts';
|
||||
import { useWorkflowOperations } from '../../../hooks/useWorkflows';
|
||||
import { Prompt } from '../../../hooks/usePrompts';
|
||||
import FileAttachmentPopup from './FileAttachmentPopup';
|
||||
|
||||
interface InputAreaProps {
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useState } from "react";
|
||||
import { FaDownload } from "react-icons/fa";
|
||||
import { MdOutlineRemoveRedEye } from "react-icons/md";
|
||||
import { useFileDownload } from "../../../../hooks/useWorkflows";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import { useFileDownload } from "../../../hooks/useWorkflows";
|
||||
import { useLanguage } from "../../../contexts/LanguageContext";
|
||||
import { Message, Document } from "./dashboardChatAreaTypes";
|
||||
|
||||
interface MessageItemProps {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useWorkflowStatus } from '../../../../hooks/useWorkflows';
|
||||
import { Prompt } from '../../../../hooks/usePrompts';
|
||||
import { useApiRequest } from '../../../../hooks/useApi';
|
||||
import { useWorkflowStatus } from '../../../hooks/useWorkflows';
|
||||
import { Prompt } from '../../../hooks/usePrompts';
|
||||
import { useApiRequest } from '../../../hooks/useApi';
|
||||
import MessageItem from './DashboardChatAreaMessageItem';
|
||||
import { Message, Document, WorkflowMessage } from './dashboardChatAreaTypes';
|
||||
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
.chat_history {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.history_title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.workflowCount {
|
||||
font-size: 14px;
|
||||
color: var(--color-gray);
|
||||
background-color: var(--color-surface);
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.scrollableContent {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.workflowsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: var(--color-gray);
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
color: var(--color-gray);
|
||||
font-size: 16px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.errorContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
color: var(--color-red);
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.retryButton {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.retryButton:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.scrollableContent::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-track {
|
||||
background: var(--color-surface);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-thumb {
|
||||
background: var(--color-gray-disabled);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-gray);
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useWorkflows } from "../../../../hooks/useWorkflows";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import DashboardChatHistoryItem from "./DashboardChatHistoryItem";
|
||||
|
||||
import styles from './DashboardChatHistory.module.css';
|
||||
|
||||
interface DashboardChatHistoryProps {
|
||||
onWorkflowResume?: (workflowId: string) => void;
|
||||
}
|
||||
|
||||
const DashboardChatHistory: React.FC<DashboardChatHistoryProps> = ({ onWorkflowResume }) => {
|
||||
const { workflows, loading, error, refetch } = useWorkflows();
|
||||
const { t } = useLanguage();
|
||||
|
||||
const handleWorkflowResume = (workflowId: string) => {
|
||||
if (onWorkflowResume) {
|
||||
onWorkflowResume(workflowId);
|
||||
}
|
||||
console.log('Resuming workflow:', workflowId);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.chat_history}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.loadingText}>{t('chat_history.loading', 'Loading workflows...')}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.chat_history}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.errorContainer}>
|
||||
<div className={styles.errorText}>{t('chat_history.error_loading', 'Error loading workflows:')} {error}</div>
|
||||
<button
|
||||
onClick={refetch}
|
||||
className={styles.retryButton}
|
||||
>
|
||||
{t('chat_history.try_again', 'Try Again')}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.chat_history}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.history_title}>{t('chat_history.title', 'Workflow History')}</h2>
|
||||
<div className={styles.workflowCount}>
|
||||
{workflows.length} {workflows.length === 1 ? t('chat_history.workflow_count', 'Workflow') : t('chat_history.workflow_count_plural', 'Workflows')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.scrollableContent}>
|
||||
{workflows.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
{t('chat_history.empty_state', 'No workflows available')}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.workflowsList}>
|
||||
{workflows.map((workflow) => (
|
||||
<DashboardChatHistoryItem
|
||||
key={workflow.id}
|
||||
workflow={workflow}
|
||||
onDelete={refetch}
|
||||
onResume={handleWorkflowResume}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardChatHistory;
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
.workflowItem {
|
||||
background: var(--color-bg);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
margin-bottom: 12px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.workflowItem:hover {
|
||||
border-color: var(--color-gray);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.workflowMain {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.workflowContent {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workflowInfo {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.workflowId {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.2;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.workflowMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.workflowStatus {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--color-secondary-disabled);
|
||||
color: var(--color-secondary);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.workflowRound {
|
||||
font-size: 12px;
|
||||
color: var(--color-gray);
|
||||
background-color: var(--color-surface);
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.workflowDates {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.workflowDate {
|
||||
font-size: 12px;
|
||||
color: var(--color-gray);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.workflowDescription {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.messagePreview {
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
background-color: var(--color-surface);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--color-gray);
|
||||
}
|
||||
|
||||
.previewText {
|
||||
font-size: 13px;
|
||||
color: var(--color-gray);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
font-style: italic;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.workflowName {
|
||||
font-size: 14px;
|
||||
color: var(--color-gray);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.actionButton:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.resumeButton {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
.resumeButton:hover:not(:disabled) {
|
||||
background-color: var(--color-secondary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
background-color: var(--color-red);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
.deleteButton:hover:not(:disabled) {
|
||||
background-color: var(--color-red-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.deletingMessage {
|
||||
padding: 8px 16px;
|
||||
background-color: var(--color-primary-disabled);
|
||||
border-top: 1px solid var(--color-gray-disabled);
|
||||
color: var(--color-primary);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.workflowMain {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { FaArrowRight } from 'react-icons/fa';
|
||||
import { AiOutlineDelete } from 'react-icons/ai';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useWorkflowOperations, useWorkflowMessages, Workflow } from '../../../../hooks/useWorkflows';
|
||||
import { useLanguage } from '../../../../contexts/LanguageContext';
|
||||
import styles from './DashboardChatHistoryItem.module.css';
|
||||
|
||||
interface DashboardChatHistoryItemProps {
|
||||
workflow: Workflow;
|
||||
onDelete?: () => void;
|
||||
onResume: (workflowId: string) => void;
|
||||
}
|
||||
|
||||
function DashboardChatHistoryItem({ workflow, onDelete, onResume }: DashboardChatHistoryItemProps) {
|
||||
const { deleteWorkflow, deletingWorkflows } = useWorkflowOperations();
|
||||
const { messages } = useWorkflowMessages(workflow.id);
|
||||
const { t } = useLanguage();
|
||||
|
||||
const isDeleting = deletingWorkflows.has(workflow.id);
|
||||
|
||||
// Get the first user message as preview
|
||||
const firstUserMessage = messages.find(msg => msg.role === 'user');
|
||||
const messagePreview = firstUserMessage?.content || t('chat_history.no_message_content', 'No message content available');
|
||||
|
||||
const handleDelete = async () => {
|
||||
const workflowName = workflow.title || `Workflow ${workflow.id.substring(0, 8)}...`;
|
||||
const confirmMessage = t('chat_history.confirm_delete', 'Are you sure you want to delete "{name}"?').replace('{name}', workflowName);
|
||||
if (window.confirm(confirmMessage)) {
|
||||
const success = await deleteWorkflow(workflow.id);
|
||||
if (success && onDelete) {
|
||||
onDelete();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleResume = () => {
|
||||
onResume(workflow.id);
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return t('chat_history.unknown_date', 'Unknown date');
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (error) {
|
||||
return t('chat_history.invalid_date', 'Invalid date');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'error':
|
||||
case 'failed':
|
||||
case 'stopped':
|
||||
case 'cancelled':
|
||||
return 'var(--color-red)';
|
||||
default:
|
||||
return 'var(--color-gray)';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBackgroundColor = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'error':
|
||||
case 'failed':
|
||||
case 'stopped':
|
||||
case 'cancelled':
|
||||
return 'var(--color-red-disabled)';
|
||||
default:
|
||||
return 'var(--color-gray-disabled)';
|
||||
}
|
||||
};
|
||||
|
||||
const shouldShowStatus = (status: string) => {
|
||||
const statusLower = status.toLowerCase();
|
||||
return statusLower === 'error' ||
|
||||
statusLower === 'failed' ||
|
||||
statusLower === 'stopped' ||
|
||||
statusLower === 'cancelled';
|
||||
};
|
||||
|
||||
const isRunning = (status: string) => {
|
||||
const statusLower = status.toLowerCase();
|
||||
return statusLower === 'running' || statusLower === 'processing';
|
||||
};
|
||||
|
||||
const getTranslatedStatus = (status: string): string => {
|
||||
const statusKey = `status.${status.toLowerCase()}`;
|
||||
return t(statusKey, status.toUpperCase());
|
||||
};
|
||||
|
||||
const renderStatusIndicator = () => {
|
||||
if (isRunning(workflow.status)) {
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.loadingSpinner}
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
ease: "linear"
|
||||
}}
|
||||
style={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
border: '2px solid var(--color-gray-disabled)',
|
||||
borderTop: '2px solid var(--color-secondary)',
|
||||
borderRadius: '50%',
|
||||
display: 'inline-block'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldShowStatus(workflow.status)) {
|
||||
return (
|
||||
<span
|
||||
className={styles.workflowStatus}
|
||||
style={{
|
||||
color: getStatusColor(workflow.status),
|
||||
backgroundColor: getStatusBackgroundColor(workflow.status)
|
||||
}}
|
||||
>
|
||||
{getTranslatedStatus(workflow.status)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const truncateMessage = (message: string, maxLength: number = 150) => {
|
||||
if (message.length <= maxLength) return message;
|
||||
return message.substring(0, maxLength) + '...';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.workflowItem}>
|
||||
<div className={styles.workflowMain}>
|
||||
<div className={styles.workflowContent}>
|
||||
<div className={styles.workflowInfo}>
|
||||
<h3 className={styles.workflowId}>
|
||||
{workflow.title || `Workflow ${workflow.id.substring(0, 8)}...`}
|
||||
</h3>
|
||||
<div className={styles.workflowMeta}>
|
||||
{renderStatusIndicator()}
|
||||
{workflow.currentRound && (
|
||||
<span className={styles.workflowRound}>
|
||||
{t('chat_history.round', 'Round')} {workflow.currentRound}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.workflowDates}>
|
||||
{workflow.startedAt && (
|
||||
<p className={styles.workflowDate}>
|
||||
{t('chat_history.started', 'Started:')} {formatDate(workflow.startedAt)}
|
||||
</p>
|
||||
)}
|
||||
{workflow.lastActivity && (
|
||||
<p className={styles.workflowDate}>
|
||||
{t('chat_history.last_activity', 'Last Activity:')} {formatDate(workflow.lastActivity)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.workflowDescription}>
|
||||
<div className={styles.messagePreview}>
|
||||
<p className={styles.previewText}>
|
||||
{truncateMessage(messagePreview)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.actionButtons}>
|
||||
<button
|
||||
onClick={handleResume}
|
||||
className={`${styles.actionButton} ${styles.resumeButton}`}
|
||||
title={t('chat_history.resume_tooltip', 'Resume workflow')}
|
||||
>
|
||||
<FaArrowRight size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className={`${styles.actionButton} ${styles.deleteButton}`}
|
||||
title={t('chat_history.delete_tooltip', 'Delete workflow')}
|
||||
>
|
||||
<AiOutlineDelete size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isDeleting && (
|
||||
<div className={styles.deletingMessage}>
|
||||
{t('chat_history.deleting', 'Deleting workflow...')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardChatHistoryItem;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useUserFiles, UserFile } from '../../../../hooks/useFiles';
|
||||
import { useUserFiles, UserFile } from '../../../hooks/useFiles';
|
||||
import DateienAll from '../../../Dateien/DateienAll';
|
||||
import DateienShared from '../../../Dateien/DateienShared';
|
||||
import DateienCreated from '../../../Dateien/DateienCreated';
|
||||
|
|
@ -3,8 +3,8 @@ import { MdOutlineRemoveRedEye } from "react-icons/md";
|
|||
import styles from "./DateienItem.module.css";
|
||||
import { useState } from "react";
|
||||
import { useFileOperations } from "../../hooks/useFiles";
|
||||
import FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaFilePreview";
|
||||
import { Document } from "../Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaTypes";
|
||||
import FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatAreaFilePreview";
|
||||
import { Document } from "../Dashboard/DashboardChat/dashboardChatAreaTypes";
|
||||
import { useLanguage } from "../../contexts/LanguageContext";
|
||||
|
||||
type DateienItemProps = {
|
||||
|
|
|
|||
578
src/components/FormGenerator/FormGenerator.module.css
Normal file
578
src/components/FormGenerator/FormGenerator.module.css
Normal file
|
|
@ -0,0 +1,578 @@
|
|||
.formGenerator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Controls Section */
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 15px;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.floatingLabelInput {
|
||||
position: relative;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-text);
|
||||
opacity: 0.6;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s ease;
|
||||
background-color: transparent;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.focusedLabel {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: -8px;
|
||||
transform: translateY(0);
|
||||
color: var(--color-secondary);
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s ease;
|
||||
background-color: var(--color-bg);
|
||||
padding: 0 4px;
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 25px;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.searchInput:focus {
|
||||
border-color: var(--color-secondary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--color-secondary), 0.1);
|
||||
}
|
||||
|
||||
.searchInput::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.filtersContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filterGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filterGroup .floatingLabelInput {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.customSelectContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.filterInput {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 25px;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
opacity: 0.6;
|
||||
min-width: 120px;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filterInput::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.filterSelect {
|
||||
height: 40px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 25px;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
opacity: 0.6;
|
||||
min-width: 120px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.filterSelect {
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 16px;
|
||||
padding-right: 35px;
|
||||
}
|
||||
|
||||
/* Hide dropdown arrow when filter has a value */
|
||||
.filterSelect.hasValue {
|
||||
background-image: none;
|
||||
color: var(--color-secondary);
|
||||
border-color: var(--color-secondary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.filterInput:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-secondary);
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 2px rgba(var(--color-secondary), 0.1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.filterSelect:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-secondary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.clearFilterButton {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.clearFilterButton:hover {
|
||||
background: none;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Table Container */
|
||||
.tableContainer {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 25px;
|
||||
background: var(--color-bg);
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px;
|
||||
font-size: 16px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
background: var(--color-bg);
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--color-bg);
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
}
|
||||
|
||||
.th.actionsColumn {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.th.sortable {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.th.sortable:hover {
|
||||
background: var(--color-gray-disabled);
|
||||
}
|
||||
|
||||
.headerContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sortIcon {
|
||||
font-size: 12px;
|
||||
color: var(--color-secondary);
|
||||
opacity: 1.;
|
||||
}
|
||||
|
||||
.resizeHandle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.resizeHandle:hover {
|
||||
background: var(--color-secondary);
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--color-primary);
|
||||
color: var(--color-text);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tr {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.tr:hover {
|
||||
background: var(--color-gray-disabled);
|
||||
}
|
||||
|
||||
.tr.selected {
|
||||
background: rgba(var(--color-secondary-rgb), 0.1);
|
||||
}
|
||||
|
||||
.tr.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Selection Column */
|
||||
.selectColumn {
|
||||
text-align: center;
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
.selectColumn input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Actions Column */
|
||||
.actionsColumn {
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
padding: 12px 8px !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Actions Column border only on body cells, not header */
|
||||
tbody .actionsColumn {
|
||||
border-top: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-family: var(--font-family);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
.actionButton:hover {
|
||||
background: var(--color-secondary-hover);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
font-size: 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Custom Tooltip */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
bottom: 120%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-text);
|
||||
color: var(--color-bg);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Tooltip arrow */
|
||||
.tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Show tooltip on button hover */
|
||||
.actionButton:hover .tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
border-top: 1px solid var(--color-gray-disabled);
|
||||
background: var(--color-bg);
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.paginationButton {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.paginationButton:hover:not(:disabled) {
|
||||
background: var(--color-gray-disabled);
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.paginationButton:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.paginationInfo {
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
margin: 0 15px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 15px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.filtersContainer {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filterGroup {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.filterInput,
|
||||
.filterSelect {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.floatingLabelInput {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.filterGroup .floatingLabelInput {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tableContainer {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.th,
|
||||
.td {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.paginationInfo {
|
||||
order: -1;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.th.sortable:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tr:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.tr.selected {
|
||||
background: rgba(var(--color-secondary-rgb), 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.actionButton:focus,
|
||||
.paginationButton:focus,
|
||||
.searchInput:focus,
|
||||
.filterInput:focus,
|
||||
.filterSelect:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for table container */
|
||||
.tableContainer::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.tableContainer::-webkit-scrollbar-track {
|
||||
background: var(--color-gray-disabled);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tableContainer::-webkit-scrollbar-thumb {
|
||||
background: var(--color-gray);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tableContainer::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-secondary);
|
||||
}
|
||||
660
src/components/FormGenerator/FormGenerator.tsx
Normal file
660
src/components/FormGenerator/FormGenerator.tsx
Normal file
|
|
@ -0,0 +1,660 @@
|
|||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import styles from './FormGenerator.module.css';
|
||||
|
||||
// Types for the FormGenerator
|
||||
export interface ColumnConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
type?: 'string' | 'number' | 'date' | 'boolean' | 'enum';
|
||||
width?: number;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
searchable?: boolean;
|
||||
formatter?: (value: any, row: any) => React.ReactNode;
|
||||
filterOptions?: string[]; // For enum/select filters
|
||||
}
|
||||
|
||||
export interface FormGeneratorProps<T = any> {
|
||||
data: T[];
|
||||
columns?: ColumnConfig[];
|
||||
title?: string;
|
||||
searchable?: boolean;
|
||||
filterable?: boolean;
|
||||
sortable?: boolean;
|
||||
resizable?: boolean;
|
||||
pagination?: boolean;
|
||||
pageSize?: number;
|
||||
onRowClick?: (row: T, index: number) => void;
|
||||
onRowSelect?: (selectedRows: T[]) => void;
|
||||
selectable?: boolean;
|
||||
loading?: boolean;
|
||||
actions?: {
|
||||
label: string;
|
||||
onClick: (row: T) => void;
|
||||
icon?: string | React.ReactNode | ((row: T) => React.ReactNode);
|
||||
}[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FormGenerator<T extends Record<string, any>>({
|
||||
data,
|
||||
columns: providedColumns,
|
||||
title,
|
||||
searchable = true,
|
||||
filterable = true,
|
||||
sortable = true,
|
||||
resizable = true,
|
||||
pagination = true,
|
||||
pageSize = 10,
|
||||
onRowClick,
|
||||
onRowSelect,
|
||||
selectable = false,
|
||||
loading = false,
|
||||
actions = [],
|
||||
className = ''
|
||||
}: FormGeneratorProps<T>) {
|
||||
// Auto-detect columns if not provided
|
||||
const detectedColumns = useMemo((): ColumnConfig[] => {
|
||||
if (providedColumns) return providedColumns;
|
||||
|
||||
if (data.length === 0) return [];
|
||||
|
||||
const sampleRow = data[0];
|
||||
return Object.keys(sampleRow).map(key => {
|
||||
const value = sampleRow[key];
|
||||
let type: ColumnConfig['type'] = 'string';
|
||||
|
||||
// Auto-detect type based on value
|
||||
if (typeof value === 'number') {
|
||||
type = 'number';
|
||||
} else if (typeof value === 'boolean') {
|
||||
type = 'boolean';
|
||||
} else if (value instanceof Date || (typeof value === 'string' && !isNaN(Date.parse(value)) && value.includes('-'))) {
|
||||
type = 'date';
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
label: key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1'),
|
||||
type,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
searchable: type === 'string',
|
||||
width: 150,
|
||||
minWidth: 100,
|
||||
maxWidth: 400
|
||||
};
|
||||
});
|
||||
}, [data, providedColumns]);
|
||||
|
||||
// State management
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [searchFocused, setSearchFocused] = useState(false);
|
||||
const [filterFocused, setFilterFocused] = useState<Record<string, boolean>>({});
|
||||
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' } | null>(null);
|
||||
const [filters, setFilters] = useState<Record<string, any>>({});
|
||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
||||
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
// Refs for resizing
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
const resizingColumn = useRef<string | null>(null);
|
||||
const startX = useRef<number>(0);
|
||||
const startWidth = useRef<number>(0);
|
||||
|
||||
// Initialize column widths
|
||||
useEffect(() => {
|
||||
const initialWidths: Record<string, number> = {};
|
||||
detectedColumns.forEach(col => {
|
||||
// Set a default width if none specified to ensure all columns have explicit widths
|
||||
initialWidths[col.key] = col.width || 150;
|
||||
});
|
||||
setColumnWidths(initialWidths);
|
||||
}, [detectedColumns]);
|
||||
|
||||
// Filter and search data
|
||||
const filteredData = useMemo(() => {
|
||||
let result = [...data];
|
||||
|
||||
// Apply search filter
|
||||
if (searchTerm && searchable) {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
result = result.filter(row => {
|
||||
return detectedColumns.some(col => {
|
||||
if (!col.searchable) return false;
|
||||
const value = row[col.key];
|
||||
return String(value).toLowerCase().includes(searchLower);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Apply column filters
|
||||
Object.entries(filters).forEach(([key, filterValue]) => {
|
||||
if (filterValue !== undefined && filterValue !== '') {
|
||||
result = result.filter(row => {
|
||||
const value = row[key];
|
||||
const column = detectedColumns.find(col => col.key === key);
|
||||
|
||||
if (column?.type === 'boolean') {
|
||||
return Boolean(value) === Boolean(filterValue);
|
||||
} else if (column?.type === 'number') {
|
||||
return Number(value) === Number(filterValue);
|
||||
} else if (column?.type === 'date') {
|
||||
// Convert DD.MM.YYYY to comparable format
|
||||
const parseDate = (dateStr: string) => {
|
||||
if (dateStr.includes('.')) {
|
||||
const [day, month, year] = dateStr.split('.');
|
||||
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||
}
|
||||
return new Date(dateStr);
|
||||
};
|
||||
|
||||
// Convert row value to DD.MM.YYYY format for comparison
|
||||
const rowDate = new Date(value);
|
||||
const rowFormatted = `${rowDate.getDate().toString().padStart(2, '0')}.${(rowDate.getMonth() + 1).toString().padStart(2, '0')}.${rowDate.getFullYear()}`;
|
||||
|
||||
// Check if filter value is complete (DD.MM.YYYY)
|
||||
if (filterValue.length === 10 && filterValue.match(/^\d{2}\.\d{2}\.\d{4}$/)) {
|
||||
return rowFormatted === filterValue;
|
||||
} else {
|
||||
// Partial matching for incomplete dates
|
||||
return rowFormatted.startsWith(filterValue);
|
||||
}
|
||||
} else {
|
||||
return String(value).toLowerCase().includes(String(filterValue).toLowerCase());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Apply sorting
|
||||
if (sortConfig) {
|
||||
result.sort((a, b) => {
|
||||
const aVal = a[sortConfig.key];
|
||||
const bVal = b[sortConfig.key];
|
||||
|
||||
if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [data, searchTerm, filters, sortConfig, detectedColumns, searchable]);
|
||||
|
||||
// Pagination
|
||||
const paginatedData = useMemo(() => {
|
||||
if (!pagination) return filteredData;
|
||||
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
return filteredData.slice(startIndex, startIndex + pageSize);
|
||||
}, [filteredData, currentPage, pageSize, pagination]);
|
||||
|
||||
const totalPages = Math.ceil(filteredData.length / pageSize);
|
||||
|
||||
// Handle sorting
|
||||
const handleSort = (key: string) => {
|
||||
if (!sortable) return;
|
||||
|
||||
setSortConfig(current => {
|
||||
if (current?.key === key) {
|
||||
return current.direction === 'asc'
|
||||
? { key, direction: 'desc' }
|
||||
: null;
|
||||
}
|
||||
return { key, direction: 'asc' };
|
||||
});
|
||||
};
|
||||
|
||||
// Handle filtering
|
||||
const handleFilter = (key: string, value: any) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
}));
|
||||
setCurrentPage(1); // Reset to first page when filtering
|
||||
};
|
||||
|
||||
// Handle filter input focus
|
||||
const handleFilterFocus = (key: string, focused: boolean) => {
|
||||
setFilterFocused(prev => ({
|
||||
...prev,
|
||||
[key]: focused
|
||||
}));
|
||||
};
|
||||
|
||||
// Handle row selection
|
||||
const handleRowSelect = (index: number) => {
|
||||
if (!selectable) return;
|
||||
|
||||
const newSelected = new Set(selectedRows);
|
||||
if (newSelected.has(index)) {
|
||||
newSelected.delete(index);
|
||||
} else {
|
||||
newSelected.add(index);
|
||||
}
|
||||
setSelectedRows(newSelected);
|
||||
|
||||
if (onRowSelect) {
|
||||
const selectedData = Array.from(newSelected).map(i => paginatedData[i]);
|
||||
onRowSelect(selectedData);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle select all
|
||||
const handleSelectAll = () => {
|
||||
if (!selectable) return;
|
||||
|
||||
if (selectedRows.size === paginatedData.length) {
|
||||
setSelectedRows(new Set());
|
||||
onRowSelect?.([]);
|
||||
} else {
|
||||
const allIndices = new Set(paginatedData.map((_, index) => index));
|
||||
setSelectedRows(allIndices);
|
||||
onRowSelect?.(paginatedData);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle column resizing
|
||||
const handleMouseDown = (e: React.MouseEvent, columnKey: string) => {
|
||||
if (!resizable) return;
|
||||
|
||||
e.preventDefault();
|
||||
resizingColumn.current = columnKey;
|
||||
startX.current = e.clientX;
|
||||
startWidth.current = columnWidths[columnKey] || 150;
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!resizingColumn.current) return;
|
||||
|
||||
const diff = e.clientX - startX.current;
|
||||
let newWidth = Math.max(100, startWidth.current + diff);
|
||||
|
||||
// Prevent extending beyond table container
|
||||
const tableContainer = tableRef.current?.parentElement;
|
||||
if (tableContainer) {
|
||||
const containerWidth = tableContainer.clientWidth;
|
||||
const actionsColumnWidth = actions.length > 0 ? 120 : 0;
|
||||
const selectColumnWidth = selectable ? 40 : 0;
|
||||
const fixedWidth = actionsColumnWidth + selectColumnWidth;
|
||||
|
||||
// Calculate total width of all OTHER data columns (excluding the one being resized)
|
||||
const otherDataColumnsWidth = detectedColumns.reduce((total, col) => {
|
||||
if (col.key !== resizingColumn.current) {
|
||||
return total + (columnWidths[col.key] || col.width || 150);
|
||||
}
|
||||
return total;
|
||||
}, 0);
|
||||
|
||||
// Maximum allowed width for this column
|
||||
const maxAllowedWidth = containerWidth - fixedWidth - otherDataColumnsWidth - 40; // 40px buffer
|
||||
newWidth = Math.min(newWidth, Math.max(100, maxAllowedWidth));
|
||||
}
|
||||
|
||||
setColumnWidths(prev => ({
|
||||
...prev,
|
||||
[resizingColumn.current!]: newWidth
|
||||
}));
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
resizingColumn.current = null;
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
// Format cell value
|
||||
const formatCellValue = (value: any, column: ColumnConfig, row: T) => {
|
||||
if (column.formatter) {
|
||||
return column.formatter(value, row);
|
||||
}
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
switch (column.type) {
|
||||
case 'date':
|
||||
return new Date(value).toLocaleDateString();
|
||||
case 'boolean':
|
||||
return value ? '✓' : '✗';
|
||||
case 'number':
|
||||
return typeof value === 'number' ? value.toLocaleString() : value;
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.formGenerator} ${className}`}>
|
||||
{(searchable || filterable) && (
|
||||
<div className={styles.controls}>
|
||||
{searchable && (
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.floatingLabelInput}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder=" "
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onFocus={() => setSearchFocused(true)}
|
||||
onBlur={() => setSearchFocused(false)}
|
||||
className={`${styles.searchInput} ${searchFocused || searchTerm ? styles.focused : ''}`}
|
||||
/>
|
||||
<label className={searchFocused || searchTerm ? styles.focusedLabel : styles.label}>Search...</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filterable && (
|
||||
<div className={styles.filtersContainer}>
|
||||
{detectedColumns.filter(col => col.filterable).map(column => (
|
||||
<div key={column.key} className={styles.filterGroup}>
|
||||
{column.type === 'boolean' ? (
|
||||
<div className={styles.customSelectContainer}>
|
||||
<select
|
||||
value={filters[column.key] || ''}
|
||||
onChange={(e) => handleFilter(column.key, e.target.value === '' ? undefined : e.target.value === 'true')}
|
||||
className={`${styles.filterSelect} ${filters[column.key] ? styles.hasValue : ''}`}
|
||||
>
|
||||
<option value="" disabled hidden>{column.label}</option>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
{filters[column.key] && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleFilter(column.key, '')}
|
||||
className={styles.clearFilterButton}
|
||||
title="Clear filter"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : column.filterOptions ? (
|
||||
<div className={styles.customSelectContainer}>
|
||||
<select
|
||||
value={filters[column.key] || ''}
|
||||
onChange={(e) => handleFilter(column.key, e.target.value)}
|
||||
className={`${styles.filterSelect} ${filters[column.key] ? styles.hasValue : ''}`}
|
||||
>
|
||||
<option value="" disabled hidden>{column.label}</option>
|
||||
|
||||
{column.filterOptions.map(option => (
|
||||
<option key={option} value={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
{filters[column.key] && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleFilter(column.key, '')}
|
||||
className={styles.clearFilterButton}
|
||||
title="Clear filter"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : column.type === 'date' ? (
|
||||
<div className={styles.floatingLabelInput}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder=" "
|
||||
value={filters[column.key] || ''}
|
||||
onChange={(e) => {
|
||||
let value = e.target.value;
|
||||
const currentValue = filters[column.key] || '';
|
||||
|
||||
// Check if user is deleting (new value is shorter)
|
||||
const isDeleting = value.length < currentValue.length;
|
||||
|
||||
if (isDeleting) {
|
||||
// When deleting, preserve the exact input without auto-formatting
|
||||
handleFilter(column.key, value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-pad single digits followed by dot (e.g., "4." -> "04.")
|
||||
value = value.replace(/^(\d)\./, '0$1.');
|
||||
value = value.replace(/\.(\d)\./, '.0$1.');
|
||||
|
||||
// Allow typing and format as DD.MM.YYYY
|
||||
const digitsOnly = value.replace(/\D/g, ''); // Remove non-digits
|
||||
|
||||
let formatted = '';
|
||||
if (digitsOnly.length >= 8) {
|
||||
// Full format: DDMMYYYY -> DD.MM.YYYY
|
||||
const day = digitsOnly.slice(0, 2);
|
||||
const month = digitsOnly.slice(2, 4);
|
||||
const year = digitsOnly.slice(4, 8);
|
||||
|
||||
// Validate day (01-31) and month (01-12)
|
||||
if (parseInt(day) > 31 || parseInt(month) > 12 || parseInt(day) === 0 || parseInt(month) === 0) {
|
||||
return; // Don't update if invalid
|
||||
}
|
||||
formatted = `${day}.${month}.${year}`;
|
||||
} else if (digitsOnly.length >= 4) {
|
||||
// Partial format: DDMM -> DD.MM.
|
||||
const day = digitsOnly.slice(0, 2);
|
||||
const month = digitsOnly.slice(2, 4);
|
||||
const remaining = digitsOnly.slice(4);
|
||||
|
||||
// Validate day (01-31) and month (01-12)
|
||||
if (parseInt(day) > 31 || parseInt(month) > 12 || parseInt(day) === 0 || parseInt(month) === 0) {
|
||||
return; // Don't update if invalid
|
||||
}
|
||||
formatted = `${day}.${month}.${remaining}`;
|
||||
} else if (digitsOnly.length >= 2) {
|
||||
// Start format: DD -> DD.
|
||||
const day = digitsOnly.slice(0, 2);
|
||||
const remaining = digitsOnly.slice(2);
|
||||
|
||||
// Validate day (01-31)
|
||||
if (parseInt(day) > 31 || parseInt(day) === 0) {
|
||||
return; // Don't update if invalid
|
||||
}
|
||||
formatted = `${day}.${remaining}`;
|
||||
} else {
|
||||
// Just digits
|
||||
formatted = digitsOnly;
|
||||
}
|
||||
|
||||
handleFilter(column.key, formatted);
|
||||
}}
|
||||
onFocus={() => handleFilterFocus(column.key, true)}
|
||||
onBlur={() => handleFilterFocus(column.key, false)}
|
||||
className={`${styles.filterInput} ${filterFocused[column.key] || filters[column.key] ? styles.focused : ''}`}
|
||||
maxLength={10}
|
||||
/>
|
||||
<label className={filterFocused[column.key] || filters[column.key] ? styles.focusedLabel : styles.label}>
|
||||
{column.label}
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.floatingLabelInput}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder=" "
|
||||
value={filters[column.key] || ''}
|
||||
onChange={(e) => handleFilter(column.key, e.target.value)}
|
||||
onFocus={() => handleFilterFocus(column.key, true)}
|
||||
onBlur={() => handleFilterFocus(column.key, false)}
|
||||
className={`${styles.filterInput} ${filterFocused[column.key] || filters[column.key] ? styles.focused : ''}`}
|
||||
/>
|
||||
<label className={filterFocused[column.key] || filters[column.key] ? styles.focusedLabel : styles.label}>
|
||||
Filter {column.label}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table */}
|
||||
<div className={styles.tableContainer}>
|
||||
{(
|
||||
<table ref={tableRef} className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
{selectable && (
|
||||
<th className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRows.size === paginatedData.length && paginatedData.length > 0}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
{actions.length > 0 && (
|
||||
|
||||
<th className={styles.actionsColumn} style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}>
|
||||
Actions
|
||||
</th>
|
||||
)}
|
||||
{detectedColumns.map(column => (
|
||||
<th
|
||||
key={column.key}
|
||||
className={`${styles.th} ${sortable && column.sortable ? styles.sortable : ''}`}
|
||||
style={{
|
||||
width: columnWidths[column.key] || column.width || 150,
|
||||
minWidth: columnWidths[column.key] || column.width || 150,
|
||||
maxWidth: columnWidths[column.key] || column.width || 150
|
||||
}}
|
||||
onClick={() => column.sortable && handleSort(column.key)}
|
||||
>
|
||||
<div className={styles.headerContent}>
|
||||
<span>{column.label}</span>
|
||||
{sortable && column.sortable && (
|
||||
<span className={styles.sortIcon}>
|
||||
{sortConfig?.key === column.key ? (
|
||||
sortConfig.direction === 'asc' ? '↑' : '↓'
|
||||
) : '↕'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{resizable && (
|
||||
<div
|
||||
className={styles.resizeHandle}
|
||||
onMouseDown={(e) => handleMouseDown(e, column.key)}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedData.map((row, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={`${styles.tr} ${selectedRows.has(index) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
|
||||
onClick={() => onRowClick?.(row, index)}
|
||||
>
|
||||
{selectable && (
|
||||
<td className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRows.has(index)}
|
||||
onChange={() => handleRowSelect(index)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{actions.length > 0 && (
|
||||
<td className={styles.actionsColumn} style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}>
|
||||
<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>
|
||||
)}
|
||||
<span className={styles.tooltip}>{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
{detectedColumns.map(column => (
|
||||
<td
|
||||
key={column.key}
|
||||
className={styles.td}
|
||||
style={{
|
||||
width: columnWidths[column.key] || column.width || 150,
|
||||
minWidth: columnWidths[column.key] || column.width || 150,
|
||||
maxWidth: columnWidths[column.key] || column.width || 150
|
||||
}}
|
||||
>
|
||||
{formatCellValue(row[column.key], column, row)}
|
||||
</td>
|
||||
))}
|
||||
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{pagination && totalPages > 1 && (
|
||||
<div className={styles.pagination}>
|
||||
<button
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
className={styles.paginationButton}
|
||||
>
|
||||
««
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className={styles.paginationButton}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
|
||||
<span className={styles.paginationInfo}>
|
||||
Page {currentPage} of {totalPages} ({filteredData.length} items)
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className={styles.paginationButton}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
className={styles.paginationButton}
|
||||
>
|
||||
»»
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormGenerator;
|
||||
2
src/components/FormGenerator/index.ts
Normal file
2
src/components/FormGenerator/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as FormGenerator } from './FormGenerator';
|
||||
export type { ColumnConfig, FormGeneratorProps } from './FormGenerator';
|
||||
197
src/components/Popup/EditForm.module.css
Normal file
197
src/components/Popup/EditForm.module.css
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
/* EditForm container */
|
||||
.editForm {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Field styling */
|
||||
.fieldGroup {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.fieldLabel {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Floating label container */
|
||||
.floatingLabelInput {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.fieldInput {
|
||||
width: 100%;
|
||||
padding: 12px 12px 8px 12px;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 25px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
background-color: var(--color-bg);
|
||||
box-sizing: border-box;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.fieldInput:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.fieldInput.fieldError {
|
||||
border-color: #ef4444;
|
||||
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
/* Floating label styles */
|
||||
.label {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-primary);
|
||||
opacity: 0.7;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.focusedLabel {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: -8px;
|
||||
transform: translateY(0);
|
||||
color: var(--color-primary);
|
||||
opacity: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background-color: var(--color-bg);
|
||||
padding: 0 4px;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.activeFocusedLabel {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: -8px;
|
||||
transform: translateY(0);
|
||||
color: var(--color-secondary);
|
||||
opacity: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background-color: var(--color-bg);
|
||||
padding: 0 4px;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.readonlyField {
|
||||
padding: 12px 12px 8px 12px;
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 25px;
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
min-height: 20px;
|
||||
opacity: 0.7;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Checkbox styling */
|
||||
.checkboxLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.checkboxInput {
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Required field indicator */
|
||||
.required {
|
||||
color: #ef4444;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* Error text */
|
||||
.errorText {
|
||||
color: #ef4444;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Button group */
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--color-primary);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
border-radius: 25px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cancelButton:hover {
|
||||
background-color: var(--color-primary-hover);
|
||||
border-color: var(--color-primary);
|
||||
color: #181818;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border-radius: 25px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.saveButton:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.saveButton:disabled {
|
||||
background-color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 640px) {
|
||||
.buttonGroup {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.cancelButton,
|
||||
.saveButton {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
250
src/components/Popup/EditForm.tsx
Normal file
250
src/components/Popup/EditForm.tsx
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import styles from './EditForm.module.css';
|
||||
|
||||
// Field configuration interface (moved from EditPopup)
|
||||
export interface EditFieldConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'string' | 'email' | 'date' | 'enum' | 'boolean' | 'readonly';
|
||||
editable: boolean;
|
||||
required?: boolean;
|
||||
options?: string[]; // For enum types
|
||||
formatter?: (value: any) => string; // For display formatting
|
||||
validator?: (value: any) => string | null; // Returns error message or null
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
// EditForm props
|
||||
export interface EditFormProps<T = any> {
|
||||
data: T;
|
||||
fields: EditFieldConfig[];
|
||||
onSave: (updatedData: T) => void;
|
||||
onCancel?: () => void;
|
||||
saveButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
showButtons?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// EditForm component - handles form logic
|
||||
export function EditForm<T extends Record<string, any>>({
|
||||
data,
|
||||
fields,
|
||||
onSave,
|
||||
onCancel,
|
||||
saveButtonText = 'Save',
|
||||
cancelButtonText = 'Cancel',
|
||||
showButtons = true,
|
||||
className = ''
|
||||
}: EditFormProps<T>) {
|
||||
const [editedData, setEditedData] = useState<T>(data);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [fieldFocused, setFieldFocused] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Reset data when data changes
|
||||
useEffect(() => {
|
||||
setEditedData({ ...data });
|
||||
setErrors({});
|
||||
setFieldFocused({});
|
||||
}, [data]);
|
||||
|
||||
// Handle field focus
|
||||
const handleFieldFocus = (fieldKey: string, focused: boolean) => {
|
||||
setFieldFocused(prev => ({
|
||||
...prev,
|
||||
[fieldKey]: focused
|
||||
}));
|
||||
};
|
||||
|
||||
// Handle field value changes
|
||||
const handleFieldChange = (fieldKey: string, value: any) => {
|
||||
setEditedData(prev => ({
|
||||
...prev,
|
||||
[fieldKey]: value
|
||||
}));
|
||||
|
||||
// Clear error for this field when user starts typing
|
||||
if (errors[fieldKey]) {
|
||||
setErrors(prev => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[fieldKey];
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Validate all fields
|
||||
const validateFields = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
fields.forEach(field => {
|
||||
const value = editedData[field.key];
|
||||
|
||||
// Check required fields
|
||||
if (field.required && (!value || value.toString().trim() === '')) {
|
||||
newErrors[field.key] = `${field.label} is required`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Run custom validator
|
||||
if (field.validator && value) {
|
||||
const error = field.validator(value);
|
||||
if (error) {
|
||||
newErrors[field.key] = error;
|
||||
}
|
||||
}
|
||||
|
||||
// Basic email validation
|
||||
if (field.type === 'email' && value && !/\S+@\S+\.\S+/.test(value)) {
|
||||
newErrors[field.key] = 'Invalid email format';
|
||||
}
|
||||
});
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
// Handle save
|
||||
const handleSave = () => {
|
||||
if (validateFields()) {
|
||||
onSave(editedData);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle cancel
|
||||
const handleCancel = () => {
|
||||
setEditedData({ ...data });
|
||||
setErrors({});
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
// Helper function to get label class
|
||||
const getLabelClass = (fieldKey: string, value: any) => {
|
||||
const isFocused = fieldFocused[fieldKey];
|
||||
const hasValue = value && value.toString().trim() !== '';
|
||||
|
||||
if (isFocused) {
|
||||
return styles.activeFocusedLabel; // Secondary color when actively focused
|
||||
} else if (hasValue) {
|
||||
return styles.focusedLabel; // Primary color when has value but not focused
|
||||
} else {
|
||||
return styles.label; // Regular position when empty and not focused
|
||||
}
|
||||
};
|
||||
|
||||
// Render field based on its type
|
||||
const renderField = (field: EditFieldConfig) => {
|
||||
const value = editedData[field.key];
|
||||
const hasError = errors[field.key];
|
||||
|
||||
if (field.type === 'readonly' || !field.editable) {
|
||||
return (
|
||||
<div className={styles.floatingLabelInput} key={field.key}>
|
||||
<div className={styles.readonlyField}>
|
||||
{field.formatter ? field.formatter(value) : (value || 'N/A')}
|
||||
</div>
|
||||
<label className={styles.focusedLabel}>
|
||||
{field.label}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'enum') {
|
||||
return (
|
||||
<div className={styles.floatingLabelInput} key={field.key}>
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
onFocus={() => handleFieldFocus(field.key, true)}
|
||||
onBlur={() => handleFieldFocus(field.key, false)}
|
||||
className={`${styles.fieldInput} ${hasError ? styles.fieldError : ''}`}
|
||||
>
|
||||
<option value="" disabled hidden></option>
|
||||
{field.options?.map(option => (
|
||||
<option key={option} value={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
<label className={getLabelClass(field.key, value)}>
|
||||
{field.label}
|
||||
{field.required && <span className={styles.required}>*</span>}
|
||||
</label>
|
||||
{hasError && <span className={styles.errorText}>{hasError}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'boolean') {
|
||||
return (
|
||||
<div className={styles.fieldGroup} key={field.key}>
|
||||
<label className={styles.checkboxLabel}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!value}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.checked)}
|
||||
onFocus={() => handleFieldFocus(field.key, true)}
|
||||
onBlur={() => handleFieldFocus(field.key, false)}
|
||||
className={styles.checkboxInput}
|
||||
/>
|
||||
{field.label}
|
||||
{field.required && <span className={styles.required}>*</span>}
|
||||
</label>
|
||||
{hasError && <span className={styles.errorText}>{hasError}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default to text input for string, email, date types
|
||||
const inputType = field.type === 'email' ? 'email' :
|
||||
field.type === 'date' ? 'date' : 'text';
|
||||
|
||||
return (
|
||||
<div className={styles.floatingLabelInput} key={field.key}>
|
||||
<input
|
||||
type={inputType}
|
||||
value={value || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
onFocus={() => handleFieldFocus(field.key, true)}
|
||||
onBlur={() => handleFieldFocus(field.key, false)}
|
||||
className={`${styles.fieldInput} ${hasError ? styles.fieldError : ''}`}
|
||||
/>
|
||||
<label className={getLabelClass(field.key, value)}>
|
||||
{field.label}
|
||||
{field.required && <span className={styles.required}>*</span>}
|
||||
</label>
|
||||
{hasError && <span className={styles.errorText}>{hasError}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.editForm} ${className}`}>
|
||||
<form onSubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
{fields.map(field => renderField(field))}
|
||||
</form>
|
||||
|
||||
{showButtons && (
|
||||
<div className={styles.buttonGroup}>
|
||||
{onCancel && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.cancelButton}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{cancelButtonText}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.saveButton}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{saveButtonText}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditForm;
|
||||
148
src/components/Popup/Popup.module.css
Normal file
148
src/components/Popup/Popup.module.css
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
/* Overlay that covers the entire screen */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Main popup container */
|
||||
.popup {
|
||||
border: 1px solid var(--color-primary);
|
||||
background: var(--color-bg);
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
animation: popupSlideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* Size variants */
|
||||
.small {
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.medium {
|
||||
max-width: 600px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.large {
|
||||
max-width: 900px;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
width: 95vw;
|
||||
height: 95vh;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
/* Popup animation */
|
||||
@keyframes popupSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Header section */
|
||||
.header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid var(--color-primary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--color-bg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
/* Content section */
|
||||
.content {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Footer section */
|
||||
.footer {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--color-primary);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
background: var(--color-bg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 640px) {
|
||||
.popup {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.small,
|
||||
.medium,
|
||||
.large {
|
||||
min-width: 280px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 12px 16px;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
90
src/components/Popup/Popup.tsx
Normal file
90
src/components/Popup/Popup.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import React from 'react';
|
||||
import styles from './Popup.module.css';
|
||||
|
||||
// Generic popup props
|
||||
export interface PopupProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
footerContent?: React.ReactNode;
|
||||
className?: string;
|
||||
size?: 'small' | 'medium' | 'large' | 'fullscreen';
|
||||
closable?: boolean;
|
||||
}
|
||||
|
||||
// Generic Popup component - can be used for any content
|
||||
export function Popup({
|
||||
isOpen,
|
||||
title,
|
||||
onClose,
|
||||
children,
|
||||
footerContent,
|
||||
className = '',
|
||||
size = 'medium',
|
||||
closable = true
|
||||
}: PopupProps) {
|
||||
|
||||
// Handle escape key
|
||||
React.useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && closable) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
// Prevent body scroll when popup is open
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, [isOpen, closable, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Handle backdrop click
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget && closable) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.overlay} onClick={handleBackdropClick}>
|
||||
<div className={`${styles.popup} ${styles[size]} ${className}`}>
|
||||
{/* Header */}
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.title}>{title}</h2>
|
||||
{closable && (
|
||||
<button
|
||||
className={styles.closeButton}
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className={styles.content}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Footer (optional) */}
|
||||
{footerContent && (
|
||||
<div className={styles.footer}>
|
||||
{footerContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Popup;
|
||||
56
src/components/Popup/ViewForm.module.css
Normal file
56
src/components/Popup/ViewForm.module.css
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* ViewForm container */
|
||||
.viewForm {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Field styling */
|
||||
.fieldGroup {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.fieldGroup:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.fieldLabel {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.fieldValue {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* Special styling for different value types */
|
||||
.fieldValue:empty::before {
|
||||
content: 'N/A';
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 640px) {
|
||||
.fieldGroup {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.fieldLabel {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.fieldValue {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
40
src/components/Popup/ViewForm.tsx
Normal file
40
src/components/Popup/ViewForm.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
import styles from './ViewForm.module.css';
|
||||
import { EditFieldConfig } from './EditForm';
|
||||
|
||||
// ViewForm props - for display-only purposes
|
||||
export interface ViewFormProps<T = any> {
|
||||
data: T;
|
||||
fields: EditFieldConfig[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ViewForm component - displays data in read-only format
|
||||
export function ViewForm<T extends Record<string, any>>({
|
||||
data,
|
||||
fields,
|
||||
className = ''
|
||||
}: ViewFormProps<T>) {
|
||||
|
||||
// Render field in view-only mode
|
||||
const renderField = (field: EditFieldConfig) => {
|
||||
const value = data[field.key];
|
||||
|
||||
return (
|
||||
<div className={styles.fieldGroup} key={field.key}>
|
||||
<label className={styles.fieldLabel}>{field.label}</label>
|
||||
<div className={styles.fieldValue}>
|
||||
{field.formatter ? field.formatter(value) : (value || 'N/A')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.viewForm} ${className}`}>
|
||||
{fields.map(field => renderField(field))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewForm;
|
||||
11
src/components/Popup/index.ts
Normal file
11
src/components/Popup/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Generic Popup components
|
||||
export { Popup, default as DefaultPopup } from './Popup';
|
||||
export type { PopupProps } from './Popup';
|
||||
|
||||
// EditForm component
|
||||
export { EditForm } from './EditForm';
|
||||
export type { EditFormProps, EditFieldConfig } from './EditForm';
|
||||
|
||||
// ViewForm component
|
||||
export { ViewForm } from './ViewForm';
|
||||
export type { ViewFormProps } from './ViewForm';
|
||||
|
|
@ -1,66 +1,35 @@
|
|||
import React from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import styles from './Sidebar.module.css'
|
||||
|
||||
import styles from './SidebarStyles/Sidebar.module.css'
|
||||
import SidebarItem from './SidebarItem';
|
||||
import useSidebarData from '../../contexts/SidebarData';
|
||||
import SidebarUser from './SidebarUser';
|
||||
import { useCurrentUser } from '../../hooks/useUsers';
|
||||
import { useSidebarMachine } from '../../hooks/machines/useSidebarMachine';
|
||||
import { useSidebarLogic } from './sidebarLogic';
|
||||
import { SidebarProps } from './sidebarTypes';
|
||||
import { GoSidebarExpand, GoSidebarCollapse } from 'react-icons/go';
|
||||
|
||||
interface SidebarItemType {
|
||||
id: string;
|
||||
name: string;
|
||||
link?: string;
|
||||
submenu?: SidebarItemType[];
|
||||
}
|
||||
|
||||
interface SidebarProps {
|
||||
data: SidebarItemType[];
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ data }) => {
|
||||
const { user, isLoading, error } = useCurrentUser();
|
||||
|
||||
const sidebarMachine = useSidebarMachine();
|
||||
const sidebar = useSidebarLogic();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`${styles.sidebarContainer} ${sidebarMachine.isMinimized ? styles.minimized : ''}`}
|
||||
animate={{
|
||||
width: sidebarMachine.isMinimized ? 80 : 240
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
<div
|
||||
className={`${styles.sidebarContainer} ${sidebar.state.isMinimized ? styles.minimized : ''}`}
|
||||
>
|
||||
<div className={styles.logoContainer}>
|
||||
<AnimatePresence mode="wait">
|
||||
{!sidebarMachine.isMinimized && (
|
||||
<motion.div
|
||||
key="logo"
|
||||
className={styles.logoWrapper}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className={styles.logoWrapper}>
|
||||
<div className={styles.logoText}>
|
||||
<span className={styles.logoPower}>Power</span>
|
||||
<span className={styles.logoOn}>On</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Minimize/Expand Toggle Button */}
|
||||
<button
|
||||
className={styles.toggleButton}
|
||||
onClick={sidebarMachine.isMinimized ? sidebarMachine.expandSidebar : sidebarMachine.minimizeSidebar}
|
||||
title={sidebarMachine.isMinimized ? "Expand Sidebar" : "Minimize Sidebar"}
|
||||
onClick={sidebar.state.isMinimized ? sidebar.expandSidebar : sidebar.minimizeSidebar}
|
||||
title={sidebar.state.isMinimized ? "Expand Sidebar" : "Minimize Sidebar"}
|
||||
>
|
||||
{sidebarMachine.isMinimized ? (
|
||||
{sidebar.state.isMinimized ? (
|
||||
<GoSidebarCollapse size={20} />
|
||||
) : (
|
||||
<GoSidebarExpand size={20} />
|
||||
|
|
@ -68,44 +37,27 @@ const Sidebar: React.FC<SidebarProps> = ({ data }) => {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SidebarUser
|
||||
user={{
|
||||
id: user?.id || 0,
|
||||
name: user?.fullName || user?.username || '',
|
||||
role: user?.privilege || ''
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
isMinimized={sidebarMachine.isMinimized}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
<div
|
||||
className={styles.sidebar}
|
||||
animate={{
|
||||
width: sidebarMachine.isMinimized ? 80 : 240
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
>
|
||||
{data.map(item => {
|
||||
return (
|
||||
<SidebarItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isOpen={sidebarMachine.isItemOpen(item.id)}
|
||||
onToggle={() => sidebarMachine.toggleItem(item.id)}
|
||||
isActive={sidebarMachine.isItemActive(item.link)}
|
||||
isMinimized={sidebarMachine.isMinimized}
|
||||
isOpen={sidebar.isItemOpen(item.id)}
|
||||
onToggle={() => sidebar.toggleItem(item.id)}
|
||||
isActive={sidebar.isItemActive(item.link)}
|
||||
isMinimized={sidebar.state.isMinimized}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
<SidebarUser
|
||||
isMinimized={sidebar.state.isMinimized}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,26 +3,9 @@ import { Link } from "react-router-dom";
|
|||
import { IoIosArrowDown } from "react-icons/io";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
import styles from './SidebarItem.module.css';
|
||||
import styles from './SidebarStyles/SidebarItem.module.css';
|
||||
import SidebarSubmenu from "./SidebarSubmenu";
|
||||
|
||||
interface SidebarItemProps {
|
||||
item: {
|
||||
id: string;
|
||||
name: string;
|
||||
link?: string;
|
||||
icon?: React.ComponentType;
|
||||
submenu?: {
|
||||
id: string;
|
||||
name: string;
|
||||
link?: string;
|
||||
}[];
|
||||
};
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
isActive: boolean;
|
||||
isMinimized: boolean;
|
||||
}
|
||||
import { SidebarItemProps } from "./sidebarTypes";
|
||||
|
||||
const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||
item,
|
||||
|
|
@ -59,7 +42,7 @@ const SidebarItem: React.FC<SidebarItemProps> = ({
|
|||
</Link>
|
||||
)}
|
||||
|
||||
{/* Overlay for minimized state to ensure full area is clickable */}
|
||||
|
||||
{isMinimized && (
|
||||
<Link
|
||||
to={item.link || "#"}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,37 @@
|
|||
/* Allgemeine Stile */
|
||||
.sidebarContainer {
|
||||
border-radius: 0px;
|
||||
border-right: 1px solid var(--color-primary);
|
||||
background: var(--color-bg);
|
||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
||||
width: 240px;
|
||||
padding-bottom: 1px;
|
||||
display: flex;
|
||||
justify-content: top;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
transition: width 0.3s ease-in-out;
|
||||
position: relative;
|
||||
|
||||
border-right: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-shrink: 0;
|
||||
flex: 1;
|
||||
font-family: var(--font-family);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden; /* Disable horizontal scrolling */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.logoContainer {
|
||||
display: flex;
|
||||
height: 80px; /* Fixed height instead of auto */
|
||||
padding: 30px 20px 7px 20px;
|
||||
height: 80px;
|
||||
padding: 30px 20px 7px 27px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
|
@ -36,7 +43,19 @@
|
|||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 50%;
|
||||
|
||||
transform: translateY(-50%); /* Center vertically - prevents jumping */
|
||||
width: calc(100% - 90px); /* Full width minus button space */
|
||||
}
|
||||
|
||||
.logo {
|
||||
|
|
@ -52,6 +71,7 @@
|
|||
align-items: center;
|
||||
letter-spacing: -0.5px;
|
||||
font-weight: 200;
|
||||
|
||||
}
|
||||
|
||||
.logoPower {
|
||||
|
|
@ -73,23 +93,28 @@
|
|||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
}
|
||||
|
||||
.toggleButton:hover {
|
||||
background: none;
|
||||
transform: scale(1.05);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
/* Minimized Sidebar Styles */
|
||||
.sidebarContainer.minimized {
|
||||
width: 80px;
|
||||
display: flex;
|
||||
justify-content: top;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebarContainer.minimized .sidebar {
|
||||
|
|
@ -97,19 +122,36 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.sidebarContainer.minimized .logoContainer {
|
||||
height: 80px; /* Same fixed height as expanded */
|
||||
height: 80px;
|
||||
padding: 15px 10px 7px 10px;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sidebarContainer.minimized .logoWrapper {
|
||||
opacity: 0;
|
||||
left: 20px; /* Keep same left position */
|
||||
top: 50%;
|
||||
transform: translateY(-50%); /* Keep same vertical centering */
|
||||
}
|
||||
|
||||
.sidebarContainer.minimized .menuText {
|
||||
opacity: 0; /* Same rule as logo - fade out when sidebar minimized */
|
||||
}
|
||||
|
||||
.sidebarContainer.minimized .hassubmenu {
|
||||
opacity: 0; /* Same rule as logo - fade out when sidebar minimized */
|
||||
}
|
||||
|
||||
.sidebarContainer.minimized .text_content {
|
||||
opacity: 0; /* Same rule as logo and menu text - fade out when sidebar minimized */
|
||||
}
|
||||
|
||||
.sidebarContainer.minimized .toggleButton {
|
||||
margin: 0 auto;
|
||||
justify-content: center;
|
||||
/* Center the toggle button */
|
||||
right: 15px; /* Center in 80px width: (80-50)/2 = 15px */
|
||||
top: 50%;
|
||||
transform: translateY(-50%); /* Keep same vertical centering */
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -3,23 +3,28 @@
|
|||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.menu li {
|
||||
display: flex;
|
||||
width: 220px;
|
||||
height: 44px;
|
||||
padding: 0 3px 0 15px;
|
||||
padding: 0 3px 0 27px;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
color: var(--color-text);
|
||||
list-style: none;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
|
||||
.menu li:hover, .menu li.active {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
border-top-right-radius: 25px;
|
||||
border-bottom-right-radius: 25px;
|
||||
}
|
||||
|
||||
.menu li:hover a, .menu li.active a {
|
||||
|
|
@ -30,14 +35,23 @@
|
|||
.menu li a {
|
||||
text-decoration: none;
|
||||
font-family: var(--font-family);
|
||||
font-size: 14px;
|
||||
font-size: 0.9rem;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
color: inherit;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
justify-content: left;
|
||||
position: absolute;
|
||||
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.menuLink {
|
||||
|
|
@ -46,6 +60,7 @@
|
|||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding-right: 15px;
|
||||
padding-left: 35px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
|
@ -56,26 +71,32 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0; /* Prevent growth during transitions */
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%); /* Center vertically */
|
||||
}
|
||||
|
||||
.hassubmenu {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: transform 0.2s ease;
|
||||
opacity: 1;
|
||||
margin-left: auto; /* Push to right side, replacing gap spacing */
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Text content styling */
|
||||
.menuText {
|
||||
transition: opacity 0.3s ease, width 0.3s ease;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
margin-left: 5px; /* Replace gap with margin to prevent layout shifts */
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Minimized overlay */
|
||||
.minimizedOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -87,35 +108,22 @@
|
|||
|
||||
/* Minimized Menu Styles */
|
||||
.menu.minimized li {
|
||||
/* Keep the same height as expanded */
|
||||
width: 46px;
|
||||
padding: 0 3px 0 15px; /* Keep same padding structure */
|
||||
justify-content: flex-start; /* Keep icons in same position */
|
||||
margin: 0; /* Remove auto centering that causes jumping */
|
||||
padding: 0 3px 0 11px;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu.minimized .icon {
|
||||
/* Keep icon in exact same position as expanded */
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.menu.minimized .menuText {
|
||||
/* Hide text content */
|
||||
.menu.minimized li a{
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu.minimized .hassubmenu {
|
||||
/* Hide arrow */
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.menu.minimized li:hover,
|
||||
.menu.minimized li.active {
|
||||
/* Keep same hover/active styles but adjust border radius for smaller width */
|
||||
border-radius: 15px;
|
||||
color: var(--color-text);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -17,12 +17,7 @@
|
|||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.verticalLine {
|
||||
width: 1px;
|
||||
background-color: var(--color-gray-disabled);
|
||||
margin-left: 46px;
|
||||
margin-right: -40px;
|
||||
}
|
||||
|
||||
|
||||
.submenuList {
|
||||
margin: 5px 0;
|
||||
317
src/components/Sidebar/SidebarStyles/SidebarUser.module.css
Normal file
317
src/components/Sidebar/SidebarStyles/SidebarUser.module.css
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
.user_section {
|
||||
display: flex;
|
||||
width: 240px;
|
||||
flex-direction: column;
|
||||
align-items: left;
|
||||
font-family: var(--font-family);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
margin-top: auto; /* Push to bottom */
|
||||
margin-bottom: 7px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.user_info {
|
||||
display: flex;
|
||||
flex-direction: left;
|
||||
align-items: center;
|
||||
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
border-top-right-radius: 25px;
|
||||
border-bottom-right-radius : 25px;
|
||||
padding: 0 3px 0 15px;
|
||||
|
||||
opacity: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
.user_info.notClickable {
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
margin: -8px;
|
||||
background-color: var(--color-secondary, rgba(0, 0, 0, 0.05)); /* Compensate for padding to maintain original size */
|
||||
}
|
||||
|
||||
.user_info.clickable {
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
margin: -8px;
|
||||
background-color: none; /* Compensate for padding to maintain original size */
|
||||
}
|
||||
|
||||
.user_info.clickable:hover {
|
||||
background-color: var(--color-secondary, rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
|
||||
.user_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
transition: justify-content 0.3s ease;
|
||||
padding: 0 3px 0 15px;
|
||||
|
||||
}
|
||||
|
||||
.user_avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.text_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
opacity: 0; /* Start hidden */
|
||||
animation: delayedFadeIn 0.3s ease-in-out forwards; /* 0.3s delay + 0.3s fade in */
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 0.8rem !important;
|
||||
color: var(--color-text) !important;
|
||||
font-style: italic;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.logout_popup {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0px;
|
||||
right: 10px;
|
||||
background: var(--color-primary);
|
||||
border: none;
|
||||
border-top-right-radius: 25px;
|
||||
border-bottom-right-radius: 25px;
|
||||
z-index: 1000;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logout_menu_button {
|
||||
width: 100%;
|
||||
padding: 12px 25px;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family);
|
||||
font-size: 0.9rem;
|
||||
color: #181818;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
}
|
||||
|
||||
.logout_menu_button:hover:not(:disabled) {
|
||||
background-color: var(--color-hover, rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
|
||||
.logout_menu_button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.logout_icon {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.user_section h1 {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
opacity: 0; /* Start hidden */
|
||||
animation: delayedFadeIn 0.3s ease-in-out forwards; /* 0.3s delay + 0.3s fade in */
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
|
||||
.user_section p {
|
||||
margin: 0;
|
||||
font-family: var(--font-family);
|
||||
opacity: 0; /* Start hidden */
|
||||
animation: delayedFadeIn 0.3s ease-in-out forwards; /* 0.3s delay + 0.3s fade in */
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.userContainer {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: transparent;
|
||||
background-color: transparent;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
/* Minimized User Section Styles */
|
||||
.user_section.minimized {
|
||||
width: 100%; /* Match menu item width */
|
||||
padding: 20px 15px 8px 15px; /* Match menu item padding structure */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: transparent;
|
||||
background-color: transparent;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
.user_section.minimized .user_info {
|
||||
justify-content: center; /* Center the content when minimized */
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
color: transparent;
|
||||
background-color: transparent;
|
||||
border: transparent;
|
||||
|
||||
}
|
||||
|
||||
.user_section.minimized .user_info.clickable:hover {
|
||||
background-color: transparent; /* Remove orange background on hover when minimized */
|
||||
}
|
||||
|
||||
.user_section.minimized .user_info.notClickable {
|
||||
background-color: transparent; /* Remove orange background on hover when minimized */
|
||||
}
|
||||
|
||||
.user_section.minimized .user_header {
|
||||
justify-content: center; /* Center the avatar when minimized */
|
||||
width: 100%;
|
||||
padding: 0; /* Remove padding to center properly */
|
||||
}
|
||||
|
||||
.user_section.minimized .user_avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.user_section.minimized .text_content {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
color: transparent;
|
||||
background-color: transparent;
|
||||
border: transparent;
|
||||
animation: none; /* Disable animation when minimized */
|
||||
}
|
||||
|
||||
.user_section.minimized h1 {
|
||||
opacity: 0;
|
||||
color: transparent;
|
||||
background-color: transparent;
|
||||
border: transparent;
|
||||
animation: none; /* Disable animation when minimized */
|
||||
|
||||
}
|
||||
|
||||
.user_section.minimized p {
|
||||
opacity: 0;
|
||||
color: transparent;
|
||||
background-color: transparent;
|
||||
border: transparent;
|
||||
animation: none; /* Disable animation when minimized */
|
||||
}
|
||||
|
||||
/* Minimized logout popup styles */
|
||||
.logout_popup_minimized {
|
||||
left: 50%; /* Center horizontally over the user icon */
|
||||
bottom: calc(100% + 8px); /* Position above the user icon */
|
||||
transform: translateX(-50%) translateY(20px); /* Center and start position for animation */
|
||||
width: 40px; /* Same size as user avatar */
|
||||
height: 40px;
|
||||
margin-left: 0;
|
||||
margin-bottom: 0;
|
||||
border-radius: 50%; /* Make it circular */
|
||||
background: var(--color-primary);
|
||||
border: 1px solid var(--color-primary);
|
||||
overflow: visible; /* Allow tooltip to show */
|
||||
opacity: 0; /* Start invisible */
|
||||
animation: flyUpFadeIn 0.3s ease-out forwards; /* Animation */
|
||||
transition: opacity 0.2s ease-out; /* Smooth fade out when disappearing */
|
||||
}
|
||||
|
||||
.logout_popup_minimized .logout_menu_button {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%; /* Make button circular */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logout_popup_minimized .logout_menu_button::after {
|
||||
content: 'Abmelden';
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.logout_popup_minimized .logout_menu_button:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.logout_popup_minimized .logout_icon {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Animation keyframes for delayed text appearance */
|
||||
@keyframes delayedFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0; /* Stay hidden for first 50% (0.3s) */
|
||||
}
|
||||
100% {
|
||||
opacity: 1; /* Fade in during last 50% (0.3s) */
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation keyframes for fly up and fade in effect */
|
||||
@keyframes flyUpFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,20 +1,8 @@
|
|||
import styles from './SidebarSubmenu.module.css';
|
||||
import styles from './SidebarStyles/SidebarSubmenu.module.css';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
|
||||
interface SidebarSubmenuProps {
|
||||
item: {
|
||||
id: string;
|
||||
name: string;
|
||||
submenu?: {
|
||||
id: string;
|
||||
name: string;
|
||||
link?: string;
|
||||
}[];
|
||||
};
|
||||
isOpen: boolean;
|
||||
}
|
||||
import { SidebarSubmenuProps } from './sidebarTypes';
|
||||
|
||||
const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen }) => {
|
||||
if (!item.submenu) return null;
|
||||
|
|
@ -30,7 +18,6 @@ const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen }) => {
|
|||
className={styles.submenu}
|
||||
>
|
||||
<div className={styles.submenuLineContainer}>
|
||||
<div className={styles.verticalLine}></div>
|
||||
<ul className={styles.submenuList}>
|
||||
{item.submenu.map(subitem => {
|
||||
const textRef = useRef<HTMLSpanElement>(null);
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
.user_section {
|
||||
display: flex;
|
||||
width: 240px;
|
||||
padding: 20px;
|
||||
flex-direction: column;
|
||||
align-items: left;
|
||||
gap: 8px;
|
||||
font-family: var(--font-family);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.user_info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
transition: justify-content 0.3s ease;
|
||||
}
|
||||
|
||||
.user_icon {
|
||||
font-size: 40px;
|
||||
color: var(--color-gray);
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.text_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
transition: opacity 0.4s ease, width 0.4s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user_section h1 {
|
||||
margin: 0;
|
||||
font-size: 16pt;
|
||||
line-height: 1.;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
transition: opacity 0.3s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user_section p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-gray);
|
||||
font-family: var(--font-family);
|
||||
transition: opacity 0.35s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logout_button {
|
||||
margin-top: 4px;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
transition: all 0.4s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logout_button:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logout_button:active {
|
||||
background-color: var(--color-red);
|
||||
}
|
||||
|
||||
.logout_text {
|
||||
transition: opacity 0.3s ease, width 0.3s ease;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Minimized User Section Styles */
|
||||
.user_section.minimized {
|
||||
width: 46px; /* Match menu item width */
|
||||
padding: 20px 15px 20px 15px; /* Match menu item padding structure */
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user_section.minimized .user_info {
|
||||
justify-content: flex-start; /* Match menu items positioning */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user_section.minimized .user_icon {
|
||||
margin-left: -12px; /* Align with menu item icons at margin-left: -4px */
|
||||
font-size: 40px; /* Keep same size as expanded state */
|
||||
}
|
||||
|
||||
.user_section.minimized .text_content {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.user_section.minimized h1 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.user_section.minimized p {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.user_section.minimized .logout_button {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
|
@ -1,53 +1,122 @@
|
|||
import React from 'react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { useMsal } from '@azure/msal-react'
|
||||
import { FaUserCircle } from 'react-icons/fa'
|
||||
import styles from './SidebarUser.module.css'
|
||||
import { FaSignOutAlt } from 'react-icons/fa'
|
||||
import styles from './SidebarStyles/SidebarUser.module.css'
|
||||
import { useCurrentUser } from '../../hooks/useUsers'
|
||||
import { SidebarUserProps } from './sidebarTypes';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface SidebarUserProps {
|
||||
user: User;
|
||||
isLoading?: boolean;
|
||||
error?: string | null;
|
||||
isMinimized?: boolean;
|
||||
}
|
||||
|
||||
const SidebarUser: React.FC<SidebarUserProps> = ({ user, isLoading, error, isMinimized = false }) => {
|
||||
const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
|
||||
const { instance } = useMsal();
|
||||
const { user, isLoading, error, logout } = useCurrentUser();
|
||||
const [showLogoutMenu, setShowLogoutMenu] = useState(false);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
const userSectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Function to get initials from full name
|
||||
const getInitials = (fullName: string): string => {
|
||||
return fullName
|
||||
.split(' ')
|
||||
.map(name => name.charAt(0).toUpperCase())
|
||||
.join('')
|
||||
.substring(0, 2); // Limit to 2 characters
|
||||
};
|
||||
|
||||
const handleUserClick = () => {
|
||||
setShowLogoutMenu(!showLogoutMenu);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
if (!user || isLoggingOut) return;
|
||||
|
||||
setIsLoggingOut(true);
|
||||
try {
|
||||
// Clear MSAL cache and sign out
|
||||
await instance.logoutRedirect({
|
||||
onRedirectNavigate: () => {
|
||||
// Clear any application-specific data from localStorage
|
||||
localStorage.clear();
|
||||
return true;
|
||||
// Pass MSAL instance for Microsoft authentication
|
||||
if (user.authenticationAuthority === 'msft') {
|
||||
await logout(instance);
|
||||
} else {
|
||||
await logout();
|
||||
}
|
||||
});
|
||||
setShowLogoutMenu(false);
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
// Keep the menu open if logout fails so user can try again
|
||||
setIsLoggingOut(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Close popup when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (userSectionRef.current && !userSectionRef.current.contains(event.target as Node)) {
|
||||
setShowLogoutMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (showLogoutMenu) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [showLogoutMenu]);
|
||||
|
||||
if (isLoading) {
|
||||
return <div className={styles.userContainer}>Lädt...</div>;
|
||||
return (
|
||||
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
|
||||
<div className={styles.userContainer}>Lädt...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className={styles.userContainer}>Fehler beim Laden des Benutzerprofils</div>;
|
||||
return (
|
||||
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
|
||||
<div className={styles.userContainer}>Fehler beim Laden des Benutzerprofils</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
|
||||
<div className={styles.userContainer}>Kein Benutzer gefunden</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
|
||||
<div className={styles.user_info}>
|
||||
<div
|
||||
ref={userSectionRef}
|
||||
className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}
|
||||
>
|
||||
{showLogoutMenu && (
|
||||
<div className={`${styles.logout_popup} ${isMinimized ? styles.logout_popup_minimized : ''}`}>
|
||||
<button
|
||||
className={styles.logout_menu_button}
|
||||
onClick={handleLogout}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<FaSignOutAlt className={styles.logout_icon} />
|
||||
{!isMinimized && (isLoggingOut ? 'Abmelden...' : 'Abmelden')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`${styles.user_info} ${showLogoutMenu ? styles.notClickable : styles.clickable}`}
|
||||
onClick={handleUserClick}
|
||||
>
|
||||
<div className={styles.user_header}>
|
||||
<div className={styles.user_avatar}>
|
||||
{getInitials(user.fullName)}
|
||||
</div>
|
||||
{!isMinimized && (
|
||||
<div className={styles.text_content}>
|
||||
<h1>{ user.name }</h1>
|
||||
<p>Rolle: {user.role}</p>
|
||||
<h1>{user.fullName}</h1>
|
||||
<p className={styles.username}>{user.username}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import Sidebar from "./Sidebar";
|
||||
|
||||
export default Sidebar;
|
||||
export { default as SidebarItem } from './SidebarItem';
|
||||
export { default as SidebarUser } from './SidebarUser';
|
||||
export * from './sidebarTypes';
|
||||
export { useSidebarLogic } from './sidebarLogic';
|
||||
68
src/components/Sidebar/sidebarLogic.ts
Normal file
68
src/components/Sidebar/sidebarLogic.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { SidebarState, SidebarContextType } from './sidebarTypes';
|
||||
|
||||
// Custom hook for sidebar logic
|
||||
export const useSidebarLogic = (): SidebarContextType => {
|
||||
const location = useLocation();
|
||||
|
||||
// Simple React state instead of state machine
|
||||
const [state, setState] = useState<SidebarState>({
|
||||
openItemId: null,
|
||||
isMinimized: false,
|
||||
});
|
||||
|
||||
// Toggle a specific menu item
|
||||
const toggleItem = useCallback((itemId: string) => {
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
openItemId: prevState.openItemId === itemId ? null : itemId,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Close all submenus
|
||||
const closeAll = useCallback(() => {
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
openItemId: null,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Check if a specific item is open
|
||||
const isItemOpen = useCallback((itemId: string) => {
|
||||
return state.openItemId === itemId;
|
||||
}, [state.openItemId]);
|
||||
|
||||
// Check if an item is the active route
|
||||
const isItemActive = useCallback((itemPath?: string) => {
|
||||
if (!itemPath) return false;
|
||||
return location.pathname === itemPath;
|
||||
}, [location.pathname]);
|
||||
|
||||
// Minimize sidebar
|
||||
const minimizeSidebar = useCallback(() => {
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
isMinimized: true,
|
||||
openItemId: null, // Close any open submenu when minimizing
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Expand sidebar
|
||||
const expandSidebar = useCallback(() => {
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
isMinimized: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
state,
|
||||
toggleItem,
|
||||
closeAll,
|
||||
isItemOpen,
|
||||
isItemActive,
|
||||
minimizeSidebar,
|
||||
expandSidebar,
|
||||
};
|
||||
};
|
||||
56
src/components/Sidebar/sidebarTypes.ts
Normal file
56
src/components/Sidebar/sidebarTypes.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
|
||||
// Base sidebar item interface
|
||||
export interface SidebarItemData {
|
||||
id: string;
|
||||
name: string;
|
||||
link?: string;
|
||||
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||
submenu?: SidebarSubmenuItemData[];
|
||||
}
|
||||
|
||||
// Submenu item interface
|
||||
export interface SidebarSubmenuItemData {
|
||||
id: string;
|
||||
name: string;
|
||||
link?: string;
|
||||
}
|
||||
|
||||
// Sidebar state interface
|
||||
export interface SidebarState {
|
||||
openItemId: string | null;
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
||||
// Sidebar context interface
|
||||
export interface SidebarContextType {
|
||||
state: SidebarState;
|
||||
toggleItem: (itemId: string) => void;
|
||||
closeAll: () => void;
|
||||
isItemOpen: (itemId: string) => boolean;
|
||||
isItemActive: (itemPath?: string) => boolean;
|
||||
minimizeSidebar: () => void;
|
||||
expandSidebar: () => void;
|
||||
}
|
||||
|
||||
// Component props interfaces
|
||||
export interface SidebarProps {
|
||||
data: SidebarItemData[];
|
||||
}
|
||||
|
||||
export interface SidebarItemProps {
|
||||
item: SidebarItemData;
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
isActive: boolean;
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
||||
export interface SidebarSubmenuProps {
|
||||
item: SidebarItemData;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export interface SidebarUserProps {
|
||||
isMinimized?: boolean;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { Language, translations } from './languageContextData';
|
||||
import { Language, TranslationKeys, loadLanguage } from '../locales';
|
||||
|
||||
// Re-export Language type for convenience
|
||||
export type { Language };
|
||||
|
|
@ -8,6 +8,8 @@ interface LanguageContextType {
|
|||
currentLanguage: Language;
|
||||
setLanguage: (language: Language) => void;
|
||||
t: (key: string, fallback?: string) => string;
|
||||
isLoading: boolean;
|
||||
reloadLanguage: () => Promise<void>;
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
|
@ -16,39 +18,68 @@ interface LanguageProviderProps {
|
|||
children: ReactNode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children }) => {
|
||||
const [currentLanguage, setCurrentLanguage] = useState<Language>('de');
|
||||
const [translations, setTranslations] = useState<TranslationKeys>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Function to load and set a language
|
||||
const loadAndSetLanguage = async (language: Language) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const newTranslations = await loadLanguage(language);
|
||||
setTranslations(newTranslations);
|
||||
setCurrentLanguage(language);
|
||||
} catch (error) {
|
||||
console.error('Failed to load language:', error);
|
||||
// Keep current language and translations if loading fails
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load saved language preference on mount
|
||||
useEffect(() => {
|
||||
const initializeLanguage = async () => {
|
||||
const savedLanguage = localStorage.getItem('language') as Language;
|
||||
let initialLanguage: Language = 'de';
|
||||
|
||||
if (savedLanguage && ['de', 'en', 'fr'].includes(savedLanguage)) {
|
||||
setCurrentLanguage(savedLanguage);
|
||||
initialLanguage = savedLanguage;
|
||||
} else {
|
||||
// Detect browser language
|
||||
const browserLang = navigator.language.split('-')[0] as Language;
|
||||
if (['de', 'en', 'fr'].includes(browserLang)) {
|
||||
setCurrentLanguage(browserLang);
|
||||
initialLanguage = browserLang;
|
||||
}
|
||||
}
|
||||
|
||||
await loadAndSetLanguage(initialLanguage);
|
||||
};
|
||||
|
||||
initializeLanguage();
|
||||
}, []);
|
||||
|
||||
const setLanguage = (language: Language) => {
|
||||
setCurrentLanguage(language);
|
||||
const setLanguage = async (language: Language) => {
|
||||
localStorage.setItem('language', language);
|
||||
await loadAndSetLanguage(language);
|
||||
};
|
||||
|
||||
const reloadLanguage = async () => {
|
||||
await loadAndSetLanguage(currentLanguage);
|
||||
};
|
||||
|
||||
const t = (key: string, fallback?: string): string => {
|
||||
const translation = translations[currentLanguage]?.[key] || translations['de'][key] || fallback || key;
|
||||
const translation = translations[key] || fallback || key;
|
||||
return translation;
|
||||
};
|
||||
|
||||
const contextValue: LanguageContextType = {
|
||||
currentLanguage,
|
||||
setLanguage,
|
||||
t
|
||||
t,
|
||||
isLoading,
|
||||
reloadLanguage
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { MdOutlineWorkOutline } from 'react-icons/md';
|
||||
import { BsChatDots } from "react-icons/bs";
|
||||
import { LuTicket } from "react-icons/lu";
|
||||
import { LuWorkflow, LuTicket } from "react-icons/lu";
|
||||
import { RiTeamLine } from "react-icons/ri";
|
||||
import { BiInfoSquare } from "react-icons/bi";
|
||||
import { GoGear } from "react-icons/go";
|
||||
import { FaRegFileAlt } from "react-icons/fa";
|
||||
import { FaPlug, FaRegFileAlt } from "react-icons/fa";
|
||||
import { TbLogs } from "react-icons/tb";
|
||||
import { FaShare } from "react-icons/fa";
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useLanguage } from './LanguageContext';
|
||||
|
|
@ -16,40 +17,48 @@ const useSidebarData = () => {
|
|||
return useMemo(() => [
|
||||
{
|
||||
id: '1',
|
||||
name: t('nav.team'),
|
||||
link: '/team-bereich',
|
||||
icon: MdOutlineWorkOutline,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: t('nav.dashboard'),
|
||||
link: '/dashboard',
|
||||
icon: LuTicket,
|
||||
},
|
||||
|
||||
{
|
||||
id: '3',
|
||||
name: t('nav.files'),
|
||||
link: '/dateien',
|
||||
icon: FaRegFileAlt,
|
||||
},
|
||||
/*{
|
||||
id: '6',
|
||||
name: 'Logs',
|
||||
link: '',
|
||||
icon: TbLogs ,
|
||||
},*/
|
||||
{
|
||||
id: '4',
|
||||
name: t('nav.workflows'),
|
||||
link: '/workflows',
|
||||
icon: LuWorkflow ,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: t('nav.connections'),
|
||||
link: '/connections',
|
||||
icon: FaPlug ,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: t('nav.team'),
|
||||
link: '/team-bereich',
|
||||
icon: MdOutlineWorkOutline,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: t('nav.settings'),
|
||||
link: '/einstellungen',
|
||||
icon: GoGear,
|
||||
},
|
||||
/*{
|
||||
{
|
||||
id: '8',
|
||||
name: 'Help',
|
||||
link: '',
|
||||
icon: BiInfoSquare,
|
||||
},*/
|
||||
name: t('nav.testSharepoint'),
|
||||
link: '/testSharepoint',
|
||||
icon: FaShare,
|
||||
},
|
||||
|
||||
], [t]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,683 +0,0 @@
|
|||
// Language type definition
|
||||
export type Language = 'de' | 'en' | 'fr';
|
||||
|
||||
// Translation keys and their values for each language
|
||||
export type TranslationKeys = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export type Translations = {
|
||||
[K in Language]: TranslationKeys;
|
||||
};
|
||||
|
||||
export const translations: Translations = {
|
||||
de: {
|
||||
// Navigation
|
||||
'nav.dashboard': 'Aktivitätszentrum',
|
||||
'nav.files': 'Dateien',
|
||||
'nav.team': 'Team-Bereich',
|
||||
'nav.settings': 'Einstellungen',
|
||||
|
||||
// Settings page
|
||||
'settings.title': 'Einstellungen',
|
||||
'settings.appearance': 'Darstellung',
|
||||
'settings.language': 'Sprache',
|
||||
'settings.about': 'Über',
|
||||
'settings.version': 'Version',
|
||||
'settings.theme': 'Theme',
|
||||
'settings.theme.description': 'Wechseln Sie zwischen hellem und dunklem Modus',
|
||||
'settings.language.description': 'Wählen Sie Ihre bevorzugte Sprache',
|
||||
'settings.theme.light': 'Hell',
|
||||
'settings.theme.dark': 'Dunkel',
|
||||
'settings.theme.toggle.light': 'Zu hellem Modus wechseln',
|
||||
'settings.theme.toggle.dark': 'Zu dunklem Modus wechseln',
|
||||
|
||||
// Languages
|
||||
'language.german': 'Deutsch',
|
||||
'language.english': 'English',
|
||||
'language.french': 'Français',
|
||||
|
||||
// Common
|
||||
'common.loading': 'Laden...',
|
||||
'common.error': 'Fehler',
|
||||
'common.success': 'Erfolgreich',
|
||||
'common.cancel': 'Abbrechen',
|
||||
'common.save': 'Speichern',
|
||||
'common.delete': 'Löschen',
|
||||
'common.edit': 'Bearbeiten',
|
||||
'common.close': 'Schließen',
|
||||
|
||||
// Auth
|
||||
'auth.login': 'Anmelden',
|
||||
'auth.register': 'Registrieren',
|
||||
'auth.logout': 'Abmelden',
|
||||
'auth.email': 'E-Mail',
|
||||
'auth.password': 'Passwort',
|
||||
|
||||
// Dashboard
|
||||
'dashboard.prompt.template': 'Prompt Vorlage',
|
||||
'dashboard.prompt.settings': 'Einstellungen',
|
||||
'dashboard.chat.area': 'Chatbereich',
|
||||
'dashboard.chat.history': 'Workflow-Verlauf',
|
||||
'dashboard.log.title': 'Log',
|
||||
'dashboard.log.workflow': 'Workflow',
|
||||
'dashboard.log.no_workflow': 'Kein Workflow ausgewählt',
|
||||
'dashboard.log.loading': 'Logs werden geladen...',
|
||||
'dashboard.log.error': 'Fehler beim Laden der Logs',
|
||||
'dashboard.log.no_logs': 'Keine Logs für diesen Workflow verfügbar',
|
||||
'dashboard.log.waiting': 'Workflow läuft... Warte auf Logs...',
|
||||
'dashboard.log.fetch_failed': 'Logs konnten nicht geladen werden',
|
||||
'dashboard.log.level.info': 'INFO',
|
||||
|
||||
// Prompt Set
|
||||
'promptset.loading': 'Prompts werden geladen...',
|
||||
'promptset.error.loading': 'Fehler beim Laden der Prompts',
|
||||
'promptset.retry': 'Erneut versuchen',
|
||||
'promptset.new_prompt': 'Neuer Prompt',
|
||||
'promptset.prompt_count': 'Prompt',
|
||||
'promptset.prompt_count_plural': 'Prompts',
|
||||
'promptset.no_prompts': 'Keine Prompts verfügbar',
|
||||
'promptset.created': 'Erstellt',
|
||||
'promptset.run_tooltip': 'Prompt ausführen',
|
||||
'promptset.share_tooltip': 'Prompt teilen',
|
||||
'promptset.delete_tooltip': 'Prompt löschen',
|
||||
'promptset.confirm_delete': 'Klicken Sie erneut zum Bestätigen',
|
||||
'promptset.deleting': 'Löschen...',
|
||||
'promptset.confirm_click': 'Zum Bestätigen klicken',
|
||||
'promptset.delete_error': 'Fehler beim Löschen',
|
||||
'promptset.deleting_message': 'Prompt wird gelöscht...',
|
||||
|
||||
// Prompt Modal
|
||||
'modal.create_prompt': 'Neuen Prompt erstellen',
|
||||
'modal.name_required': 'Name ist erforderlich',
|
||||
'modal.content_required': 'Inhalt ist erforderlich',
|
||||
'modal.create_error': 'Fehler beim Erstellen des Prompts',
|
||||
'modal.name_label': 'Name',
|
||||
'modal.content_label': 'Inhalt',
|
||||
'modal.name_placeholder': 'Geben Sie einen Namen für den Prompt ein',
|
||||
'modal.content_placeholder': 'Geben Sie den Inhalt des Prompts ein',
|
||||
'modal.cancel': 'Abbrechen',
|
||||
'modal.creating': 'Erstellen...',
|
||||
'modal.create': 'Prompt erstellen',
|
||||
|
||||
// Share Modal
|
||||
'share_modal.title': 'Prompt teilen',
|
||||
'share_modal.select_users': 'Benutzer auswählen',
|
||||
'share_modal.select_all': 'Alle auswählen',
|
||||
'share_modal.deselect_all': 'Alle abwählen',
|
||||
'share_modal.loading_users': 'Benutzer werden geladen...',
|
||||
'share_modal.error_loading_users': 'Fehler beim Laden der Benutzer',
|
||||
'share_modal.no_users_available': 'Keine Benutzer verfügbar',
|
||||
'share_modal.no_users_selected': 'Bitte wählen Sie mindestens einen Benutzer aus',
|
||||
'share_modal.one_user_selected': '1 Benutzer ausgewählt',
|
||||
'share_modal.multiple_users_selected': '{count} Benutzer ausgewählt',
|
||||
'share_modal.custom_title': 'Benutzerdefinierter Titel (optional)',
|
||||
'share_modal.title_placeholder': 'Geben Sie einen benutzerdefinierten Titel ein',
|
||||
'share_modal.message': 'Nachricht (optional)',
|
||||
'share_modal.message_placeholder': 'Fügen Sie eine Nachricht für die Empfänger hinzu',
|
||||
'share_modal.share': 'Teilen',
|
||||
'share_modal.sharing': 'Wird geteilt...',
|
||||
'share_modal.share_error': 'Fehler beim Teilen des Prompts',
|
||||
|
||||
// Prompt Settings
|
||||
'prompt_settings.title': 'Prompt Einstellungen',
|
||||
'prompt_settings.content_placeholder': 'Einstellungen werden in zukünftigen Updates hinzugefügt.',
|
||||
|
||||
// Chat Area
|
||||
'chat.continue_conversation': 'Gespräch fortsetzen...',
|
||||
'chat.enter_message': 'Nachricht eingeben...',
|
||||
'chat.remove_file': 'Datei entfernen',
|
||||
'chat.attach_file': 'Datei anhängen',
|
||||
'chat.you': 'You',
|
||||
'chat.click_to_open': 'Klicken Sie, um zu öffnen',
|
||||
'chat.preview_document': 'Dokument vorschauen',
|
||||
'chat.download_document': 'Dokument herunterladen',
|
||||
'chat.workflow_failed': 'Workflow fehlgeschlagen.',
|
||||
'chat.retry_workflow': 'Nochmal versuchen',
|
||||
'chat.sending_followup': 'Folgenachricht wird gesendet...',
|
||||
'chat.sending_message': 'Nachricht wird gesendet...',
|
||||
'chat.error_prefix': 'Fehler:',
|
||||
'chat.error_loading_messages': 'Fehler beim Laden der Nachrichten:',
|
||||
'chat.loading_workflow_messages': 'Workflow-Nachrichten werden geladen...',
|
||||
'chat.start_conversation': 'Beginne ein Gespräch, indem du eine Nachricht eingibst, eine Vorlage auswählst oder einen vorherigen Workflow fortsetzt …',
|
||||
|
||||
// File Preview
|
||||
'file_preview.loading': 'Vorschau wird geladen...',
|
||||
'file_preview.error': 'Fehler',
|
||||
'file_preview.no_preview': 'Keine Vorschau verfügbar',
|
||||
'file_preview.close_preview': 'Vorschau schließen',
|
||||
'file_preview.python': 'Python',
|
||||
|
||||
// Chat History
|
||||
'chat_history.loading': 'Workflows werden geladen...',
|
||||
'chat_history.error_loading': 'Fehler beim Laden der Workflows:',
|
||||
'chat_history.try_again': 'Nochmal versuchen',
|
||||
'chat_history.title': 'Workflow-Verlauf',
|
||||
'chat_history.workflow_count': 'Workflow',
|
||||
'chat_history.workflow_count_plural': 'Workflows',
|
||||
'chat_history.empty_state': 'Keine Workflows verfügbar',
|
||||
'chat_history.confirm_delete': 'Sind Sie sicher, dass Sie Workflow "{id}..." löschen möchten?',
|
||||
'chat_history.no_message_content': 'Kein Nachrichteninhalt verfügbar',
|
||||
'chat_history.unknown_date': 'Unbekanntes Datum',
|
||||
'chat_history.invalid_date': 'Ungültiges Datum',
|
||||
'chat_history.started': 'Gestartet:',
|
||||
'chat_history.last_activity': 'Letzte Aktivität:',
|
||||
'chat_history.round': 'Runde',
|
||||
'chat_history.resume_tooltip': 'Workflow fortsetzen',
|
||||
'chat_history.delete_tooltip': 'Workflow löschen',
|
||||
'chat_history.deleting': 'Workflow wird gelöscht...',
|
||||
|
||||
// Workflow Status
|
||||
'status.error': 'FEHLER',
|
||||
'status.failed': 'FEHLGESCHLAGEN',
|
||||
'status.stopped': 'GESTOPPT',
|
||||
'status.cancelled': 'ABGEBROCHEN',
|
||||
'status.running': 'LÄUFT',
|
||||
'status.processing': 'VERARBEITUNG',
|
||||
'status.completed': 'ABGESCHLOSSEN',
|
||||
'status.pending': 'WARTEND',
|
||||
|
||||
// Files
|
||||
'files.unknown_size': 'Unbekannte Größe',
|
||||
'files.unknown_date': 'Unbekanntes Datum',
|
||||
'files.source.uploaded': 'Hochgeladen',
|
||||
'files.source.ai_created': 'KI-erstellt',
|
||||
'files.source.shared': 'Geteilt',
|
||||
'files.source.unknown': 'Unbekannt',
|
||||
'files.preview_tooltip': 'Datei vorschauen',
|
||||
'files.download_tooltip': 'Datei herunterladen',
|
||||
'files.delete_tooltip': 'Datei löschen',
|
||||
'files.delete_confirm_tooltip': 'Klicken Sie erneut zum Bestätigen der Löschung',
|
||||
'files.downloading': 'Laden...',
|
||||
'files.deleting': 'Löschen...',
|
||||
'files.delete_confirm': 'Zum Bestätigen klicken...',
|
||||
'files.no_files': 'Keine Dateien gefunden.',
|
||||
'files.no_shared_files': 'Keine mit Ihnen geteilten Dateien gefunden.',
|
||||
'files.no_ai_files': 'Keine von der KI erstellten Dateien gefunden.',
|
||||
'files.no_uploaded_files': 'Keine hochgeladenen Dateien gefunden.',
|
||||
'files.header.name': 'Name',
|
||||
'files.header.type': 'Typ',
|
||||
'files.header.size': 'Größe',
|
||||
'files.header.date': 'Datum',
|
||||
'files.selector.title': 'Dateien auswählen',
|
||||
'files.selector.tab.all': 'Alle Dateien',
|
||||
'files.selector.tab.uploads': 'Hochgeladen',
|
||||
'files.selector.tab.created': 'KI-erstellt',
|
||||
'files.selector.tab.shared': 'Geteilt',
|
||||
'files.selector.select_all': 'Alle auswählen',
|
||||
'files.selector.deselect_all': 'Alle abwählen',
|
||||
'files.selector.file_selected': 'Datei',
|
||||
'files.selector.files_selected': 'Dateien',
|
||||
'files.selector.selected_suffix': 'ausgewählt',
|
||||
'files.selector.upload_new': 'Neue Datei hochladen',
|
||||
'files.selector.loading': 'Dateien werden geladen...',
|
||||
'files.selector.error_loading': 'Fehler beim Laden der Dateien:',
|
||||
'files.upload.title': 'Datei hochladen',
|
||||
'files.upload.drop_here': 'Datei hier ablegen...',
|
||||
'files.upload.uploading': 'Lädt hoch...',
|
||||
'files.upload.drag_files': 'Dateien hierher ziehen',
|
||||
'files.upload.or': 'oder',
|
||||
'files.upload.browse': 'Durchsuchen',
|
||||
'files.upload.selected_file': 'Ausgewählte Datei:',
|
||||
'files.upload.upload_button': 'Hochladen',
|
||||
'files.upload.uploading_button': 'Wird hochgeladen...',
|
||||
'files.upload.success': 'Datei erfolgreich hochgeladen!',
|
||||
'files.upload.error': 'Beim Hochladen ist ein Fehler aufgetreten.',
|
||||
'files.upload.unexpected_error': 'Beim Hochladen ist ein unerwarteter Fehler aufgetreten.',
|
||||
|
||||
// Files Page
|
||||
'files.page.tab.all': 'Alle Dateien',
|
||||
'files.page.tab.uploads': 'Meine Uploads',
|
||||
'files.page.tab.created': 'Erstellte Dateien',
|
||||
'files.page.tab.shared': 'Geteilte Dateien',
|
||||
'files.page.add_file': 'Datei hinzufügen',
|
||||
'files.page.loading': 'Dateien werden geladen...',
|
||||
'files.page.error': 'Fehler:',
|
||||
},
|
||||
en: {
|
||||
// Navigation
|
||||
'nav.dashboard': 'Activity Center',
|
||||
'nav.files': 'Files',
|
||||
'nav.team': 'Team Area',
|
||||
'nav.settings': 'Settings',
|
||||
|
||||
// Settings page
|
||||
'settings.title': 'Settings',
|
||||
'settings.appearance': 'Appearance',
|
||||
'settings.language': 'Language',
|
||||
'settings.about': 'About',
|
||||
'settings.version': 'Version',
|
||||
'settings.theme': 'Theme',
|
||||
'settings.theme.description': 'Switch between light and dark mode',
|
||||
'settings.language.description': 'Choose your preferred language',
|
||||
'settings.theme.light': 'Light',
|
||||
'settings.theme.dark': 'Dark',
|
||||
'settings.theme.toggle.light': 'Switch to light mode',
|
||||
'settings.theme.toggle.dark': 'Switch to dark mode',
|
||||
|
||||
// Languages
|
||||
'language.german': 'Deutsch',
|
||||
'language.english': 'English',
|
||||
'language.french': 'Français',
|
||||
|
||||
// Common
|
||||
'common.loading': 'Loading...',
|
||||
'common.error': 'Error',
|
||||
'common.success': 'Success',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.save': 'Save',
|
||||
'common.delete': 'Delete',
|
||||
'common.edit': 'Edit',
|
||||
'common.close': 'Close',
|
||||
|
||||
// Auth
|
||||
'auth.login': 'Login',
|
||||
'auth.register': 'Register',
|
||||
'auth.logout': 'Logout',
|
||||
'auth.email': 'Email',
|
||||
'auth.password': 'Password',
|
||||
|
||||
// Dashboard
|
||||
'dashboard.prompt.template': 'Prompt Template',
|
||||
'dashboard.prompt.settings': 'Settings',
|
||||
'dashboard.chat.area': 'Chat Area',
|
||||
'dashboard.chat.history': 'Workflow History',
|
||||
'dashboard.log.title': 'Log',
|
||||
'dashboard.log.workflow': 'Workflow',
|
||||
'dashboard.log.no_workflow': 'No workflow selected',
|
||||
'dashboard.log.loading': 'Loading logs...',
|
||||
'dashboard.log.error': 'Error loading logs',
|
||||
'dashboard.log.no_logs': 'No logs available for this workflow',
|
||||
'dashboard.log.waiting': 'Workflow running... Waiting for logs...',
|
||||
'dashboard.log.fetch_failed': 'Failed to fetch logs',
|
||||
'dashboard.log.level.info': 'INFO',
|
||||
|
||||
// Prompt Set
|
||||
'promptset.loading': 'Loading prompts...',
|
||||
'promptset.error.loading': 'Error loading prompts',
|
||||
'promptset.retry': 'Try again',
|
||||
'promptset.new_prompt': 'New Prompt',
|
||||
'promptset.prompt_count': 'Prompt',
|
||||
'promptset.prompt_count_plural': 'Prompts',
|
||||
'promptset.no_prompts': 'No prompts available',
|
||||
'promptset.created': 'Created',
|
||||
'promptset.run_tooltip': 'Run prompt',
|
||||
'promptset.share_tooltip': 'Share prompt',
|
||||
'promptset.delete_tooltip': 'Delete prompt',
|
||||
'promptset.confirm_delete': 'Click again to confirm',
|
||||
'promptset.deleting': 'Deleting...',
|
||||
'promptset.confirm_click': 'Click to confirm',
|
||||
'promptset.delete_error': 'Error deleting',
|
||||
'promptset.deleting_message': 'Deleting prompt...',
|
||||
|
||||
// Prompt Modal
|
||||
'modal.create_prompt': 'Create New Prompt',
|
||||
'modal.name_required': 'Name is required',
|
||||
'modal.content_required': 'Content is required',
|
||||
'modal.create_error': 'Error creating prompt',
|
||||
'modal.name_label': 'Name',
|
||||
'modal.content_label': 'Content',
|
||||
'modal.name_placeholder': 'Enter a name for the prompt',
|
||||
'modal.content_placeholder': 'Enter the prompt content',
|
||||
'modal.cancel': 'Cancel',
|
||||
'modal.creating': 'Creating...',
|
||||
'modal.create': 'Create Prompt',
|
||||
|
||||
// Share Modal
|
||||
'share_modal.title': 'Share Prompt',
|
||||
'share_modal.select_users': 'Select Users',
|
||||
'share_modal.select_all': 'Select All',
|
||||
'share_modal.deselect_all': 'Deselect All',
|
||||
'share_modal.loading_users': 'Loading users...',
|
||||
'share_modal.error_loading_users': 'Error loading users',
|
||||
'share_modal.no_users_available': 'No users available',
|
||||
'share_modal.no_users_selected': 'Please select at least one user',
|
||||
'share_modal.one_user_selected': '1 user selected',
|
||||
'share_modal.multiple_users_selected': '{count} users selected',
|
||||
'share_modal.custom_title': 'Custom Title (optional)',
|
||||
'share_modal.title_placeholder': 'Enter a custom title',
|
||||
'share_modal.message': 'Message (optional)',
|
||||
'share_modal.message_placeholder': 'Add a message for recipients',
|
||||
'share_modal.share': 'Share',
|
||||
'share_modal.sharing': 'Sharing...',
|
||||
'share_modal.share_error': 'Error sharing prompt',
|
||||
|
||||
// Prompt Settings
|
||||
'prompt_settings.title': 'Prompt Settings',
|
||||
'prompt_settings.content_placeholder': 'Settings content will be added here in future updates.',
|
||||
|
||||
// Chat Area
|
||||
'chat.continue_conversation': 'Continue conversation...',
|
||||
'chat.enter_message': 'Enter message...',
|
||||
'chat.remove_file': 'Remove file',
|
||||
'chat.attach_file': 'Attach file',
|
||||
'chat.you': 'You',
|
||||
'chat.click_to_open': 'Click to open',
|
||||
'chat.preview_document': 'Preview document',
|
||||
'chat.download_document': 'Download document',
|
||||
'chat.workflow_failed': 'Workflow failed.',
|
||||
'chat.retry_workflow': 'Try again',
|
||||
'chat.sending_followup': 'Sending follow-up message...',
|
||||
'chat.sending_message': 'Sending message...',
|
||||
'chat.error_prefix': 'Error:',
|
||||
'chat.error_loading_messages': 'Error loading messages:',
|
||||
'chat.loading_workflow_messages': 'Loading workflow messages...',
|
||||
'chat.start_conversation': 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...',
|
||||
|
||||
// File Preview
|
||||
'file_preview.loading': 'Loading preview...',
|
||||
'file_preview.error': 'Error',
|
||||
'file_preview.no_preview': 'No preview available',
|
||||
'file_preview.close_preview': 'Close preview',
|
||||
'file_preview.python': 'Python',
|
||||
|
||||
// Chat History
|
||||
'chat_history.loading': 'Loading workflows...',
|
||||
'chat_history.error_loading': 'Error loading workflows:',
|
||||
'chat_history.try_again': 'Try Again',
|
||||
'chat_history.title': 'Workflow History',
|
||||
'chat_history.workflow_count': 'Workflow',
|
||||
'chat_history.workflow_count_plural': 'Workflows',
|
||||
'chat_history.empty_state': 'No workflows available',
|
||||
'chat_history.confirm_delete': 'Are you sure you want to delete workflow "{id}..."?',
|
||||
'chat_history.no_message_content': 'No message content available',
|
||||
'chat_history.unknown_date': 'Unknown date',
|
||||
'chat_history.invalid_date': 'Invalid date',
|
||||
'chat_history.started': 'Started:',
|
||||
'chat_history.last_activity': 'Last Activity:',
|
||||
'chat_history.round': 'Round',
|
||||
'chat_history.resume_tooltip': 'Resume workflow',
|
||||
'chat_history.delete_tooltip': 'Delete workflow',
|
||||
'chat_history.deleting': 'Deleting workflow...',
|
||||
|
||||
// Workflow Status
|
||||
'status.error': 'ERROR',
|
||||
'status.failed': 'FAILED',
|
||||
'status.stopped': 'STOPPED',
|
||||
'status.cancelled': 'CANCELLED',
|
||||
'status.running': 'RUNNING',
|
||||
'status.processing': 'PROCESSING',
|
||||
'status.completed': 'COMPLETED',
|
||||
'status.pending': 'PENDING',
|
||||
|
||||
// Files
|
||||
'files.unknown_size': 'Unknown Size',
|
||||
'files.unknown_date': 'Unknown Date',
|
||||
'files.source.uploaded': 'Uploaded',
|
||||
'files.source.ai_created': 'AI-created',
|
||||
'files.source.shared': 'Shared',
|
||||
'files.source.unknown': 'Unknown',
|
||||
'files.preview_tooltip': 'Preview file',
|
||||
'files.download_tooltip': 'Download file',
|
||||
'files.delete_tooltip': 'Delete file',
|
||||
'files.delete_confirm_tooltip': 'Click again to confirm deletion',
|
||||
'files.downloading': 'Downloading...',
|
||||
'files.deleting': 'Deleting...',
|
||||
'files.delete_confirm': 'Click to confirm...',
|
||||
'files.no_files': 'No files found.',
|
||||
'files.no_shared_files': 'No shared files found.',
|
||||
'files.no_ai_files': 'No AI-created files found.',
|
||||
'files.no_uploaded_files': 'No uploaded files found.',
|
||||
'files.header.name': 'Name',
|
||||
'files.header.type': 'Type',
|
||||
'files.header.size': 'Size',
|
||||
'files.header.date': 'Date',
|
||||
'files.selector.title': 'Select files',
|
||||
'files.selector.tab.all': 'All files',
|
||||
'files.selector.tab.uploads': 'Uploaded',
|
||||
'files.selector.tab.created': 'AI-created',
|
||||
'files.selector.tab.shared': 'Shared',
|
||||
'files.selector.select_all': 'Select all',
|
||||
'files.selector.deselect_all': 'Deselect all',
|
||||
'files.selector.file_selected': 'File',
|
||||
'files.selector.files_selected': 'Files',
|
||||
'files.selector.selected_suffix': 'selected',
|
||||
'files.selector.upload_new': 'Upload new file',
|
||||
'files.selector.loading': 'Loading files...',
|
||||
'files.selector.error_loading': 'Error loading files:',
|
||||
'files.upload.title': 'Upload file',
|
||||
'files.upload.drop_here': 'Drop file here...',
|
||||
'files.upload.uploading': 'Uploading...',
|
||||
'files.upload.drag_files': 'Drag files here',
|
||||
'files.upload.or': 'or',
|
||||
'files.upload.browse': 'Browse',
|
||||
'files.upload.selected_file': 'Selected file:',
|
||||
'files.upload.upload_button': 'Upload',
|
||||
'files.upload.uploading_button': 'Uploading...',
|
||||
'files.upload.success': 'File uploaded successfully!',
|
||||
'files.upload.error': 'An error occurred while uploading.',
|
||||
'files.upload.unexpected_error': 'An unexpected error occurred while uploading.',
|
||||
|
||||
// Files Page
|
||||
'files.page.tab.all': 'All Files',
|
||||
'files.page.tab.uploads': 'My Uploads',
|
||||
'files.page.tab.created': 'Created Files',
|
||||
'files.page.tab.shared': 'Shared Files',
|
||||
'files.page.add_file': 'Add File',
|
||||
'files.page.loading': 'Loading files...',
|
||||
'files.page.error': 'Error:',
|
||||
},
|
||||
fr: {
|
||||
// Navigation
|
||||
'nav.dashboard': 'Centre d\'activité',
|
||||
'nav.files': 'Fichiers',
|
||||
'nav.team': 'Espace équipe',
|
||||
'nav.settings': 'Paramètres',
|
||||
|
||||
// Settings page
|
||||
'settings.title': 'Paramètres',
|
||||
'settings.appearance': 'Apparence',
|
||||
'settings.language': 'Langue',
|
||||
'settings.about': 'À propos',
|
||||
'settings.version': 'Version',
|
||||
'settings.theme': 'Thème',
|
||||
'settings.theme.description': 'Basculer entre le mode clair et sombre',
|
||||
'settings.language.description': 'Choisissez votre langue préférée',
|
||||
'settings.theme.light': 'Clair',
|
||||
'settings.theme.dark': 'Sombre',
|
||||
'settings.theme.toggle.light': 'Passer en mode clair',
|
||||
'settings.theme.toggle.dark': 'Passer en mode sombre',
|
||||
|
||||
// Languages
|
||||
'language.german': 'Deutsch',
|
||||
'language.english': 'English',
|
||||
'language.french': 'Français',
|
||||
|
||||
// Common
|
||||
'common.loading': 'Chargement...',
|
||||
'common.error': 'Erreur',
|
||||
'common.success': 'Succès',
|
||||
'common.cancel': 'Annuler',
|
||||
'common.save': 'Enregistrer',
|
||||
'common.delete': 'Supprimer',
|
||||
'common.edit': 'Modifier',
|
||||
'common.close': 'Fermer',
|
||||
|
||||
// Auth
|
||||
'auth.login': 'Se connecter',
|
||||
'auth.register': 'S\'inscrire',
|
||||
'auth.logout': 'Se déconnecter',
|
||||
'auth.email': 'E-mail',
|
||||
'auth.password': 'Mot de passe',
|
||||
|
||||
// Dashboard
|
||||
'dashboard.prompt.template': 'Modèle de prompt',
|
||||
'dashboard.prompt.settings': 'Paramètres',
|
||||
'dashboard.chat.area': 'Zone de chat',
|
||||
'dashboard.chat.history': 'Historique des workflows',
|
||||
'dashboard.log.title': 'Journal',
|
||||
'dashboard.log.workflow': 'Workflow',
|
||||
'dashboard.log.no_workflow': 'Aucun workflow sélectionné',
|
||||
'dashboard.log.loading': 'Chargement des logs...',
|
||||
'dashboard.log.error': 'Erreur lors du chargement des logs',
|
||||
'dashboard.log.no_logs': 'Aucun log disponible pour ce workflow',
|
||||
'dashboard.log.waiting': 'Workflow en cours... En attente des logs...',
|
||||
'dashboard.log.fetch_failed': 'Échec du chargement des logs',
|
||||
'dashboard.log.level.info': 'INFO',
|
||||
|
||||
// Prompt Set
|
||||
'promptset.loading': 'Chargement des prompts...',
|
||||
'promptset.error.loading': 'Erreur lors du chargement des prompts',
|
||||
'promptset.retry': 'Réessayer',
|
||||
'promptset.new_prompt': 'Nouveau prompt',
|
||||
'promptset.prompt_count': 'Prompt',
|
||||
'promptset.prompt_count_plural': 'Prompts',
|
||||
'promptset.no_prompts': 'Aucun prompt disponible',
|
||||
'promptset.created': 'Créé',
|
||||
'promptset.run_tooltip': 'Exécuter le prompt',
|
||||
'promptset.share_tooltip': 'Partager le prompt',
|
||||
'promptset.delete_tooltip': 'Supprimer le prompt',
|
||||
'promptset.confirm_delete': 'Cliquez à nouveau pour confirmer',
|
||||
'promptset.deleting': 'Suppression...',
|
||||
'promptset.confirm_click': 'Cliquez pour confirmer',
|
||||
'promptset.delete_error': 'Erreur lors de la suppression',
|
||||
'promptset.deleting_message': 'Suppression du prompt...',
|
||||
|
||||
// Prompt Modal
|
||||
'modal.create_prompt': 'Créer un nouveau prompt',
|
||||
'modal.name_required': 'Le nom est requis',
|
||||
'modal.content_required': 'Le contenu est requis',
|
||||
'modal.create_error': 'Erreur lors de la création du prompt',
|
||||
'modal.name_label': 'Nom',
|
||||
'modal.content_label': 'Contenu',
|
||||
'modal.name_placeholder': 'Entrez un nom pour le prompt',
|
||||
'modal.content_placeholder': 'Entrez le contenu du prompt',
|
||||
'modal.cancel': 'Annuler',
|
||||
'modal.creating': 'Création...',
|
||||
'modal.create': 'Créer le prompt',
|
||||
|
||||
// Share Modal
|
||||
'share_modal.title': 'Partager le prompt',
|
||||
'share_modal.select_users': 'Sélectionner les utilisateurs',
|
||||
'share_modal.select_all': 'Tout sélectionner',
|
||||
'share_modal.deselect_all': 'Tout désélectionner',
|
||||
'share_modal.loading_users': 'Chargement des utilisateurs...',
|
||||
'share_modal.error_loading_users': 'Erreur lors du chargement des utilisateurs',
|
||||
'share_modal.no_users_available': 'Aucun utilisateur disponible',
|
||||
'share_modal.no_users_selected': 'Veuillez sélectionner au moins un utilisateur',
|
||||
'share_modal.one_user_selected': '1 utilisateur sélectionné',
|
||||
'share_modal.multiple_users_selected': '{count} utilisateurs sélectionnés',
|
||||
'share_modal.custom_title': 'Titre personnalisé (facultatif)',
|
||||
'share_modal.title_placeholder': 'Entrez un titre personnalisé',
|
||||
'share_modal.message': 'Message (facultatif)',
|
||||
'share_modal.message_placeholder': 'Ajoutez un message pour les destinataires',
|
||||
'share_modal.share': 'Partager',
|
||||
'share_modal.sharing': 'Partage en cours...',
|
||||
'share_modal.share_error': 'Erreur lors du partage du prompt',
|
||||
|
||||
// Prompt Settings
|
||||
'prompt_settings.title': 'Paramètres de prompt',
|
||||
'prompt_settings.content_placeholder': 'Le contenu des paramètres sera ajouté dans les futures mises à jour.',
|
||||
|
||||
// Chat Area
|
||||
'chat.continue_conversation': 'Continuer la conversation...',
|
||||
'chat.enter_message': 'Entrez votre message...',
|
||||
'chat.remove_file': 'Supprimer le fichier',
|
||||
'chat.attach_file': 'Joindre un fichier',
|
||||
'chat.you': 'Vous',
|
||||
'chat.click_to_open': 'Cliquez pour ouvrir',
|
||||
'chat.preview_document': 'Aperçu du document',
|
||||
'chat.download_document': 'Télécharger le document',
|
||||
'chat.workflow_failed': 'Échec du workflow.',
|
||||
'chat.retry_workflow': 'Réessayer',
|
||||
'chat.sending_followup': 'Envoi du message de suivi...',
|
||||
'chat.sending_message': 'Envoi du message...',
|
||||
'chat.error_prefix': 'Erreur:',
|
||||
'chat.error_loading_messages': 'Erreur lors du chargement des messages:',
|
||||
'chat.loading_workflow_messages': 'Chargement des messages de workflow...',
|
||||
'chat.start_conversation': 'Commencez une conversation en entrant un message, en sélectionnant un modèle ou en continuant un workflow précédent...',
|
||||
|
||||
// File Preview
|
||||
'file_preview.loading': 'Chargement de l\'aperçu...',
|
||||
'file_preview.error': 'Erreur',
|
||||
'file_preview.no_preview': 'Aucun aperçu disponible',
|
||||
'file_preview.close_preview': 'Fermer l\'aperçu',
|
||||
'file_preview.python': 'Python',
|
||||
|
||||
// Chat History
|
||||
'chat_history.loading': 'Chargement des workflows...',
|
||||
'chat_history.error_loading': 'Erreur lors du chargement des workflows:',
|
||||
'chat_history.try_again': 'Réessayer',
|
||||
'chat_history.title': 'Historique des workflows',
|
||||
'chat_history.workflow_count': 'Workflow',
|
||||
'chat_history.workflow_count_plural': 'Workflows',
|
||||
'chat_history.empty_state': 'Aucun workflow disponible',
|
||||
'chat_history.confirm_delete': 'Êtes-vous sûr de vouloir supprimer le workflow "{id}..."?',
|
||||
'chat_history.no_message_content': 'Aucun contenu de message disponible',
|
||||
'chat_history.unknown_date': 'Date inconnue',
|
||||
'chat_history.invalid_date': 'Date invalide',
|
||||
'chat_history.started': 'Démarré:',
|
||||
'chat_history.last_activity': 'Dernière activité:',
|
||||
'chat_history.round': 'Tour',
|
||||
'chat_history.resume_tooltip': 'Reprendre le workflow',
|
||||
'chat_history.delete_tooltip': 'Supprimer le workflow',
|
||||
'chat_history.deleting': 'Suppression du workflow...',
|
||||
|
||||
// Workflow Status
|
||||
'status.error': 'ERREUR',
|
||||
'status.failed': 'ÉCHEC',
|
||||
'status.stopped': 'ARRÊTÉ',
|
||||
'status.cancelled': 'ANNULÉ',
|
||||
'status.running': 'EN COURS',
|
||||
'status.processing': 'TRAITEMENT',
|
||||
'status.completed': 'TERMINÉ',
|
||||
'status.pending': 'EN ATTENTE',
|
||||
|
||||
// Files
|
||||
'files.unknown_size': 'Taille inconnue',
|
||||
'files.unknown_date': 'Date inconnue',
|
||||
'files.source.uploaded': 'Téléchargé',
|
||||
'files.source.ai_created': 'Créé par IA',
|
||||
'files.source.shared': 'Partagé',
|
||||
'files.source.unknown': 'Inconnu',
|
||||
'files.preview_tooltip': 'Aperçu du fichier',
|
||||
'files.download_tooltip': 'Télécharger le fichier',
|
||||
'files.delete_tooltip': 'Supprimer le fichier',
|
||||
'files.delete_confirm_tooltip': 'Cliquez à nouveau pour confirmer la suppression',
|
||||
'files.downloading': 'Téléchargement...',
|
||||
'files.deleting': 'Suppression...',
|
||||
'files.delete_confirm': 'Cliquez pour confirmer...',
|
||||
'files.no_files': 'Aucun fichier trouvé.',
|
||||
'files.no_shared_files': 'Aucun fichier partagé trouvé.',
|
||||
'files.no_ai_files': 'Aucun fichier créé par IA trouvé.',
|
||||
'files.no_uploaded_files': 'Aucun fichier téléchargé trouvé.',
|
||||
'files.header.name': 'Nom',
|
||||
'files.header.type': 'Type',
|
||||
'files.header.size': 'Taille',
|
||||
'files.header.date': 'Date',
|
||||
'files.selector.title': 'Sélectionner des fichiers',
|
||||
'files.selector.tab.all': 'Tous les fichiers',
|
||||
'files.selector.tab.uploads': 'Téléchargés',
|
||||
'files.selector.tab.created': 'Créés par IA',
|
||||
'files.selector.tab.shared': 'Partagés',
|
||||
'files.selector.select_all': 'Tout sélectionner',
|
||||
'files.selector.deselect_all': 'Tout désélectionner',
|
||||
'files.selector.file_selected': 'Fichier',
|
||||
'files.selector.files_selected': 'Fichiers',
|
||||
'files.selector.selected_suffix': 'sélectionné(s)',
|
||||
'files.selector.upload_new': 'Télécharger un nouveau fichier',
|
||||
'files.selector.loading': 'Chargement des fichiers...',
|
||||
'files.selector.error_loading': 'Erreur lors du chargement des fichiers:',
|
||||
'files.upload.title': 'Télécharger un fichier',
|
||||
'files.upload.drop_here': 'Déposer le fichier ici...',
|
||||
'files.upload.uploading': 'Téléchargement...',
|
||||
'files.upload.drag_files': 'Glisser les fichiers ici',
|
||||
'files.upload.or': 'ou',
|
||||
'files.upload.browse': 'Parcourir',
|
||||
'files.upload.selected_file': 'Fichier sélectionné:',
|
||||
'files.upload.upload_button': 'Télécharger',
|
||||
'files.upload.uploading_button': 'Téléchargement...',
|
||||
'files.upload.success': 'Fichier téléchargé avec succès!',
|
||||
'files.upload.error': 'Une erreur s\'est produite lors du téléchargement.',
|
||||
'files.upload.unexpected_error': 'Une erreur inattendue s\'est produite lors du téléchargement.',
|
||||
|
||||
// Files Page
|
||||
'files.page.tab.all': 'Tous les fichiers',
|
||||
'files.page.tab.uploads': 'Mes téléchargements',
|
||||
'files.page.tab.created': 'Fichiers créés',
|
||||
'files.page.tab.shared': 'Fichiers partagés',
|
||||
'files.page.add_file': 'Ajouter un fichier',
|
||||
'files.page.loading': 'Chargement des fichiers...',
|
||||
'files.page.error': 'Erreur:',
|
||||
}
|
||||
};
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
import { useActor } from '@xstate/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { sidebarMachine, getSidebarState } from '../../machines/sidebarMachine';
|
||||
|
||||
// Custom hook that provides all sidebar functionality
|
||||
export const useSidebarMachine = () => {
|
||||
// Step 1: Create the machine actor (this is like useState but for state machines)
|
||||
const [state, send] = useActor(sidebarMachine);
|
||||
|
||||
// Step 2: Get the current route from React Router
|
||||
const location = useLocation();
|
||||
|
||||
// Step 3: Update the machine when the route changes
|
||||
useEffect(() => {
|
||||
send({
|
||||
type: 'NAVIGATE',
|
||||
path: location.pathname,
|
||||
});
|
||||
}, [location.pathname, send]);
|
||||
|
||||
// Step 4: Get derived state using our helper function
|
||||
const sidebarState = getSidebarState(state.context);
|
||||
|
||||
// Step 5: Create easy-to-use action functions
|
||||
const actions = {
|
||||
// Toggle a specific menu item
|
||||
toggleItem: (itemId: string) => {
|
||||
send({
|
||||
type: 'TOGGLE_ITEM',
|
||||
itemId,
|
||||
});
|
||||
},
|
||||
|
||||
// Close all submenus
|
||||
closeAll: () => {
|
||||
send({
|
||||
type: 'CLOSE_ALL',
|
||||
});
|
||||
},
|
||||
|
||||
// Check if a specific item is open
|
||||
isItemOpen: (itemId: string) => {
|
||||
return sidebarState.isItemOpen(itemId);
|
||||
},
|
||||
|
||||
// Check if an item is the active route
|
||||
isItemActive: (itemPath?: string) => {
|
||||
if (!itemPath) return false;
|
||||
return location.pathname === itemPath;
|
||||
},
|
||||
|
||||
// Minimize/expand sidebar
|
||||
minimizeSidebar: () => {
|
||||
send({
|
||||
type: 'MINIMIZE_SIDEBAR',
|
||||
});
|
||||
},
|
||||
|
||||
expandSidebar: () => {
|
||||
send({
|
||||
type: 'EXPAND_SIDEBAR',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Step 6: Return everything the components need
|
||||
return {
|
||||
// State queries (read-only)
|
||||
hasOpenSubmenu: sidebarState.hasOpenSubmenu,
|
||||
openItemId: sidebarState.openItemId,
|
||||
activePath: sidebarState.activePath,
|
||||
isMinimized: sidebarState.isMinimized,
|
||||
currentState: state.value, // For debugging: 'collapsed' or 'expanded'
|
||||
|
||||
// Actions (functions to call)
|
||||
toggleItem: actions.toggleItem,
|
||||
closeAll: actions.closeAll,
|
||||
isItemOpen: actions.isItemOpen,
|
||||
isItemActive: actions.isItemActive,
|
||||
minimizeSidebar: actions.minimizeSidebar,
|
||||
expandSidebar: actions.expandSidebar,
|
||||
|
||||
// Raw state machine state (for debugging)
|
||||
_debugState: state,
|
||||
_debugSend: send,
|
||||
};
|
||||
};
|
||||
|
||||
// Step 7: Type export for components that need it
|
||||
export type UseSidebarMachine = ReturnType<typeof useSidebarMachine>;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import api from '../api';
|
||||
|
||||
// Generic API error handling
|
||||
|
|
@ -26,7 +26,7 @@ export function useApiRequest<RequestData = any, ResponseData = any>() {
|
|||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const request = async ({
|
||||
const request = useCallback(async ({
|
||||
url,
|
||||
method,
|
||||
data,
|
||||
|
|
@ -52,7 +52,7 @@ export function useApiRequest<RequestData = any, ResponseData = any>() {
|
|||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
request,
|
||||
|
|
|
|||
|
|
@ -30,23 +30,43 @@ export function useAuth() {
|
|||
params.append('client_id', '');
|
||||
params.append('client_secret', '');
|
||||
|
||||
// Create a custom axios instance for this request
|
||||
const instance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
withCredentials: true,
|
||||
// Generate a simple CSRF token (in production, this should come from the server)
|
||||
const csrfToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
|
||||
// Use the existing api instance with custom headers for this request
|
||||
const response = await api.post('/api/local/login', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRF-Token': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const response = await instance.post('/api/token', params);
|
||||
|
||||
// Store the entire auth response
|
||||
if (response.data.accessToken) {
|
||||
localStorage.setItem('auth_data', JSON.stringify(response.data));
|
||||
// Normalize the response structure to match what the frontend expects
|
||||
let normalizedAuthData;
|
||||
if (response.data.token_data) {
|
||||
// Backend returns token_data with tokenAccess field, normalize to accessToken
|
||||
normalizedAuthData = {
|
||||
accessToken: response.data.token_data.tokenAccess || response.data.access_token,
|
||||
tokenType: response.data.token_data.tokenType || 'bearer',
|
||||
userId: response.data.token_data.userId,
|
||||
expiresAt: response.data.token_data.expiresAt,
|
||||
createdAt: response.data.token_data.createdAt
|
||||
};
|
||||
} else {
|
||||
// Fallback to old structure if needed
|
||||
normalizedAuthData = {
|
||||
accessToken: response.data.access_token,
|
||||
tokenType: response.data.token_type || 'bearer'
|
||||
};
|
||||
}
|
||||
|
||||
return response.data;
|
||||
// Store the normalized auth response
|
||||
localStorage.setItem('auth_data', JSON.stringify(normalizedAuthData));
|
||||
|
||||
return {
|
||||
accessToken: normalizedAuthData.accessToken,
|
||||
tokenType: normalizedAuthData.tokenType
|
||||
};
|
||||
} catch (error: any) {
|
||||
let errorMessage = 'An error occurred during login';
|
||||
|
||||
|
|
@ -85,8 +105,6 @@ interface MsalAuthResponse {
|
|||
}
|
||||
|
||||
export function useMsalAuth() {
|
||||
const { instance, accounts } = useMsal();
|
||||
const { request, isLoading, error } = useApiRequest<null, MsalAuthResponse>();
|
||||
const [msalError, setMsalError] = useState<string | null>(null);
|
||||
const [isMsalLoading, setIsMsalLoading] = useState(false);
|
||||
|
||||
|
|
@ -95,75 +113,93 @@ export function useMsalAuth() {
|
|||
setMsalError(null);
|
||||
|
||||
try {
|
||||
let msalToken;
|
||||
return new Promise((resolve, reject) => {
|
||||
// Open popup to backend Microsoft login route
|
||||
const popup = window.open(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/api/msft/login?state=login`,
|
||||
'msft-login',
|
||||
'width=500,height=600,scrollbars=yes,resizable=yes'
|
||||
);
|
||||
|
||||
// If we have an account, try to get the token silently
|
||||
if (accounts.length > 0) {
|
||||
const silentRequest = {
|
||||
scopes: ['user.read'],
|
||||
account: accounts[0]
|
||||
if (!popup) {
|
||||
setMsalError('Popup was blocked. Please allow popups and try again.');
|
||||
setIsMsalLoading(false);
|
||||
reject(new Error('Popup was blocked'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for messages from the popup
|
||||
const messageListener = (event: MessageEvent) => {
|
||||
// Verify origin for security
|
||||
const apiUrl = new URL(import.meta.env.VITE_API_BASE_URL);
|
||||
if (event.origin !== apiUrl.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.type === 'msft_auth_success') {
|
||||
// Store the auth data with normalized field names
|
||||
if (event.data.token_data) {
|
||||
const normalizedTokenData = {
|
||||
accessToken: event.data.token_data.tokenAccess, // Convert tokenAccess to accessToken
|
||||
tokenType: event.data.token_data.tokenType,
|
||||
userId: event.data.token_data.userId,
|
||||
expiresAt: event.data.token_data.expiresAt,
|
||||
createdAt: event.data.token_data.createdAt
|
||||
};
|
||||
localStorage.setItem('auth_data', JSON.stringify(normalizedTokenData));
|
||||
}
|
||||
|
||||
// Clean up
|
||||
window.removeEventListener('message', messageListener);
|
||||
popup.close();
|
||||
setIsMsalLoading(false);
|
||||
|
||||
// Resolve with the token data
|
||||
resolve({
|
||||
accessToken: event.data.token_data.tokenAccess,
|
||||
tokenType: event.data.token_data.tokenType || 'bearer',
|
||||
user: {
|
||||
username: '', // Will be populated by the backend
|
||||
email: '',
|
||||
fullName: '',
|
||||
mandateId: 0
|
||||
}
|
||||
});
|
||||
} else if (event.data.type === 'msft_connection_error') {
|
||||
// Handle error
|
||||
window.removeEventListener('message', messageListener);
|
||||
popup.close();
|
||||
setIsMsalLoading(false);
|
||||
setMsalError(event.data.error || 'Microsoft authentication failed');
|
||||
reject(new Error(event.data.error || 'Microsoft authentication failed'));
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await instance.acquireTokenSilent(silentRequest);
|
||||
msalToken = response.accessToken;
|
||||
} catch (e) {
|
||||
// If silent token acquisition fails, fall back to popup
|
||||
const response = await instance.acquireTokenPopup(silentRequest);
|
||||
msalToken = response.accessToken;
|
||||
}
|
||||
} else {
|
||||
// No account, do popup login
|
||||
const response = await instance.loginPopup({
|
||||
scopes: ['user.read']
|
||||
});
|
||||
// Add message listener
|
||||
window.addEventListener('message', messageListener);
|
||||
|
||||
if (response.account) {
|
||||
const tokenResponse = await instance.acquireTokenSilent({
|
||||
scopes: ['user.read'],
|
||||
account: response.account
|
||||
});
|
||||
msalToken = tokenResponse.accessToken;
|
||||
} else {
|
||||
throw new Error('Failed to get account after login');
|
||||
}
|
||||
}
|
||||
|
||||
// Exchange MSAL token for backend token
|
||||
const response = await api.post('/api/msft/token', null, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${msalToken}`
|
||||
}
|
||||
});
|
||||
|
||||
// Store the backend token
|
||||
if (response.data.accessToken) {
|
||||
localStorage.setItem('auth_data', JSON.stringify(response.data));
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
let errorMessage = 'MSAL Login fehlgeschlagen';
|
||||
|
||||
if (error.response) {
|
||||
errorMessage = error.response.data?.detail || error.response.data?.message || errorMessage;
|
||||
} else if (error.request) {
|
||||
errorMessage = 'Keine Antwort vom Server erhalten';
|
||||
} else {
|
||||
errorMessage = error.message || errorMessage;
|
||||
}
|
||||
|
||||
setMsalError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
// Handle popup closing without completing auth
|
||||
const checkClosed = setInterval(() => {
|
||||
if (popup.closed) {
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageListener);
|
||||
setIsMsalLoading(false);
|
||||
setMsalError('Authentication was cancelled');
|
||||
reject(new Error('Authentication was cancelled'));
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
} catch (error: any) {
|
||||
setMsalError(error.message || 'Microsoft authentication failed');
|
||||
setIsMsalLoading(false);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loginWithMsal,
|
||||
error: msalError || error,
|
||||
isLoading: isMsalLoading || isLoading
|
||||
error: msalError,
|
||||
isLoading: isMsalLoading
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -174,47 +210,83 @@ interface RegisterData {
|
|||
email: string;
|
||||
fullName: string;
|
||||
language?: string;
|
||||
enabled?: boolean;
|
||||
privilege?: string;
|
||||
}
|
||||
|
||||
interface RegisterResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
user?: {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
language: string;
|
||||
enabled: boolean;
|
||||
privilege: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function useRegister() {
|
||||
const { request, isLoading, error } = useApiRequest<RegisterData, any>();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const register = async (userData: RegisterData): Promise<RegisterResponse> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Add default language if not provided
|
||||
// Prepare data to match backend expectations
|
||||
// Backend expects userData as object and password as embedded field
|
||||
const dataToSend = {
|
||||
...userData,
|
||||
language: userData.language || 'de'
|
||||
userData: {
|
||||
username: userData.username,
|
||||
email: userData.email,
|
||||
fullName: userData.fullName,
|
||||
language: userData.language || 'de',
|
||||
enabled: userData.enabled !== undefined ? userData.enabled : true,
|
||||
privilege: userData.privilege || 'user'
|
||||
},
|
||||
password: userData.password
|
||||
};
|
||||
|
||||
const response = await request({
|
||||
url: '/api/users/register',
|
||||
method: 'post',
|
||||
data: dataToSend,
|
||||
additionalConfig: {
|
||||
const response = await api.post('/api/local/register', dataToSend, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Registration successful',
|
||||
user: response
|
||||
user: response.data
|
||||
};
|
||||
} catch (error: any) {
|
||||
let errorMessage = 'An error occurred during registration';
|
||||
|
||||
if (error.response) {
|
||||
// Handle validation errors from FastAPI
|
||||
if (error.response.data?.detail) {
|
||||
if (Array.isArray(error.response.data.detail)) {
|
||||
// Handle FastAPI validation errors array
|
||||
errorMessage = error.response.data.detail.map((err: any) => err.msg).join(', ');
|
||||
} else {
|
||||
errorMessage = error.response.data.detail;
|
||||
}
|
||||
} else {
|
||||
errorMessage = 'Registration failed';
|
||||
}
|
||||
} else if (error.request) {
|
||||
errorMessage = 'No response received from server';
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -262,7 +334,7 @@ export function useMsalRegister() {
|
|||
|
||||
// Register the user through our backend
|
||||
const response = await request({
|
||||
url: '/api/users/register-with-msal',
|
||||
url: '/api/msft/register',
|
||||
method: 'post',
|
||||
data: userData,
|
||||
additionalConfig: {
|
||||
|
|
@ -288,3 +360,121 @@ export function useMsalRegister() {
|
|||
isLoading
|
||||
};
|
||||
}
|
||||
|
||||
// Username availability check
|
||||
export function useUsernameAvailability() {
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const checkAvailability = async (username: string, authenticationAuthority: string = 'local'): Promise<{
|
||||
username: string;
|
||||
authenticationAuthority: string;
|
||||
available: boolean;
|
||||
message: string;
|
||||
}> => {
|
||||
setIsChecking(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await api.get('/api/local/available', {
|
||||
params: {
|
||||
username,
|
||||
authenticationAuthority
|
||||
}
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
let errorMessage = 'Failed to check username availability';
|
||||
|
||||
if (error.response) {
|
||||
errorMessage = error.response.data?.detail || errorMessage;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
checkAvailability,
|
||||
isChecking,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
// Logout function
|
||||
export function useLogout() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const logout = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await api.post('/api/local/logout');
|
||||
|
||||
// Clear local storage
|
||||
localStorage.removeItem('auth_data');
|
||||
|
||||
// Redirect to login page
|
||||
window.location.href = '/login';
|
||||
} catch (error: any) {
|
||||
let errorMessage = 'Logout failed';
|
||||
|
||||
if (error.response) {
|
||||
errorMessage = error.response.data?.detail || errorMessage;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
logout,
|
||||
isLoading,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
// Get current user
|
||||
export function useCurrentUser() {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const getCurrentUser = async (): Promise<any> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await api.get('/api/local/me');
|
||||
setUser(response.data);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
let errorMessage = 'Failed to get current user';
|
||||
|
||||
if (error.response) {
|
||||
errorMessage = error.response.data?.detail || errorMessage;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
user,
|
||||
getCurrentUser,
|
||||
isLoading,
|
||||
error
|
||||
};
|
||||
}
|
||||
336
src/hooks/useConnections.ts
Normal file
336
src/hooks/useConnections.ts
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useApiRequest } from './useApi';
|
||||
|
||||
// Connection interfaces based on backend UserConnection model
|
||||
export interface Connection {
|
||||
id: string;
|
||||
userId: string;
|
||||
authority: 'local' | 'google' | 'msft';
|
||||
externalId: string;
|
||||
externalUsername: string;
|
||||
externalEmail?: string;
|
||||
status: 'active' | 'expired' | 'revoked' | 'pending';
|
||||
connectedAt: string;
|
||||
lastChecked: string;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateConnectionData {
|
||||
id?: string;
|
||||
userId?: string;
|
||||
authority?: 'msft' | 'google'; // Keep for compatibility with existing code
|
||||
type?: 'msft' | 'google'; // Backend expects this field
|
||||
externalId?: string;
|
||||
externalUsername?: string;
|
||||
externalEmail?: string;
|
||||
status?: 'active' | 'expired' | 'revoked' | 'pending';
|
||||
connectedAt?: string;
|
||||
lastChecked?: string;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
export interface ConnectResponse {
|
||||
authUrl: string;
|
||||
}
|
||||
|
||||
// Hook for managing connections
|
||||
export function useConnections() {
|
||||
const [connections, setConnections] = useState<Connection[]>([]);
|
||||
const { request, isLoading, error } = useApiRequest<any, any>();
|
||||
|
||||
// Fetch all connections
|
||||
const fetchConnections = async (): Promise<Connection[]> => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: '/api/connections/',
|
||||
method: 'get'
|
||||
});
|
||||
setConnections(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching connections:', error);
|
||||
setConnections([]);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new connection
|
||||
const createConnection = async (connectionData: CreateConnectionData): Promise<Connection> => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: '/api/connections/',
|
||||
method: 'post',
|
||||
data: connectionData
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setConnections(prev => {
|
||||
const existing = prev.find(conn => conn.id === data.id);
|
||||
if (existing) {
|
||||
return prev.map(conn => conn.id === data.id ? data : conn);
|
||||
} else {
|
||||
return [...prev, data];
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error creating connection:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to a service (initiate OAuth)
|
||||
const connectService = async (connectionId: string): Promise<ConnectResponse> => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: `/api/connections/${connectionId}/connect`,
|
||||
method: 'post'
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error connecting service:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Disconnect from a service
|
||||
const disconnectService = async (connectionId: string): Promise<{ message: string }> => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: `/api/connections/${connectionId}/disconnect`,
|
||||
method: 'post'
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setConnections(prev =>
|
||||
prev.map(conn =>
|
||||
conn.id === connectionId
|
||||
? { ...conn, status: 'inactive' as any, lastChecked: new Date().toISOString() }
|
||||
: conn
|
||||
)
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error disconnecting service:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Delete a connection
|
||||
const deleteConnection = async (connectionId: string): Promise<{ message: string }> => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: `/api/connections/${connectionId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setConnections(prev => prev.filter(conn => conn.id !== connectionId));
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error deleting connection:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Update a connection
|
||||
const updateConnection = async (connectionId: string, updateData: Partial<Connection>): Promise<Connection> => {
|
||||
try {
|
||||
// Use PUT endpoint for updating connections
|
||||
const data = await request({
|
||||
url: `/api/connections/${connectionId}`,
|
||||
method: 'put',
|
||||
data: updateData
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setConnections(prev =>
|
||||
prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn)
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error updating connection:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
connections,
|
||||
fetchConnections,
|
||||
createConnection,
|
||||
updateConnection,
|
||||
connectService,
|
||||
disconnectService,
|
||||
deleteConnection,
|
||||
isLoading,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
// Hook for OAuth connection popup flow (similar to useMsalAuth)
|
||||
export function useOAuthConnect() {
|
||||
const { connectService, fetchConnections } = useConnections();
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [connectError, setConnectError] = useState<string | null>(null);
|
||||
|
||||
const connectWithPopup = async (connectionId: string): Promise<void> => {
|
||||
setIsConnecting(true);
|
||||
setConnectError(null);
|
||||
|
||||
try {
|
||||
// Get the OAuth URL from backend
|
||||
const response = await connectService(connectionId);
|
||||
if (!response.authUrl) {
|
||||
throw new Error('No OAuth URL received from backend');
|
||||
}
|
||||
|
||||
console.log('OAuth URL from backend:', response.authUrl);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Convert relative URL to absolute URL if needed
|
||||
let authUrl = response.authUrl;
|
||||
if (authUrl.startsWith('/')) {
|
||||
authUrl = `${import.meta.env.VITE_API_BASE_URL}${authUrl}`;
|
||||
}
|
||||
|
||||
// Open popup using the same pattern as useAuthentication.ts
|
||||
const popup = window.open(
|
||||
authUrl,
|
||||
'oauth-connect',
|
||||
'width=500,height=600,scrollbars=yes,resizable=yes'
|
||||
);
|
||||
|
||||
if (!popup) {
|
||||
setConnectError('Popup was blocked. Please allow popups and try again.');
|
||||
setIsConnecting(false);
|
||||
reject(new Error('Popup was blocked'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle popup closing without completing auth
|
||||
const checkClosed = setInterval(() => {
|
||||
if (popup.closed) {
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageListener);
|
||||
setIsConnecting(false);
|
||||
console.log('OAuth popup closed');
|
||||
// Refresh connections anyway in case it succeeded
|
||||
fetchConnections();
|
||||
setConnectError('Authentication was cancelled');
|
||||
reject(new Error('Authentication was cancelled'));
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Listen for messages from the popup (similar to useMsalAuth)
|
||||
const messageListener = (event: MessageEvent) => {
|
||||
// Verify origin for security
|
||||
const apiUrl = new URL(import.meta.env.VITE_API_BASE_URL);
|
||||
if (event.origin !== apiUrl.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.type === 'msft_connection_success' || event.data.type === 'google_connection_success') {
|
||||
// Clean up - IMPORTANT: clear the checkClosed interval first
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageListener);
|
||||
popup.close();
|
||||
setIsConnecting(false);
|
||||
console.log('OAuth connection successful');
|
||||
// Refresh connections
|
||||
fetchConnections();
|
||||
resolve();
|
||||
} else if (event.data.type === 'msft_connection_error' || event.data.type === 'google_connection_error') {
|
||||
// Handle error - also clear the checkClosed interval
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageListener);
|
||||
popup.close();
|
||||
setIsConnecting(false);
|
||||
setConnectError(event.data.error || 'OAuth connection failed');
|
||||
reject(new Error(event.data.error || 'OAuth connection failed'));
|
||||
}
|
||||
};
|
||||
|
||||
// Add message listener
|
||||
window.addEventListener('message', messageListener);
|
||||
});
|
||||
} catch (error: any) {
|
||||
setConnectError(error.message || 'OAuth connection failed');
|
||||
setIsConnecting(false);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
connectWithPopup,
|
||||
isConnecting,
|
||||
error: connectError
|
||||
};
|
||||
}
|
||||
|
||||
// Hook for disconnecting services
|
||||
export function useDisconnect() {
|
||||
const { disconnectService, fetchConnections } = useConnections();
|
||||
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||
const [disconnectError, setDisconnectError] = useState<string | null>(null);
|
||||
|
||||
const disconnect = async (connectionId: string): Promise<void> => {
|
||||
setIsDisconnecting(true);
|
||||
setDisconnectError(null);
|
||||
|
||||
try {
|
||||
await disconnectService(connectionId);
|
||||
console.log('Service disconnected successfully');
|
||||
// Refresh connections to update the status
|
||||
await fetchConnections();
|
||||
} catch (error: any) {
|
||||
setDisconnectError(error.message || 'Disconnect failed');
|
||||
console.error('Error disconnecting service:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsDisconnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
disconnect,
|
||||
isDisconnecting,
|
||||
error: disconnectError
|
||||
};
|
||||
}
|
||||
|
||||
// Hook for individual connection operations
|
||||
export function useConnection(connectionId?: string) {
|
||||
const [connection, setConnection] = useState<Connection | null>(null);
|
||||
const { request, isLoading, error } = useApiRequest<any, Connection[]>();
|
||||
|
||||
const fetchConnection = async (id: string = connectionId!): Promise<Connection | null> => {
|
||||
if (!id) return null;
|
||||
|
||||
try {
|
||||
// Since there's no individual connection endpoint, fetch all and filter
|
||||
const data: Connection[] = await request({
|
||||
url: '/api/connections/',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
const foundConnection = data.find((conn: Connection) => conn.id === id);
|
||||
setConnection(foundConnection || null);
|
||||
return foundConnection || null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching connection:', error);
|
||||
setConnection(null);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
connection,
|
||||
fetchConnection,
|
||||
isLoading,
|
||||
error
|
||||
};
|
||||
}
|
||||
277
src/hooks/useSharePointTest.ts
Normal file
277
src/hooks/useSharePointTest.ts
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import { useState } from 'react';
|
||||
import { useApiRequest } from './useApi';
|
||||
|
||||
// SharePoint interfaces
|
||||
export interface SharePointConnection {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status: string;
|
||||
authority: string;
|
||||
lastChecked: string | null;
|
||||
expiresAt: string | null;
|
||||
externalUsername: string;
|
||||
externalEmail: string;
|
||||
}
|
||||
|
||||
export interface SharePointDocument {
|
||||
documentName: string;
|
||||
documentData: any;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export interface SharePointResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
documents?: SharePointDocument[];
|
||||
};
|
||||
error?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface SharePointListRequest {
|
||||
connectionReference: string;
|
||||
siteUrl: string;
|
||||
folderPaths: string[];
|
||||
includeSubfolders?: boolean;
|
||||
expectedDocumentFormats?: Array<{
|
||||
extension: string;
|
||||
mimeType: string;
|
||||
description?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SharePointFindRequest {
|
||||
connectionReference: string;
|
||||
siteUrl: string;
|
||||
query: string;
|
||||
searchScope?: string;
|
||||
expectedDocumentFormats?: Array<{
|
||||
extension: string;
|
||||
mimeType: string;
|
||||
description?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SharePointReadRequest {
|
||||
documentList: string;
|
||||
connectionReference: string;
|
||||
siteUrl: string;
|
||||
documentPaths: string[];
|
||||
includeMetadata?: boolean;
|
||||
expectedDocumentFormats?: Array<{
|
||||
extension: string;
|
||||
mimeType: string;
|
||||
description?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SharePointUploadRequest {
|
||||
connectionReference: string;
|
||||
siteUrl: string;
|
||||
documentPaths: string[];
|
||||
documentList: string;
|
||||
fileNames: string[];
|
||||
expectedDocumentFormats?: Array<{
|
||||
extension: string;
|
||||
mimeType: string;
|
||||
description?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Hook for SharePoint testing operations
|
||||
export function useSharePointTest() {
|
||||
const { request, isLoading, error } = useApiRequest<any, any>();
|
||||
const [lastResponse, setLastResponse] = useState<SharePointResponse | null>(null);
|
||||
|
||||
// Get user's Microsoft connections
|
||||
const getConnections = async (): Promise<SharePointConnection[]> => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/test-sharepoint/connections',
|
||||
method: 'get'
|
||||
});
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching SharePoint connections:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Test a specific connection
|
||||
const testConnection = async (connectionId: string): Promise<any> => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: `/api/test-sharepoint/test-connection/${connectionId}`,
|
||||
method: 'get'
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error testing connection:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// List documents in SharePoint folders
|
||||
const listDocuments = async (data: SharePointListRequest): Promise<SharePointResponse> => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/test-sharepoint/list-documents',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
setLastResponse(response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error listing SharePoint documents:', error);
|
||||
const errorResponse: SharePointResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: 'Failed to list documents'
|
||||
};
|
||||
setLastResponse(errorResponse);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Find documents by query
|
||||
const findDocuments = async (data: SharePointFindRequest): Promise<SharePointResponse> => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/test-sharepoint/find-documents',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
setLastResponse(response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error finding SharePoint documents:', error);
|
||||
const errorResponse: SharePointResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: 'Failed to find documents'
|
||||
};
|
||||
setLastResponse(errorResponse);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Read documents from SharePoint
|
||||
const readDocuments = async (data: SharePointReadRequest): Promise<SharePointResponse> => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/test-sharepoint/read-documents',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
setLastResponse(response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error reading SharePoint documents:', error);
|
||||
const errorResponse: SharePointResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: 'Failed to read documents'
|
||||
};
|
||||
setLastResponse(errorResponse);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Upload documents to SharePoint
|
||||
const uploadDocuments = async (data: SharePointUploadRequest): Promise<SharePointResponse> => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/test-sharepoint/upload-documents',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
setLastResponse(response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error uploading SharePoint documents:', error);
|
||||
const errorResponse: SharePointResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: 'Failed to upload documents'
|
||||
};
|
||||
setLastResponse(errorResponse);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Get example request bodies
|
||||
const getExamples = async (): Promise<any> => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/test-sharepoint/example-requests',
|
||||
method: 'get'
|
||||
});
|
||||
return response.examples || {};
|
||||
} catch (error) {
|
||||
console.error('Error getting example requests:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Debug user's authentication tokens
|
||||
const debugTokens = async (): Promise<any> => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/test-sharepoint/debug-tokens',
|
||||
method: 'get'
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error debugging tokens:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Debug detailed token information
|
||||
const debugTokenDetails = async (): Promise<any> => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/test-sharepoint/debug-token-details',
|
||||
method: 'get'
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error debugging token details:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Discover SharePoint sites
|
||||
const discoverSites = async (): Promise<any> => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/test-sharepoint/discover-sites',
|
||||
method: 'get'
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error discovering SharePoint sites:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// API methods
|
||||
getConnections,
|
||||
testConnection,
|
||||
listDocuments,
|
||||
findDocuments,
|
||||
readDocuments,
|
||||
uploadDocuments,
|
||||
getExamples,
|
||||
debugTokens,
|
||||
debugTokenDetails,
|
||||
discoverSites,
|
||||
|
||||
// State
|
||||
isLoading,
|
||||
error,
|
||||
lastResponse
|
||||
};
|
||||
}
|
||||
|
|
@ -3,12 +3,15 @@ import { useApiRequest } from './useApi';
|
|||
|
||||
// User interfaces
|
||||
export interface User {
|
||||
id: number;
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
language: string;
|
||||
enabled: boolean;
|
||||
privilege: string;
|
||||
mandateId: number;
|
||||
authenticationAuthority: string;
|
||||
mandateId: string;
|
||||
}
|
||||
|
||||
export type UserUpdateData = Partial<Omit<User, 'id' | 'mandateId'>>;
|
||||
|
|
@ -21,7 +24,7 @@ export function useCurrentUser() {
|
|||
const fetchCurrentUser = async () => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: '/api/user/me',
|
||||
url: '/api/local/me',
|
||||
method: 'get'
|
||||
});
|
||||
setUser(data);
|
||||
|
|
@ -30,6 +33,54 @@ export function useCurrentUser() {
|
|||
}
|
||||
};
|
||||
|
||||
const logout = async (msalInstance?: any) => {
|
||||
if (!user) {
|
||||
throw new Error('No user to logout');
|
||||
}
|
||||
|
||||
try {
|
||||
let logoutEndpoint = '/api/local/logout';
|
||||
|
||||
// Determine the correct logout endpoint based on authentication authority
|
||||
if (user.authenticationAuthority === 'msft') {
|
||||
logoutEndpoint = '/api/msft/logout';
|
||||
} else if (user.authenticationAuthority === 'local') {
|
||||
logoutEndpoint = '/api/local/logout';
|
||||
}
|
||||
|
||||
await request({
|
||||
url: logoutEndpoint,
|
||||
method: 'post'
|
||||
});
|
||||
|
||||
// Clear user state after successful logout
|
||||
setUser(null);
|
||||
|
||||
// Clear any local storage data
|
||||
localStorage.clear();
|
||||
|
||||
// Handle MSAL logout for Microsoft authentication
|
||||
if (user.authenticationAuthority === 'msft' && msalInstance) {
|
||||
try {
|
||||
await msalInstance.logoutRedirect({
|
||||
onRedirectNavigate: () => true
|
||||
});
|
||||
return; // MSAL will handle the redirect
|
||||
} catch (msalError) {
|
||||
console.error('MSAL logout failed:', msalError);
|
||||
// Continue with regular redirect if MSAL logout fails
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to login or home page
|
||||
window.location.href = '/login';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentUser();
|
||||
}, []);
|
||||
|
|
@ -38,7 +89,8 @@ export function useCurrentUser() {
|
|||
user,
|
||||
error,
|
||||
isLoading,
|
||||
refetch: fetchCurrentUser
|
||||
refetch: fetchCurrentUser,
|
||||
logout
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
263
src/locales/de.ts
Normal file
263
src/locales/de.ts
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
export default {
|
||||
// Navigation
|
||||
'nav.dashboard': 'Zentrale',
|
||||
'nav.files': 'Dateien',
|
||||
'nav.team': 'Team-Bereich',
|
||||
'nav.connections': 'Verbindungen',
|
||||
'nav.workflows': 'Workflows',
|
||||
'nav.settings': 'Einstellungen',
|
||||
|
||||
// Settings page
|
||||
'settings.title': 'Einstellungen',
|
||||
'settings.appearance': 'Darstellung',
|
||||
'settings.language': 'Sprache',
|
||||
'settings.about': 'Über',
|
||||
'settings.version': 'Version',
|
||||
'settings.theme': 'Theme',
|
||||
'settings.theme.description': 'Wechseln Sie zwischen hellem und dunklem Modus',
|
||||
'settings.language.description': 'Wählen Sie Ihre bevorzugte Sprache',
|
||||
'settings.theme.light': 'Hell',
|
||||
'settings.theme.dark': 'Dunkel',
|
||||
'settings.theme.toggle.light': 'Zu hellem Modus wechseln',
|
||||
'settings.theme.toggle.dark': 'Zu dunklem Modus wechseln',
|
||||
|
||||
// Languages
|
||||
'language.german': 'Deutsch',
|
||||
'language.english': 'English',
|
||||
'language.french': 'Français',
|
||||
|
||||
// Common
|
||||
'common.loading': 'Laden...',
|
||||
'common.error': 'Fehler',
|
||||
'common.success': 'Erfolgreich',
|
||||
'common.cancel': 'Abbrechen',
|
||||
'common.save': 'Speichern',
|
||||
'common.delete': 'Löschen',
|
||||
'common.edit': 'Bearbeiten',
|
||||
'common.close': 'Schließen',
|
||||
|
||||
// Auth
|
||||
'auth.login': 'Anmelden',
|
||||
'auth.register': 'Registrieren',
|
||||
'auth.logout': 'Abmelden',
|
||||
'auth.email': 'E-Mail',
|
||||
'auth.password': 'Passwort',
|
||||
|
||||
// Dashboard
|
||||
'dashboard.prompt.template': 'Prompt Vorlage',
|
||||
'dashboard.prompt.settings': 'Einstellungen',
|
||||
'dashboard.chat.area': 'Chatbereich',
|
||||
'dashboard.chat.history': 'Workflow-Verlauf',
|
||||
'dashboard.log.title': 'Log',
|
||||
'dashboard.log.workflow': 'Workflow',
|
||||
'dashboard.log.no_workflow': 'Kein Workflow ausgewählt',
|
||||
'dashboard.log.loading': 'Logs werden geladen...',
|
||||
'dashboard.log.error': 'Fehler beim Laden der Logs',
|
||||
'dashboard.log.no_logs': 'Keine Logs für diesen Workflow verfügbar',
|
||||
'dashboard.log.waiting': 'Workflow läuft... Warte auf Logs...',
|
||||
'dashboard.log.fetch_failed': 'Logs konnten nicht geladen werden',
|
||||
'dashboard.log.level.info': 'INFO',
|
||||
|
||||
// Prompt Set
|
||||
'promptset.loading': 'Prompts werden geladen...',
|
||||
'promptset.error.loading': 'Fehler beim Laden der Prompts',
|
||||
'promptset.retry': 'Erneut versuchen',
|
||||
'promptset.new_prompt': 'Neuer Prompt',
|
||||
'promptset.prompt_count': 'Prompt',
|
||||
'promptset.prompt_count_plural': 'Prompts',
|
||||
'promptset.no_prompts': 'Keine Prompts verfügbar',
|
||||
'promptset.created': 'Erstellt',
|
||||
'promptset.run_tooltip': 'Prompt ausführen',
|
||||
'promptset.share_tooltip': 'Prompt teilen',
|
||||
'promptset.delete_tooltip': 'Prompt löschen',
|
||||
'promptset.confirm_delete': 'Klicken Sie erneut zum Bestätigen',
|
||||
'promptset.deleting': 'Löschen...',
|
||||
'promptset.confirm_click': 'Zum Bestätigen klicken',
|
||||
'promptset.delete_error': 'Fehler beim Löschen',
|
||||
'promptset.deleting_message': 'Prompt wird gelöscht...',
|
||||
|
||||
// Connections
|
||||
'connections.title': 'Verbindungen',
|
||||
'connections.connect_google': 'Google verbinden',
|
||||
'connections.connect_microsoft': 'Microsoft verbinden',
|
||||
'connections.edit_connection_title': '{authority} Verbindung bearbeiten',
|
||||
'connections.update_connection': 'Verbindung aktualisieren',
|
||||
'connections.service_connections': 'Service-Verbindungen',
|
||||
'connections.error': 'Fehler',
|
||||
'connections.connection_error': 'Verbindungsfehler',
|
||||
'connections.disconnect_error': 'Trennungsfehler',
|
||||
'connections.unknown': 'Unbekannt',
|
||||
'connections.not_available': 'Nicht verfügbar',
|
||||
'connections.invalid_date': 'Ungültiges Datum',
|
||||
'connections.confirm_delete': 'Sind Sie sicher, dass Sie die {service} Verbindung löschen möchten?',
|
||||
|
||||
// Connection Fields
|
||||
'connections.field.service': 'Service',
|
||||
'connections.field.status': 'Status',
|
||||
'connections.field.external_username': 'Externer Benutzername',
|
||||
'connections.field.external_email': 'Externe E-Mail',
|
||||
'connections.field.connected_at': 'Verbunden am',
|
||||
'connections.field.last_checked': 'Zuletzt geprüft',
|
||||
'connections.field.expires_at': 'Läuft ab am',
|
||||
|
||||
// Connection Services
|
||||
'connections.service.google': 'Google',
|
||||
'connections.service.microsoft': 'Microsoft',
|
||||
'connections.service.local': 'Lokal',
|
||||
|
||||
// Connection Placeholders
|
||||
'connections.placeholder.external_username': 'Externen Benutzernamen eingeben',
|
||||
'connections.placeholder.external_email': 'Externe E-Mail-Adresse eingeben',
|
||||
|
||||
// Connection Actions
|
||||
'connections.action.edit': 'Bearbeiten',
|
||||
'connections.action.toggle_connection': 'Verbindung umschalten',
|
||||
'connections.action.delete': 'Löschen',
|
||||
|
||||
// Prompt Modal
|
||||
'modal.create_prompt': 'Neuen Prompt erstellen',
|
||||
'modal.name_required': 'Name ist erforderlich',
|
||||
'modal.content_required': 'Inhalt ist erforderlich',
|
||||
'modal.create_error': 'Fehler beim Erstellen des Prompts',
|
||||
'modal.name_label': 'Name',
|
||||
'modal.content_label': 'Inhalt',
|
||||
'modal.name_placeholder': 'Geben Sie einen Namen für den Prompt ein',
|
||||
'modal.content_placeholder': 'Geben Sie den Inhalt des Prompts ein',
|
||||
'modal.cancel': 'Abbrechen',
|
||||
'modal.creating': 'Erstellen...',
|
||||
'modal.create': 'Prompt erstellen',
|
||||
|
||||
// Share Modal
|
||||
'share_modal.title': 'Prompt teilen',
|
||||
'share_modal.select_users': 'Benutzer auswählen',
|
||||
'share_modal.select_all': 'Alle auswählen',
|
||||
'share_modal.deselect_all': 'Alle abwählen',
|
||||
'share_modal.loading_users': 'Benutzer werden geladen...',
|
||||
'share_modal.error_loading_users': 'Fehler beim Laden der Benutzer',
|
||||
'share_modal.no_users_available': 'Keine Benutzer verfügbar',
|
||||
'share_modal.no_users_selected': 'Bitte wählen Sie mindestens einen Benutzer aus',
|
||||
'share_modal.one_user_selected': '1 Benutzer ausgewählt',
|
||||
'share_modal.multiple_users_selected': '{count} Benutzer ausgewählt',
|
||||
'share_modal.custom_title': 'Benutzerdefinierter Titel (optional)',
|
||||
'share_modal.title_placeholder': 'Geben Sie einen benutzerdefinierten Titel ein',
|
||||
'share_modal.message': 'Nachricht (optional)',
|
||||
'share_modal.message_placeholder': 'Fügen Sie eine Nachricht für die Empfänger hinzu',
|
||||
'share_modal.share': 'Teilen',
|
||||
'share_modal.sharing': 'Wird geteilt...',
|
||||
'share_modal.share_error': 'Fehler beim Teilen des Prompts',
|
||||
|
||||
// Prompt Settings
|
||||
'prompt_settings.title': 'Prompt Einstellungen',
|
||||
'prompt_settings.content_placeholder': 'Einstellungen werden in zukünftigen Updates hinzugefügt.',
|
||||
|
||||
// Chat Area
|
||||
'chat.continue_conversation': 'Gespräch fortsetzen...',
|
||||
'chat.enter_message': 'Nachricht eingeben...',
|
||||
'chat.remove_file': 'Datei entfernen',
|
||||
'chat.attach_file': 'Datei anhängen',
|
||||
'chat.you': 'You',
|
||||
'chat.click_to_open': 'Klicken Sie, um zu öffnen',
|
||||
'chat.preview_document': 'Dokument vorschauen',
|
||||
'chat.download_document': 'Dokument herunterladen',
|
||||
'chat.workflow_failed': 'Workflow fehlgeschlagen.',
|
||||
'chat.retry_workflow': 'Nochmal versuchen',
|
||||
'chat.sending_followup': 'Folgenachricht wird gesendet...',
|
||||
'chat.sending_message': 'Nachricht wird gesendet...',
|
||||
'chat.error_prefix': 'Fehler:',
|
||||
'chat.error_loading_messages': 'Fehler beim Laden der Nachrichten:',
|
||||
'chat.loading_workflow_messages': 'Workflow-Nachrichten werden geladen...',
|
||||
'chat.start_conversation': 'Beginne ein Gespräch, indem du eine Nachricht eingibst, eine Vorlage auswählst oder einen vorherigen Workflow fortsetzt …',
|
||||
|
||||
// File Preview
|
||||
'file_preview.loading': 'Vorschau wird geladen...',
|
||||
'file_preview.error': 'Fehler',
|
||||
'file_preview.no_preview': 'Keine Vorschau verfügbar',
|
||||
'file_preview.close_preview': 'Vorschau schließen',
|
||||
'file_preview.python': 'Python',
|
||||
|
||||
// Chat History
|
||||
'chat_history.loading': 'Workflows werden geladen...',
|
||||
'chat_history.error_loading': 'Fehler beim Laden der Workflows:',
|
||||
'chat_history.try_again': 'Nochmal versuchen',
|
||||
'chat_history.title': 'Workflow-Verlauf',
|
||||
'chat_history.workflow_count': 'Workflow',
|
||||
'chat_history.workflow_count_plural': 'Workflows',
|
||||
'chat_history.empty_state': 'Keine Workflows verfügbar',
|
||||
'chat_history.confirm_delete': 'Sind Sie sicher, dass Sie Workflow "{id}..." löschen möchten?',
|
||||
'chat_history.no_message_content': 'Kein Nachrichteninhalt verfügbar',
|
||||
'chat_history.unknown_date': 'Unbekanntes Datum',
|
||||
'chat_history.invalid_date': 'Ungültiges Datum',
|
||||
'chat_history.started': 'Gestartet:',
|
||||
'chat_history.last_activity': 'Letzte Aktivität:',
|
||||
'chat_history.round': 'Runde',
|
||||
'chat_history.resume_tooltip': 'Workflow fortsetzen',
|
||||
'chat_history.delete_tooltip': 'Workflow löschen',
|
||||
'chat_history.deleting': 'Workflow wird gelöscht...',
|
||||
|
||||
// Workflow Status
|
||||
'status.error': 'FEHLER',
|
||||
'status.failed': 'FEHLGESCHLAGEN',
|
||||
'status.stopped': 'GESTOPPT',
|
||||
'status.cancelled': 'ABGEBROCHEN',
|
||||
'status.running': 'LÄUFT',
|
||||
'status.processing': 'VERARBEITUNG',
|
||||
'status.completed': 'ABGESCHLOSSEN',
|
||||
'status.pending': 'WARTEND',
|
||||
|
||||
// Files
|
||||
'files.unknown_size': 'Unbekannte Größe',
|
||||
'files.unknown_date': 'Unbekanntes Datum',
|
||||
'files.source.uploaded': 'Hochgeladen',
|
||||
'files.source.ai_created': 'KI-erstellt',
|
||||
'files.source.shared': 'Geteilt',
|
||||
'files.source.unknown': 'Unbekannt',
|
||||
'files.preview_tooltip': 'Datei vorschauen',
|
||||
'files.download_tooltip': 'Datei herunterladen',
|
||||
'files.delete_tooltip': 'Datei löschen',
|
||||
'files.delete_confirm_tooltip': 'Klicken Sie erneut zum Bestätigen der Löschung',
|
||||
'files.downloading': 'Laden...',
|
||||
'files.deleting': 'Löschen...',
|
||||
'files.delete_confirm': 'Zum Bestätigen klicken...',
|
||||
'files.no_files': 'Keine Dateien gefunden.',
|
||||
'files.no_shared_files': 'Keine mit Ihnen geteilten Dateien gefunden.',
|
||||
'files.no_ai_files': 'Keine von der KI erstellten Dateien gefunden.',
|
||||
'files.no_uploaded_files': 'Keine hochgeladenen Dateien gefunden.',
|
||||
'files.header.name': 'Name',
|
||||
'files.header.type': 'Typ',
|
||||
'files.header.size': 'Größe',
|
||||
'files.header.date': 'Datum',
|
||||
'files.selector.title': 'Dateien auswählen',
|
||||
'files.selector.tab.all': 'Alle Dateien',
|
||||
'files.selector.tab.uploads': 'Hochgeladen',
|
||||
'files.selector.tab.created': 'KI-erstellt',
|
||||
'files.selector.tab.shared': 'Geteilt',
|
||||
'files.selector.select_all': 'Alle auswählen',
|
||||
'files.selector.deselect_all': 'Alle abwählen',
|
||||
'files.selector.file_selected': 'Datei',
|
||||
'files.selector.files_selected': 'Dateien',
|
||||
'files.selector.selected_suffix': 'ausgewählt',
|
||||
'files.selector.upload_new': 'Neue Datei hochladen',
|
||||
'files.selector.loading': 'Dateien werden geladen...',
|
||||
'files.selector.error_loading': 'Fehler beim Laden der Dateien:',
|
||||
'files.upload.title': 'Datei hochladen',
|
||||
'files.upload.drop_here': 'Datei hier ablegen...',
|
||||
'files.upload.uploading': 'Lädt hoch...',
|
||||
'files.upload.drag_files': 'Dateien hierher ziehen',
|
||||
'files.upload.or': 'oder',
|
||||
'files.upload.browse': 'Durchsuchen',
|
||||
'files.upload.selected_file': 'Ausgewählte Datei:',
|
||||
'files.upload.upload_button': 'Hochladen',
|
||||
'files.upload.uploading_button': 'Wird hochgeladen...',
|
||||
'files.upload.success': 'Datei erfolgreich hochgeladen!',
|
||||
'files.upload.error': 'Beim Hochladen ist ein Fehler aufgetreten.',
|
||||
'files.upload.unexpected_error': 'Beim Hochladen ist ein unerwarteter Fehler aufgetreten.',
|
||||
|
||||
// Files Page
|
||||
'files.page.tab.all': 'Alle Dateien',
|
||||
'files.page.tab.uploads': 'Meine Uploads',
|
||||
'files.page.tab.created': 'Erstellte Dateien',
|
||||
'files.page.tab.shared': 'Geteilte Dateien',
|
||||
'files.page.add_file': 'Datei hinzufügen',
|
||||
'files.page.loading': 'Dateien werden geladen...',
|
||||
'files.page.error': 'Fehler:',
|
||||
};
|
||||
264
src/locales/en.ts
Normal file
264
src/locales/en.ts
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
export default {
|
||||
// Navigation
|
||||
'nav.dashboard': 'Dashboard',
|
||||
'nav.files': 'Files',
|
||||
'nav.team': 'Team Area',
|
||||
'nav.workflows': 'Workflows',
|
||||
'nav.connections': 'Connections',
|
||||
'nav.settings': 'Settings',
|
||||
|
||||
// Settings page
|
||||
'settings.title': 'Settings',
|
||||
'settings.appearance': 'Appearance',
|
||||
'settings.language': 'Language',
|
||||
'settings.about': 'About',
|
||||
'settings.version': 'Version',
|
||||
'settings.theme': 'Theme',
|
||||
'settings.theme.description': 'Switch between light and dark mode',
|
||||
'settings.language.description': 'Choose your preferred language',
|
||||
'settings.theme.light': 'Light',
|
||||
'settings.theme.dark': 'Dark',
|
||||
'settings.theme.toggle.light': 'Switch to light mode',
|
||||
'settings.theme.toggle.dark': 'Switch to dark mode',
|
||||
|
||||
// Languages
|
||||
'language.german': 'Deutsch',
|
||||
'language.english': 'English',
|
||||
'language.french': 'Français',
|
||||
|
||||
// Common
|
||||
'common.loading': 'Loading...',
|
||||
'common.error': 'Error',
|
||||
'common.success': 'Success',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.save': 'Save',
|
||||
'common.delete': 'Delete',
|
||||
'common.edit': 'Edit',
|
||||
'common.close': 'Close',
|
||||
|
||||
// Auth
|
||||
'auth.login': 'Login',
|
||||
'auth.register': 'Register',
|
||||
'auth.logout': 'Logout',
|
||||
'auth.email': 'Email',
|
||||
'auth.password': 'Password',
|
||||
|
||||
// Dashboard
|
||||
'dashboard.prompt.template': 'Prompt Template',
|
||||
'dashboard.prompt.settings': 'Settings',
|
||||
'dashboard.chat.area': 'Chat Area',
|
||||
'dashboard.chat.history': 'Workflow History',
|
||||
'dashboard.log.title': 'Log',
|
||||
'dashboard.log.workflow': 'Workflow',
|
||||
'dashboard.log.no_workflow': 'No workflow selected',
|
||||
'dashboard.log.loading': 'Loading logs...',
|
||||
'dashboard.log.error': 'Error loading logs',
|
||||
'dashboard.log.no_logs': 'No logs available for this workflow',
|
||||
'dashboard.log.waiting': 'Workflow running... Waiting for logs...',
|
||||
'dashboard.log.fetch_failed': 'Failed to fetch logs',
|
||||
'dashboard.log.level.info': 'INFO',
|
||||
|
||||
// Prompt Set
|
||||
'promptset.loading': 'Loading prompts...',
|
||||
'promptset.error.loading': 'Error loading prompts',
|
||||
'promptset.retry': 'Try again',
|
||||
'promptset.new_prompt': 'New Prompt',
|
||||
'promptset.prompt_count': 'Prompt',
|
||||
'promptset.prompt_count_plural': 'Prompts',
|
||||
'promptset.no_prompts': 'No prompts available',
|
||||
'promptset.created': 'Created',
|
||||
'promptset.run_tooltip': 'Run prompt',
|
||||
'promptset.share_tooltip': 'Share prompt',
|
||||
'promptset.delete_tooltip': 'Delete prompt',
|
||||
'promptset.confirm_delete': 'Click again to confirm',
|
||||
'promptset.deleting': 'Deleting...',
|
||||
'promptset.confirm_click': 'Click to confirm',
|
||||
'promptset.delete_error': 'Error deleting',
|
||||
'promptset.deleting_message': 'Deleting prompt...',
|
||||
|
||||
// Connections
|
||||
'connections.title': 'Connections',
|
||||
'connections.connect_google': 'Connect Google',
|
||||
'connections.connect_microsoft': 'Connect Microsoft',
|
||||
'connections.edit_connection_title': 'Edit {authority} Connection',
|
||||
'connections.update_connection': 'Update Connection',
|
||||
'connections.service_connections': 'Service Connections',
|
||||
'connections.error': 'Error',
|
||||
'connections.connection_error': 'Connection Error',
|
||||
'connections.disconnect_error': 'Disconnect Error',
|
||||
'connections.unknown': 'Unknown',
|
||||
'connections.not_available': 'N/A',
|
||||
'connections.invalid_date': 'Invalid Date',
|
||||
'connections.confirm_delete': 'Are you sure you want to delete the {service} connection?',
|
||||
|
||||
// Connection Fields
|
||||
'connections.field.service': 'Service',
|
||||
'connections.field.status': 'Status',
|
||||
'connections.field.external_username': 'External Username',
|
||||
'connections.field.external_email': 'External Email',
|
||||
'connections.field.connected_at': 'Connected At',
|
||||
'connections.field.last_checked': 'Last Checked',
|
||||
'connections.field.expires_at': 'Expires At',
|
||||
|
||||
// Connection Services
|
||||
'connections.service.google': 'Google',
|
||||
'connections.service.microsoft': 'Microsoft',
|
||||
'connections.service.local': 'Local',
|
||||
|
||||
// Connection Placeholders
|
||||
'connections.placeholder.external_username': 'Enter external username',
|
||||
'connections.placeholder.external_email': 'Enter external email address',
|
||||
|
||||
// Connection Actions
|
||||
'connections.action.edit': 'Edit',
|
||||
'connections.action.toggle_connection': 'Toggle Connection',
|
||||
'connections.action.delete': 'Delete',
|
||||
|
||||
|
||||
// Prompt Modal
|
||||
'modal.create_prompt': 'Create New Prompt',
|
||||
'modal.name_required': 'Name is required',
|
||||
'modal.content_required': 'Content is required',
|
||||
'modal.create_error': 'Error creating prompt',
|
||||
'modal.name_label': 'Name',
|
||||
'modal.content_label': 'Content',
|
||||
'modal.name_placeholder': 'Enter a name for the prompt',
|
||||
'modal.content_placeholder': 'Enter the prompt content',
|
||||
'modal.cancel': 'Cancel',
|
||||
'modal.creating': 'Creating...',
|
||||
'modal.create': 'Create Prompt',
|
||||
|
||||
// Share Modal
|
||||
'share_modal.title': 'Share Prompt',
|
||||
'share_modal.select_users': 'Select Users',
|
||||
'share_modal.select_all': 'Select All',
|
||||
'share_modal.deselect_all': 'Deselect All',
|
||||
'share_modal.loading_users': 'Loading users...',
|
||||
'share_modal.error_loading_users': 'Error loading users',
|
||||
'share_modal.no_users_available': 'No users available',
|
||||
'share_modal.no_users_selected': 'Please select at least one user',
|
||||
'share_modal.one_user_selected': '1 user selected',
|
||||
'share_modal.multiple_users_selected': '{count} users selected',
|
||||
'share_modal.custom_title': 'Custom Title (optional)',
|
||||
'share_modal.title_placeholder': 'Enter a custom title',
|
||||
'share_modal.message': 'Message (optional)',
|
||||
'share_modal.message_placeholder': 'Add a message for recipients',
|
||||
'share_modal.share': 'Share',
|
||||
'share_modal.sharing': 'Sharing...',
|
||||
'share_modal.share_error': 'Error sharing prompt',
|
||||
|
||||
// Prompt Settings
|
||||
'prompt_settings.title': 'Prompt Settings',
|
||||
'prompt_settings.content_placeholder': 'Settings content will be added here in future updates.',
|
||||
|
||||
// Chat Area
|
||||
'chat.continue_conversation': 'Continue conversation...',
|
||||
'chat.enter_message': 'Enter message...',
|
||||
'chat.remove_file': 'Remove file',
|
||||
'chat.attach_file': 'Attach file',
|
||||
'chat.you': 'You',
|
||||
'chat.click_to_open': 'Click to open',
|
||||
'chat.preview_document': 'Preview document',
|
||||
'chat.download_document': 'Download document',
|
||||
'chat.workflow_failed': 'Workflow failed.',
|
||||
'chat.retry_workflow': 'Try again',
|
||||
'chat.sending_followup': 'Sending follow-up message...',
|
||||
'chat.sending_message': 'Sending message...',
|
||||
'chat.error_prefix': 'Error:',
|
||||
'chat.error_loading_messages': 'Error loading messages:',
|
||||
'chat.loading_workflow_messages': 'Loading workflow messages...',
|
||||
'chat.start_conversation': 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...',
|
||||
|
||||
// File Preview
|
||||
'file_preview.loading': 'Loading preview...',
|
||||
'file_preview.error': 'Error',
|
||||
'file_preview.no_preview': 'No preview available',
|
||||
'file_preview.close_preview': 'Close preview',
|
||||
'file_preview.python': 'Python',
|
||||
|
||||
// Chat History
|
||||
'chat_history.loading': 'Loading workflows...',
|
||||
'chat_history.error_loading': 'Error loading workflows:',
|
||||
'chat_history.try_again': 'Try Again',
|
||||
'chat_history.title': 'Workflow History',
|
||||
'chat_history.workflow_count': 'Workflow',
|
||||
'chat_history.workflow_count_plural': 'Workflows',
|
||||
'chat_history.empty_state': 'No workflows available',
|
||||
'chat_history.confirm_delete': 'Are you sure you want to delete workflow "{id}..."?',
|
||||
'chat_history.no_message_content': 'No message content available',
|
||||
'chat_history.unknown_date': 'Unknown date',
|
||||
'chat_history.invalid_date': 'Invalid date',
|
||||
'chat_history.started': 'Started:',
|
||||
'chat_history.last_activity': 'Last Activity:',
|
||||
'chat_history.round': 'Round',
|
||||
'chat_history.resume_tooltip': 'Resume workflow',
|
||||
'chat_history.delete_tooltip': 'Delete workflow',
|
||||
'chat_history.deleting': 'Deleting workflow...',
|
||||
|
||||
// Workflow Status
|
||||
'status.error': 'ERROR',
|
||||
'status.failed': 'FAILED',
|
||||
'status.stopped': 'STOPPED',
|
||||
'status.cancelled': 'CANCELLED',
|
||||
'status.running': 'RUNNING',
|
||||
'status.processing': 'PROCESSING',
|
||||
'status.completed': 'COMPLETED',
|
||||
'status.pending': 'PENDING',
|
||||
|
||||
// Files
|
||||
'files.unknown_size': 'Unknown Size',
|
||||
'files.unknown_date': 'Unknown Date',
|
||||
'files.source.uploaded': 'Uploaded',
|
||||
'files.source.ai_created': 'AI-created',
|
||||
'files.source.shared': 'Shared',
|
||||
'files.source.unknown': 'Unknown',
|
||||
'files.preview_tooltip': 'Preview file',
|
||||
'files.download_tooltip': 'Download file',
|
||||
'files.delete_tooltip': 'Delete file',
|
||||
'files.delete_confirm_tooltip': 'Click again to confirm deletion',
|
||||
'files.downloading': 'Downloading...',
|
||||
'files.deleting': 'Deleting...',
|
||||
'files.delete_confirm': 'Click to confirm...',
|
||||
'files.no_files': 'No files found.',
|
||||
'files.no_shared_files': 'No shared files found.',
|
||||
'files.no_ai_files': 'No AI-created files found.',
|
||||
'files.no_uploaded_files': 'No uploaded files found.',
|
||||
'files.header.name': 'Name',
|
||||
'files.header.type': 'Type',
|
||||
'files.header.size': 'Size',
|
||||
'files.header.date': 'Date',
|
||||
'files.selector.title': 'Select files',
|
||||
'files.selector.tab.all': 'All files',
|
||||
'files.selector.tab.uploads': 'Uploaded',
|
||||
'files.selector.tab.created': 'AI-created',
|
||||
'files.selector.tab.shared': 'Shared',
|
||||
'files.selector.select_all': 'Select all',
|
||||
'files.selector.deselect_all': 'Deselect all',
|
||||
'files.selector.file_selected': 'File',
|
||||
'files.selector.files_selected': 'Files',
|
||||
'files.selector.selected_suffix': 'selected',
|
||||
'files.selector.upload_new': 'Upload new file',
|
||||
'files.selector.loading': 'Loading files...',
|
||||
'files.selector.error_loading': 'Error loading files:',
|
||||
'files.upload.title': 'Upload file',
|
||||
'files.upload.drop_here': 'Drop file here...',
|
||||
'files.upload.uploading': 'Uploading...',
|
||||
'files.upload.drag_files': 'Drag files here',
|
||||
'files.upload.or': 'or',
|
||||
'files.upload.browse': 'Browse',
|
||||
'files.upload.selected_file': 'Selected file:',
|
||||
'files.upload.upload_button': 'Upload',
|
||||
'files.upload.uploading_button': 'Uploading...',
|
||||
'files.upload.success': 'File uploaded successfully!',
|
||||
'files.upload.error': 'An error occurred while uploading.',
|
||||
'files.upload.unexpected_error': 'An unexpected error occurred while uploading.',
|
||||
|
||||
// Files Page
|
||||
'files.page.tab.all': 'All Files',
|
||||
'files.page.tab.uploads': 'My Uploads',
|
||||
'files.page.tab.created': 'Created Files',
|
||||
'files.page.tab.shared': 'Shared Files',
|
||||
'files.page.add_file': 'Add File',
|
||||
'files.page.loading': 'Loading files...',
|
||||
'files.page.error': 'Error:',
|
||||
};
|
||||
263
src/locales/fr.ts
Normal file
263
src/locales/fr.ts
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
export default {
|
||||
// Navigation
|
||||
'nav.dashboard': 'Centre d\'activité',
|
||||
'nav.files': 'Fichiers',
|
||||
'nav.team': 'Espace équipe',
|
||||
'nav.workflows': 'Workflows',
|
||||
'nav.connections': 'Connections',
|
||||
'nav.settings': 'Paramètres',
|
||||
|
||||
// Settings page
|
||||
'settings.title': 'Paramètres',
|
||||
'settings.appearance': 'Apparence',
|
||||
'settings.language': 'Langue',
|
||||
'settings.about': 'À propos',
|
||||
'settings.version': 'Version',
|
||||
'settings.theme': 'Thème',
|
||||
'settings.theme.description': 'Basculer entre le mode clair et sombre',
|
||||
'settings.language.description': 'Choisissez votre langue préférée',
|
||||
'settings.theme.light': 'Clair',
|
||||
'settings.theme.dark': 'Sombre',
|
||||
'settings.theme.toggle.light': 'Passer en mode clair',
|
||||
'settings.theme.toggle.dark': 'Passer en mode sombre',
|
||||
|
||||
// Languages
|
||||
'language.german': 'Deutsch',
|
||||
'language.english': 'English',
|
||||
'language.french': 'Français',
|
||||
|
||||
// Common
|
||||
'common.loading': 'Chargement...',
|
||||
'common.error': 'Erreur',
|
||||
'common.success': 'Succès',
|
||||
'common.cancel': 'Annuler',
|
||||
'common.save': 'Enregistrer',
|
||||
'common.delete': 'Supprimer',
|
||||
'common.edit': 'Modifier',
|
||||
'common.close': 'Fermer',
|
||||
|
||||
// Auth
|
||||
'auth.login': 'Se connecter',
|
||||
'auth.register': 'S\'inscrire',
|
||||
'auth.logout': 'Se déconnecter',
|
||||
'auth.email': 'E-mail',
|
||||
'auth.password': 'Mot de passe',
|
||||
|
||||
// Dashboard
|
||||
'dashboard.prompt.template': 'Modèle de prompt',
|
||||
'dashboard.prompt.settings': 'Paramètres',
|
||||
'dashboard.chat.area': 'Zone de chat',
|
||||
'dashboard.chat.history': 'Historique des workflows',
|
||||
'dashboard.log.title': 'Journal',
|
||||
'dashboard.log.workflow': 'Workflow',
|
||||
'dashboard.log.no_workflow': 'Aucun workflow sélectionné',
|
||||
'dashboard.log.loading': 'Chargement des logs...',
|
||||
'dashboard.log.error': 'Erreur lors du chargement des logs',
|
||||
'dashboard.log.no_logs': 'Aucun log disponible pour ce workflow',
|
||||
'dashboard.log.waiting': 'Workflow en cours... En attente des logs...',
|
||||
'dashboard.log.fetch_failed': 'Échec du chargement des logs',
|
||||
'dashboard.log.level.info': 'INFO',
|
||||
|
||||
// Prompt Set
|
||||
'promptset.loading': 'Chargement des prompts...',
|
||||
'promptset.error.loading': 'Erreur lors du chargement des prompts',
|
||||
'promptset.retry': 'Réessayer',
|
||||
'promptset.new_prompt': 'Nouveau prompt',
|
||||
'promptset.prompt_count': 'Prompt',
|
||||
'promptset.prompt_count_plural': 'Prompts',
|
||||
'promptset.no_prompts': 'Aucun prompt disponible',
|
||||
'promptset.created': 'Créé',
|
||||
'promptset.run_tooltip': 'Exécuter le prompt',
|
||||
'promptset.share_tooltip': 'Partager le prompt',
|
||||
'promptset.delete_tooltip': 'Supprimer le prompt',
|
||||
'promptset.confirm_delete': 'Cliquez à nouveau pour confirmer',
|
||||
'promptset.deleting': 'Suppression...',
|
||||
'promptset.confirm_click': 'Cliquez pour confirmer',
|
||||
'promptset.delete_error': 'Erreur lors de la suppression',
|
||||
'promptset.deleting_message': 'Suppression du prompt...',
|
||||
|
||||
// Connections
|
||||
'connections.title': 'Connexions',
|
||||
'connections.connect_google': 'Connecter Google',
|
||||
'connections.connect_microsoft': 'Connecter Microsoft',
|
||||
'connections.edit_connection_title': 'Modifier la connexion {authority}',
|
||||
'connections.update_connection': 'Mettre à jour la connexion',
|
||||
'connections.service_connections': 'Connexions de service',
|
||||
'connections.error': 'Erreur',
|
||||
'connections.connection_error': 'Erreur de connexion',
|
||||
'connections.disconnect_error': 'Erreur de déconnexion',
|
||||
'connections.unknown': 'Inconnu',
|
||||
'connections.not_available': 'N/D',
|
||||
'connections.invalid_date': 'Date invalide',
|
||||
'connections.confirm_delete': 'Êtes-vous sûr de vouloir supprimer la connexion {service} ?',
|
||||
|
||||
// Connection Fields
|
||||
'connections.field.service': 'Service',
|
||||
'connections.field.status': 'Statut',
|
||||
'connections.field.external_username': 'Nom d\'utilisateur externe',
|
||||
'connections.field.external_email': 'E-mail externe',
|
||||
'connections.field.connected_at': 'Connecté le',
|
||||
'connections.field.last_checked': 'Dernière vérification',
|
||||
'connections.field.expires_at': 'Expire le',
|
||||
|
||||
// Connection Services
|
||||
'connections.service.google': 'Google',
|
||||
'connections.service.microsoft': 'Microsoft',
|
||||
'connections.service.local': 'Local',
|
||||
|
||||
// Connection Placeholders
|
||||
'connections.placeholder.external_username': 'Entrez le nom d\'utilisateur externe',
|
||||
'connections.placeholder.external_email': 'Entrez l\'adresse e-mail externe',
|
||||
|
||||
// Connection Actions
|
||||
'connections.action.edit': 'Modifier',
|
||||
'connections.action.toggle_connection': 'Basculer la connexion',
|
||||
'connections.action.delete': 'Supprimer',
|
||||
|
||||
// Prompt Modal
|
||||
'modal.create_prompt': 'Créer un nouveau prompt',
|
||||
'modal.name_required': 'Le nom est requis',
|
||||
'modal.content_required': 'Le contenu est requis',
|
||||
'modal.create_error': 'Erreur lors de la création du prompt',
|
||||
'modal.name_label': 'Nom',
|
||||
'modal.content_label': 'Contenu',
|
||||
'modal.name_placeholder': 'Entrez un nom pour le prompt',
|
||||
'modal.content_placeholder': 'Entrez le contenu du prompt',
|
||||
'modal.cancel': 'Annuler',
|
||||
'modal.creating': 'Création...',
|
||||
'modal.create': 'Créer le prompt',
|
||||
|
||||
// Share Modal
|
||||
'share_modal.title': 'Partager le prompt',
|
||||
'share_modal.select_users': 'Sélectionner les utilisateurs',
|
||||
'share_modal.select_all': 'Tout sélectionner',
|
||||
'share_modal.deselect_all': 'Tout désélectionner',
|
||||
'share_modal.loading_users': 'Chargement des utilisateurs...',
|
||||
'share_modal.error_loading_users': 'Erreur lors du chargement des utilisateurs',
|
||||
'share_modal.no_users_available': 'Aucun utilisateur disponible',
|
||||
'share_modal.no_users_selected': 'Veuillez sélectionner au moins un utilisateur',
|
||||
'share_modal.one_user_selected': '1 utilisateur sélectionné',
|
||||
'share_modal.multiple_users_selected': '{count} utilisateurs sélectionnés',
|
||||
'share_modal.custom_title': 'Titre personnalisé (facultatif)',
|
||||
'share_modal.title_placeholder': 'Entrez un titre personnalisé',
|
||||
'share_modal.message': 'Message (facultatif)',
|
||||
'share_modal.message_placeholder': 'Ajoutez un message pour les destinataires',
|
||||
'share_modal.share': 'Partager',
|
||||
'share_modal.sharing': 'Partage en cours...',
|
||||
'share_modal.share_error': 'Erreur lors du partage du prompt',
|
||||
|
||||
// Prompt Settings
|
||||
'prompt_settings.title': 'Paramètres de prompt',
|
||||
'prompt_settings.content_placeholder': 'Le contenu des paramètres sera ajouté dans les futures mises à jour.',
|
||||
|
||||
// Chat Area
|
||||
'chat.continue_conversation': 'Continuer la conversation...',
|
||||
'chat.enter_message': 'Entrez votre message...',
|
||||
'chat.remove_file': 'Supprimer le fichier',
|
||||
'chat.attach_file': 'Joindre un fichier',
|
||||
'chat.you': 'Vous',
|
||||
'chat.click_to_open': 'Cliquez pour ouvrir',
|
||||
'chat.preview_document': 'Aperçu du document',
|
||||
'chat.download_document': 'Télécharger le document',
|
||||
'chat.workflow_failed': 'Échec du workflow.',
|
||||
'chat.retry_workflow': 'Réessayer',
|
||||
'chat.sending_followup': 'Envoi du message de suivi...',
|
||||
'chat.sending_message': 'Envoi du message...',
|
||||
'chat.error_prefix': 'Erreur:',
|
||||
'chat.error_loading_messages': 'Erreur lors du chargement des messages:',
|
||||
'chat.loading_workflow_messages': 'Chargement des messages de workflow...',
|
||||
'chat.start_conversation': 'Commencez une conversation en entrant un message, en sélectionnant un modèle ou en continuant un workflow précédent...',
|
||||
|
||||
// File Preview
|
||||
'file_preview.loading': 'Chargement de l\'aperçu...',
|
||||
'file_preview.error': 'Erreur',
|
||||
'file_preview.no_preview': 'Aucun aperçu disponible',
|
||||
'file_preview.close_preview': 'Fermer l\'aperçu',
|
||||
'file_preview.python': 'Python',
|
||||
|
||||
// Chat History
|
||||
'chat_history.loading': 'Chargement des workflows...',
|
||||
'chat_history.error_loading': 'Erreur lors du chargement des workflows:',
|
||||
'chat_history.try_again': 'Réessayer',
|
||||
'chat_history.title': 'Historique des workflows',
|
||||
'chat_history.workflow_count': 'Workflow',
|
||||
'chat_history.workflow_count_plural': 'Workflows',
|
||||
'chat_history.empty_state': 'Aucun workflow disponible',
|
||||
'chat_history.confirm_delete': 'Êtes-vous sûr de vouloir supprimer le workflow "{id}..."?',
|
||||
'chat_history.no_message_content': 'Aucun contenu de message disponible',
|
||||
'chat_history.unknown_date': 'Date inconnue',
|
||||
'chat_history.invalid_date': 'Date invalide',
|
||||
'chat_history.started': 'Démarré:',
|
||||
'chat_history.last_activity': 'Dernière activité:',
|
||||
'chat_history.round': 'Tour',
|
||||
'chat_history.resume_tooltip': 'Reprendre le workflow',
|
||||
'chat_history.delete_tooltip': 'Supprimer le workflow',
|
||||
'chat_history.deleting': 'Suppression du workflow...',
|
||||
|
||||
// Workflow Status
|
||||
'status.error': 'ERREUR',
|
||||
'status.failed': 'ÉCHEC',
|
||||
'status.stopped': 'ARRÊTÉ',
|
||||
'status.cancelled': 'ANNULÉ',
|
||||
'status.running': 'EN COURS',
|
||||
'status.processing': 'TRAITEMENT',
|
||||
'status.completed': 'TERMINÉ',
|
||||
'status.pending': 'EN ATTENTE',
|
||||
|
||||
// Files
|
||||
'files.unknown_size': 'Taille inconnue',
|
||||
'files.unknown_date': 'Date inconnue',
|
||||
'files.source.uploaded': 'Téléchargé',
|
||||
'files.source.ai_created': 'Créé par IA',
|
||||
'files.source.shared': 'Partagé',
|
||||
'files.source.unknown': 'Inconnu',
|
||||
'files.preview_tooltip': 'Aperçu du fichier',
|
||||
'files.download_tooltip': 'Télécharger le fichier',
|
||||
'files.delete_tooltip': 'Supprimer le fichier',
|
||||
'files.delete_confirm_tooltip': 'Cliquez à nouveau pour confirmer la suppression',
|
||||
'files.downloading': 'Téléchargement...',
|
||||
'files.deleting': 'Suppression...',
|
||||
'files.delete_confirm': 'Cliquez pour confirmer...',
|
||||
'files.no_files': 'Aucun fichier trouvé.',
|
||||
'files.no_shared_files': 'Aucun fichier partagé trouvé.',
|
||||
'files.no_ai_files': 'Aucun fichier créé par IA trouvé.',
|
||||
'files.no_uploaded_files': 'Aucun fichier téléchargé trouvé.',
|
||||
'files.header.name': 'Nom',
|
||||
'files.header.type': 'Type',
|
||||
'files.header.size': 'Taille',
|
||||
'files.header.date': 'Date',
|
||||
'files.selector.title': 'Sélectionner des fichiers',
|
||||
'files.selector.tab.all': 'Tous les fichiers',
|
||||
'files.selector.tab.uploads': 'Téléchargés',
|
||||
'files.selector.tab.created': 'Créés par IA',
|
||||
'files.selector.tab.shared': 'Partagés',
|
||||
'files.selector.select_all': 'Tout sélectionner',
|
||||
'files.selector.deselect_all': 'Tout désélectionner',
|
||||
'files.selector.file_selected': 'Fichier',
|
||||
'files.selector.files_selected': 'Fichiers',
|
||||
'files.selector.selected_suffix': 'sélectionné(s)',
|
||||
'files.selector.upload_new': 'Télécharger un nouveau fichier',
|
||||
'files.selector.loading': 'Chargement des fichiers...',
|
||||
'files.selector.error_loading': 'Erreur lors du chargement des fichiers:',
|
||||
'files.upload.title': 'Télécharger un fichier',
|
||||
'files.upload.drop_here': 'Déposer le fichier ici...',
|
||||
'files.upload.uploading': 'Téléchargement...',
|
||||
'files.upload.drag_files': 'Glisser les fichiers ici',
|
||||
'files.upload.or': 'ou',
|
||||
'files.upload.browse': 'Parcourir',
|
||||
'files.upload.selected_file': 'Fichier sélectionné:',
|
||||
'files.upload.upload_button': 'Télécharger',
|
||||
'files.upload.uploading_button': 'Téléchargement...',
|
||||
'files.upload.success': 'Fichier téléchargé avec succès!',
|
||||
'files.upload.error': 'Une erreur s\'est produite lors du téléchargement.',
|
||||
'files.upload.unexpected_error': 'Une erreur inattendue s\'est produite lors du téléchargement.',
|
||||
|
||||
// Files Page
|
||||
'files.page.tab.all': 'Tous les fichiers',
|
||||
'files.page.tab.uploads': 'Mes téléchargements',
|
||||
'files.page.tab.created': 'Fichiers créés',
|
||||
'files.page.tab.shared': 'Fichiers partagés',
|
||||
'files.page.add_file': 'Ajouter un fichier',
|
||||
'files.page.loading': 'Chargement des fichiers...',
|
||||
'files.page.error': 'Erreur:',
|
||||
};
|
||||
37
src/locales/index.ts
Normal file
37
src/locales/index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Language, TranslationKeys } from './types';
|
||||
|
||||
// Dynamic language loader
|
||||
export const loadLanguage = async (language: Language): Promise<TranslationKeys> => {
|
||||
try {
|
||||
let translations: TranslationKeys;
|
||||
|
||||
switch (language) {
|
||||
case 'de':
|
||||
const de = await import('./de');
|
||||
translations = de.default;
|
||||
break;
|
||||
case 'en':
|
||||
const en = await import('./en');
|
||||
translations = en.default;
|
||||
break;
|
||||
case 'fr':
|
||||
const fr = await import('./fr');
|
||||
translations = fr.default;
|
||||
break;
|
||||
default:
|
||||
// Fallback to German if language not found
|
||||
const fallback = await import('./de');
|
||||
translations = fallback.default;
|
||||
}
|
||||
|
||||
return translations;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load language ${language}:`, error);
|
||||
// Fallback to German in case of error
|
||||
const fallback = await import('./de');
|
||||
return fallback.default;
|
||||
}
|
||||
};
|
||||
|
||||
// Re-export types
|
||||
export type { Language, TranslationKeys } from './types';
|
||||
12
src/locales/types.ts
Normal file
12
src/locales/types.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Language type definition
|
||||
export type Language = 'de' | 'en' | 'fr';
|
||||
|
||||
// Translation keys and their values
|
||||
export type TranslationKeys = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
// Language loader interface
|
||||
export interface LanguageLoader {
|
||||
loadLanguage: (language: Language) => Promise<TranslationKeys>;
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
import { setup, assign } from 'xstate';
|
||||
|
||||
export type SidebarEvent =
|
||||
| { type: 'TOGGLE_ITEM'; itemId: string }
|
||||
| { type: 'CLOSE_ALL' }
|
||||
| { type: 'NAVIGATE'; path: string }
|
||||
| { type: 'MINIMIZE_SIDEBAR' }
|
||||
| { type: 'EXPAND_SIDEBAR' };
|
||||
|
||||
export interface SidebarContext {
|
||||
openItemId: string | null;
|
||||
activePath: string;
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
||||
export const sidebarMachine = setup({
|
||||
types: {
|
||||
context: {} as SidebarContext,
|
||||
events: {} as SidebarEvent,
|
||||
},
|
||||
}).createMachine({
|
||||
id: 'sidebar',
|
||||
initial: 'collapsed',
|
||||
context: {
|
||||
openItemId: null,
|
||||
activePath: '/',
|
||||
isMinimized: false,
|
||||
},
|
||||
states: {
|
||||
collapsed: {
|
||||
on: {
|
||||
TOGGLE_ITEM: {
|
||||
guard: ({ context }) => !context.isMinimized,
|
||||
target: 'expanded',
|
||||
actions: assign({
|
||||
openItemId: ({ event }) => event.itemId,
|
||||
}),
|
||||
},
|
||||
CLOSE_ALL: {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
expanded: {
|
||||
on: {
|
||||
TOGGLE_ITEM: [
|
||||
{
|
||||
guard: ({ context, event }) => !context.isMinimized && context.openItemId === event.itemId,
|
||||
target: 'collapsed',
|
||||
actions: assign({
|
||||
openItemId: null,
|
||||
}),
|
||||
},
|
||||
{
|
||||
guard: ({ context }) => !context.isMinimized,
|
||||
target: 'expanded',
|
||||
actions: assign({
|
||||
openItemId: ({ event }) => event.itemId,
|
||||
}),
|
||||
},
|
||||
],
|
||||
CLOSE_ALL: {
|
||||
target: 'collapsed',
|
||||
actions: assign({
|
||||
openItemId: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
on: {
|
||||
NAVIGATE: {
|
||||
actions: assign({
|
||||
activePath: ({ event }) => event.path,
|
||||
}),
|
||||
},
|
||||
MINIMIZE_SIDEBAR: {
|
||||
actions: assign({
|
||||
isMinimized: true,
|
||||
openItemId: null, // Close any open submenu when minimizing
|
||||
}),
|
||||
},
|
||||
EXPAND_SIDEBAR: {
|
||||
actions: assign({
|
||||
isMinimized: false,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getSidebarState = (context: SidebarContext) => {
|
||||
return {
|
||||
hasOpenSubmenu: context.openItemId !== null,
|
||||
openItemId: context.openItemId,
|
||||
isItemOpen: (itemId: string) => context.openItemId === itemId,
|
||||
activePath: context.activePath,
|
||||
isMinimized: context.isMinimized,
|
||||
};
|
||||
};
|
||||
|
||||
export type SidebarMachineState = ReturnType<typeof sidebarMachine.transition>;
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
.dateienContainer {
|
||||
margin: 51px 49px 0 36px;
|
||||
display: flex;
|
||||
padding: 0px 30px 30px 30px;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
border-radius: 30px;
|
||||
background: var(--color-bg);
|
||||
position: relative;
|
||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow: hidden;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.horizontalLineLight {
|
||||
width: calc(100% + 60px);
|
||||
background-color: var(--color-gray-disabled);
|
||||
height: 1px;
|
||||
margin-left: -30px;
|
||||
margin-bottom: 0;
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Combined Header with Tabs and Add Button */
|
||||
.combinedHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
min-height: 62px;
|
||||
}
|
||||
|
||||
.datei_hinzufügen_button {
|
||||
border-radius: 30px;
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: left;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.datei_hinzufügen_button:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.add_icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Tab Navigation Styles */
|
||||
.tabButtonDiv {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tabButtonWrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 20px 0px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.tabButtonActive {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tabButtonInactive {
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.tabButtonInactive:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tabUnderline {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
background-color: var(--color-text);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* Content area */
|
||||
.contentArea {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
height: calc(100vh - 300px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
98
src/pages/Home/Connections.tsx
Normal file
98
src/pages/Home/Connections.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import React, { useState } from 'react';
|
||||
import styles from './HomeStyles/Connections.module.css';
|
||||
import sharedStyles from './HomeStyles/pages.module.css';
|
||||
import { IoIosLink } from 'react-icons/io';
|
||||
import {
|
||||
ConnectionsTable,
|
||||
ConnectionEditModal,
|
||||
ConnectionsErrorDisplay,
|
||||
useConnectionsLogic,
|
||||
Connection
|
||||
} from '../../components/Connections';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
function Connections() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Use the custom hook for all business logic
|
||||
const {
|
||||
connections,
|
||||
isLoading,
|
||||
isConnecting,
|
||||
isDisconnecting,
|
||||
error,
|
||||
connectError,
|
||||
disconnectError,
|
||||
editPopupOpen,
|
||||
editingConnection,
|
||||
connectionColumns,
|
||||
connectionEditFields,
|
||||
tableActions,
|
||||
handleCreateConnection,
|
||||
handleSaveConnection,
|
||||
handleCancelEdit
|
||||
} = useConnectionsLogic();
|
||||
|
||||
// Local state for selected connections (if needed)
|
||||
const [selectedConnections, setSelectedConnections] = useState<Connection[]>([]);
|
||||
|
||||
return (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
{/* Page Header */}
|
||||
<div className={sharedStyles.pageHeader}>
|
||||
<h1 className={sharedStyles.pageTitle}>{t('connections.title')}</h1>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
className={sharedStyles.primaryButton}
|
||||
onClick={() => handleCreateConnection('google')}
|
||||
disabled={isLoading || isConnecting || isDisconnecting}
|
||||
>
|
||||
<span className={sharedStyles.buttonIcon}><IoIosLink /></span>
|
||||
{t('connections.connect_google')}
|
||||
</button>
|
||||
<button
|
||||
className={sharedStyles.primaryButton}
|
||||
onClick={() => handleCreateConnection('msft')}
|
||||
disabled={isLoading || isConnecting || isDisconnecting}
|
||||
>
|
||||
<span className={sharedStyles.buttonIcon}><IoIosLink /></span>
|
||||
{t('connections.connect_microsoft')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
|
||||
<div className={sharedStyles.contentArea}>
|
||||
{/* Error Display */}
|
||||
<ConnectionsErrorDisplay
|
||||
error={error}
|
||||
connectError={connectError}
|
||||
disconnectError={disconnectError}
|
||||
/>
|
||||
|
||||
{/* Connections Table */}
|
||||
<ConnectionsTable
|
||||
connections={connections}
|
||||
columns={connectionColumns}
|
||||
actions={tableActions}
|
||||
isLoading={isLoading}
|
||||
onRowSelect={setSelectedConnections}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit Connection Modal */}
|
||||
<ConnectionEditModal
|
||||
isOpen={editPopupOpen}
|
||||
connection={editingConnection}
|
||||
fields={connectionEditFields}
|
||||
onSave={handleSaveConnection}
|
||||
onCancel={handleCancelEdit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Connections;
|
||||
|
|
@ -1,16 +1,12 @@
|
|||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import DashboardPrompt from '../components/Dashboard/DashboardPrompt/DashboardPrompt';
|
||||
import DashboardChat from '../components/Dashboard/DashboardChat/DashboardChat';
|
||||
import DashboardLog from '../components/Dashboard/DashboardLog/DashboardLog';
|
||||
import { Prompt } from '../hooks/usePrompts';
|
||||
import styles from './Dashboard.module.css'
|
||||
import { Prompt } from '../../hooks/usePrompts';
|
||||
import styles from './HomeStyles/Dashboard.module.css'
|
||||
import DashboardChat from '../../components/Dashboard/DashboardChat/DashboardChat';
|
||||
|
||||
function Dashboard () {
|
||||
const [isChatExpanded, setIsChatExpanded] = useState(false);
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
||||
const [isPromptAreaCollapsed, setIsPromptAreaCollapsed] = useState(false);
|
||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
||||
const [workflowCompleted, setWorkflowCompleted] = useState(false);
|
||||
|
||||
const handleChatToggleExpand = () => {
|
||||
setIsChatExpanded(!isChatExpanded);
|
||||
|
|
@ -22,12 +18,14 @@ function Dashboard () {
|
|||
};
|
||||
|
||||
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
|
||||
setCurrentWorkflowId(workflowId);
|
||||
setCurrentWorkflowId(prevId => {
|
||||
// Reset completion status when workflow changes
|
||||
if (workflowId !== currentWorkflowId) {
|
||||
if (workflowId !== prevId) {
|
||||
setWorkflowCompleted(false);
|
||||
}
|
||||
}, [currentWorkflowId]);
|
||||
return workflowId;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleWorkflowCompletedChange = useCallback((completed: boolean) => {
|
||||
setWorkflowCompleted(completed);
|
||||
|
|
@ -64,3 +62,11 @@ function Dashboard () {
|
|||
}
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
function setCurrentWorkflowId(arg0: (prevId: any) => string | null) {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
|
||||
function setWorkflowCompleted(arg0: boolean) {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import styles from './Dateien.module.css'
|
||||
import styles from './HomeStyles/Dateien.module.css'
|
||||
import sharedStyles from './HomeStyles/pages.module.css'
|
||||
import { IoAddCircleOutline } from "react-icons/io5";
|
||||
import DateienUpload from '../../components/Dateien/DateienHinzufügen/DateienUploadTool';
|
||||
import DateienAll from '../../components/Dateien/DateienAll';
|
||||
|
|
@ -73,10 +74,14 @@ function Dateien() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={styles.dateienContainer}>
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
<h1 className={sharedStyles.pageTitle}>Dateien</h1>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
|
||||
{/* Combined Header with Tabs and Add Button */}
|
||||
<motion.div
|
||||
className={styles.combinedHeader}
|
||||
className={`${sharedStyles.pageHeader} ${styles.combinedHeader}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
|
|
@ -110,16 +115,16 @@ function Dateien() {
|
|||
</div>
|
||||
|
||||
<button
|
||||
className={styles.datei_hinzufügen_button}
|
||||
className={`${sharedStyles.primaryButton} ${styles.datei_hinzufügen_button}`}
|
||||
onClick={() => setIsUploadOpen(true)}
|
||||
>
|
||||
<IoAddCircleOutline className={styles.add_icon}/>
|
||||
<IoAddCircleOutline className={sharedStyles.buttonIcon}/>
|
||||
{t('files.page.add_file', 'Add File')}
|
||||
</button>
|
||||
</motion.div>
|
||||
<div className={styles.horizontalLineLight}></div>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
|
||||
<div className={styles.contentArea}>
|
||||
<div className={`${sharedStyles.contentArea} ${styles.contentArea}`}>
|
||||
<DateienUpload
|
||||
isOpen={isUploadOpen}
|
||||
onClose={handleUploadClose}
|
||||
|
|
@ -148,6 +153,7 @@ function Dateien() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import styles from './Einstellungen.module.css';
|
||||
import styles from './HomeStyles/Einstellungen.module.css';
|
||||
import sharedStyles from './HomeStyles/pages.module.css';
|
||||
import { useLanguage, Language } from '../../contexts/LanguageContext';
|
||||
|
||||
function Einstellungen() {
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const { currentLanguage, setLanguage, t } = useLanguage();
|
||||
const { currentLanguage, setLanguage, t, isLoading } = useLanguage();
|
||||
|
||||
// Sync component state with current theme on mount
|
||||
useEffect(() => {
|
||||
|
|
@ -31,6 +32,16 @@ function Einstellungen() {
|
|||
localStorage.setItem('theme', newIsDarkMode ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
const handleLanguageChange = async (language: Language) => {
|
||||
if (language === currentLanguage) return;
|
||||
|
||||
try {
|
||||
await setLanguage(language);
|
||||
} catch (error) {
|
||||
console.error('Failed to change language:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getLanguageLabel = (lang: Language): string => {
|
||||
switch (lang) {
|
||||
case 'de': return t('language.german');
|
||||
|
|
@ -40,15 +51,25 @@ function Einstellungen() {
|
|||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={styles.einstellungenContainer}>
|
||||
<div className={styles.contentWrapper}>
|
||||
<div className={styles.settingsCard}>
|
||||
<h1 className={styles.title}>{t('settings.title')}</h1>
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className={styles.settingsSection}>
|
||||
<h2 className={styles.sectionTitle}>{t('settings.appearance')}</h2>
|
||||
return (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
<h1 className={sharedStyles.pageTitle}>{t('settings.title')}</h1>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
|
||||
<div className={sharedStyles.contentArea}>
|
||||
<div className={styles.settingItem}>
|
||||
<div className={styles.settingInfo}>
|
||||
<span className={styles.settingLabel}>{t('settings.theme')}</span>
|
||||
|
|
@ -84,7 +105,7 @@ function Einstellungen() {
|
|||
<select
|
||||
className={styles.languageSelect}
|
||||
value={currentLanguage}
|
||||
onChange={(e) => setLanguage(e.target.value as Language)}
|
||||
onChange={(e) => handleLanguageChange(e.target.value as Language)}
|
||||
aria-label={t('settings.language')}
|
||||
>
|
||||
<option value="de">{getLanguageLabel('de')}</option>
|
||||
|
|
@ -105,7 +126,7 @@ function Einstellungen() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
|
||||
import styles from './Home.module.css'
|
||||
import styles from './HomeStyles/Home.module.css'
|
||||
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import Sidebar from '../../components/Sidebar';
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
13
src/pages/Home/HomeStyles/Connections.module.css
Normal file
13
src/pages/Home/HomeStyles/Connections.module.css
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
.connectionsTable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: var(--color-red);
|
||||
padding: 10px;
|
||||
background-color: var(--color-red-hover);
|
||||
border: 1px solid var(--color-red);
|
||||
border-radius: 25px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
79
src/pages/Home/HomeStyles/Dateien.module.css
Normal file
79
src/pages/Home/HomeStyles/Dateien.module.css
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/* Dateien Page Styles
|
||||
*
|
||||
* This page now uses shared styles from pages.module.css for consistency.
|
||||
* Only file management-specific styles should be defined here.
|
||||
*/
|
||||
|
||||
/* Remove old styles - now using shared styles */
|
||||
/* .dateienContainer - now using sharedStyles.pageContainer + sharedStyles.pageCardWithContentPadding */
|
||||
/* .horizontalLineLight - now using sharedStyles.horizontalDivider */
|
||||
/* .datei_hinzufügen_button - now using sharedStyles.primaryButton */
|
||||
/* .add_icon - now using sharedStyles.buttonIcon */
|
||||
|
||||
/* Dateien-specific header customization */
|
||||
.combinedHeader {
|
||||
/* Additional styling for the header with tabs - extends sharedStyles.pageHeader */
|
||||
}
|
||||
|
||||
/* Additional button customization if needed */
|
||||
.datei_hinzufügen_button {
|
||||
/* Any Dateien-specific button customizations go here */
|
||||
}
|
||||
|
||||
/* Tab Navigation Styles */
|
||||
.tabButtonDiv {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tabButtonWrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 20px 0px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.tabButtonActive {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tabButtonInactive {
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.tabButtonInactive:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tabUnderline {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
background-color: var(--color-text);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* Content area customization */
|
||||
.contentArea {
|
||||
/* Any Dateien-specific content area customizations go here */
|
||||
/* Base styling now comes from sharedStyles.contentArea */
|
||||
}
|
||||
|
|
@ -1,42 +1,3 @@
|
|||
.einstellungenContainer {
|
||||
margin: 51px 49px 0 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
justify-content: top;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow: hidden;
|
||||
font-family: var(--font-family);
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0px 0;
|
||||
}
|
||||
|
||||
.settingsCard {
|
||||
display: flex;
|
||||
padding: 30px;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
border-radius: 30px;
|
||||
background: var(--color-bg);
|
||||
position: relative;
|
||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0 0 20px 0;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.settingsSection {
|
||||
display: flex;
|
||||
|
|
@ -44,15 +5,6 @@
|
|||
gap: 20px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.settingItem {
|
||||
display: flex;
|
||||
28
src/pages/Home/HomeStyles/TeamBereich.module.css
Normal file
28
src/pages/Home/HomeStyles/TeamBereich.module.css
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
.membersList {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0; /* Custom top margin for spacing after divider */
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Keep specific height requirement for member list items */
|
||||
.membersList li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px; /* Specific height requirement for each member item */
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
font-size: 16px;
|
||||
transition: background-color 0.2s ease;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
16
src/pages/Home/HomeStyles/Workflows.module.css
Normal file
16
src/pages/Home/HomeStyles/Workflows.module.css
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* Workflows Page Styles
|
||||
*
|
||||
* This page now uses shared styles from pages.module.css for consistency.
|
||||
* Only workflow-specific styles should be defined here.
|
||||
*/
|
||||
|
||||
/* Remove old .workflowsContainer - now using sharedStyles.pageContainer + sharedStyles.pageCard */
|
||||
|
||||
/* Workflow-specific styles go here */
|
||||
.workflowItem {
|
||||
/* Future workflow item styling */
|
||||
}
|
||||
|
||||
.workflowActions {
|
||||
/* Future workflow action styling */
|
||||
}
|
||||
146
src/pages/Home/HomeStyles/pages.module.css
Normal file
146
src/pages/Home/HomeStyles/pages.module.css
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
|
||||
.pageContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: top;
|
||||
height: calc(100vh);
|
||||
overflow: hidden;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
/* Content wrapper for scrollable content */
|
||||
.contentWrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Card-style container with background and shadow */
|
||||
.pageCard {
|
||||
display: flex;
|
||||
padding: 25px;
|
||||
flex-direction: column;
|
||||
align-self: top;
|
||||
background: var(--color-bg);
|
||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.30);
|
||||
gap: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Page headers with consistent spacing */
|
||||
.pageHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
min-height: 62px;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
/* Page titles */
|
||||
.pageTitle {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
font-family: var(--font-family);
|
||||
|
||||
}
|
||||
|
||||
/* Common button styles */
|
||||
.primaryButton {
|
||||
border-radius: 30px;
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.primaryButton:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.secondaryButton {
|
||||
border-radius: 30px;
|
||||
background: var(--color-gray-disabled);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.secondaryButton:hover {
|
||||
background-color: var(--color-gray);
|
||||
}
|
||||
|
||||
/* Common icon styles for buttons */
|
||||
.buttonIcon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Horizontal divider lines */
|
||||
.horizontalDivider {
|
||||
width: calc(100% + 60px);
|
||||
background-color: var(--color-primary);
|
||||
height: 1px;
|
||||
margin-left: -30px;
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Content areas */
|
||||
.contentArea {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
}
|
||||
|
||||
.scrollableContent {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Responsive design for smaller screens */
|
||||
@media (max-width: 768px) {
|
||||
.pageContainer {
|
||||
margin: 10px;
|
||||
height: calc(100vh - 20px);
|
||||
max-height: calc(100vh - 20px);
|
||||
}
|
||||
|
||||
.pageCard,
|
||||
.pageCardWithContentPadding {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.pageHeader {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.pageTitle {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
63
src/pages/Home/TeamBereich.tsx
Normal file
63
src/pages/Home/TeamBereich.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import styles from './HomeStyles/TeamBereich.module.css'
|
||||
import sharedStyles from './HomeStyles/pages.module.css'
|
||||
|
||||
import MitgliederItem from '../../components/Mitglieder/MitgliederItem';
|
||||
import { IoPersonAddSharp } from "react-icons/io5";
|
||||
import { useOrgUsers } from '../../hooks/useUsers';
|
||||
|
||||
function TeamBereich () {
|
||||
const { users, loading, error, refetch } = useOrgUsers();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={`${sharedStyles.pageCard} ${styles.mitgliederContainer}`}>
|
||||
<h1 className={sharedStyles.pageTitle}>Team-Bereich</h1>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
<p>Lade Mitglieder...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={`${sharedStyles.pageCard}`}>
|
||||
<h1 className={sharedStyles.pageTitle}>Team-Bereich</h1>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
<p>Fehler beim Laden der Mitglieder: {error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={`${sharedStyles.pageCard} ${styles.mitgliederContainer}`}>
|
||||
<h1 className={sharedStyles.pageTitle}>Team-Bereich</h1>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
<button className={`${sharedStyles.secondaryButton} ${styles.mitglieder_hinzufügen_button}`}>
|
||||
<IoPersonAddSharp className={sharedStyles.buttonIcon} />
|
||||
Mitglied hinzufügen
|
||||
</button>
|
||||
<ul className={styles.membersList}>
|
||||
{users.map((user) => (
|
||||
<MitgliederItem
|
||||
key={user.id}
|
||||
user={user}
|
||||
refetchUsers={refetch}
|
||||
totalUsers={users.length}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
{users.length === 0 && (
|
||||
<p>Keine Mitglieder gefunden.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamBereich;
|
||||
|
||||
317
src/pages/Home/TestSharepoint.module.css
Normal file
317
src/pages/Home/TestSharepoint.module.css
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
.container {
|
||||
padding: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 15px;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.connectionsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.connectionCard {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.connectionCard:hover {
|
||||
border-color: var(--border-accent);
|
||||
}
|
||||
|
||||
.connectionCard.active {
|
||||
border-color: var(--success);
|
||||
background: var(--bg-success-subtle);
|
||||
}
|
||||
|
||||
.connectionInfo {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.connectionName {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.connectionStatus {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.connectionActions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.testArea {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.requestPanel,
|
||||
.responsePanel {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.panelTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 15px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.formGroup {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.input,
|
||||
.textarea,
|
||||
.select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.textarea:focus,
|
||||
.select:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-accent);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-accent);
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.button.danger {
|
||||
background: var(--error);
|
||||
color: white;
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.button.danger:hover {
|
||||
background: var(--error-dark);
|
||||
}
|
||||
|
||||
.responseArea {
|
||||
background: var(--bg-code);
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.responseArea.success {
|
||||
border-color: var(--success);
|
||||
background: var(--bg-success-subtle);
|
||||
}
|
||||
|
||||
.responseArea.error {
|
||||
border-color: var(--error);
|
||||
background: var(--bg-error-subtle);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
background: var(--bg-error-subtle);
|
||||
border: 1px solid var(--error);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
color: var(--error);
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.successMessage {
|
||||
background: var(--bg-success-subtle);
|
||||
border: 1px solid var(--success);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
color: var(--success);
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.exampleButton {
|
||||
background: var(--bg-accent);
|
||||
color: var(--text-accent);
|
||||
border: 1px solid var(--border-accent);
|
||||
}
|
||||
|
||||
.exampleButton:hover {
|
||||
background: var(--bg-accent-hover);
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.statusBadge.active {
|
||||
background: var(--bg-success-subtle);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.statusBadge.pending {
|
||||
background: var(--bg-warning-subtle);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.statusBadge.expired {
|
||||
background: var(--bg-error-subtle);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.statusBadge.revoked {
|
||||
background: var(--bg-error-subtle);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
.tabContent {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 5px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.requestPreview {
|
||||
background: var(--bg-code);
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
602
src/pages/Home/TestSharepoint.tsx
Normal file
602
src/pages/Home/TestSharepoint.tsx
Normal file
|
|
@ -0,0 +1,602 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
useSharePointTest,
|
||||
SharePointConnection,
|
||||
SharePointListRequest,
|
||||
SharePointFindRequest,
|
||||
SharePointReadRequest,
|
||||
SharePointUploadRequest,
|
||||
SharePointResponse
|
||||
} from '../../hooks/useSharePointTest';
|
||||
import styles from './TestSharepoint.module.css';
|
||||
|
||||
type TestOperation = 'list' | 'find' | 'read' | 'upload';
|
||||
|
||||
interface FormData {
|
||||
connectionReference: string;
|
||||
siteUrl: string;
|
||||
folderPaths: string[];
|
||||
query: string;
|
||||
searchScope: string;
|
||||
documentList: string;
|
||||
documentPaths: string[];
|
||||
fileNames: string[];
|
||||
includeMetadata: boolean;
|
||||
includeSubfolders: boolean;
|
||||
}
|
||||
|
||||
function TestSharepoint() {
|
||||
const {
|
||||
getConnections,
|
||||
testConnection,
|
||||
listDocuments,
|
||||
findDocuments,
|
||||
readDocuments,
|
||||
uploadDocuments,
|
||||
getExamples,
|
||||
debugTokens,
|
||||
debugTokenDetails,
|
||||
discoverSites,
|
||||
isLoading,
|
||||
error,
|
||||
lastResponse
|
||||
} = useSharePointTest();
|
||||
|
||||
// State
|
||||
const [connections, setConnections] = useState<SharePointConnection[]>([]);
|
||||
const [selectedConnection, setSelectedConnection] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState<TestOperation>('list');
|
||||
const [examples, setExamples] = useState<any>({});
|
||||
const [testResults, setTestResults] = useState<any>({});
|
||||
const [tokenDebugInfo, setTokenDebugInfo] = useState<any>(null);
|
||||
|
||||
// Form data
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
connectionReference: '',
|
||||
siteUrl: 'https://your-tenant.sharepoint.com/sites/your-site',
|
||||
folderPaths: ['/'], // Start at root folder
|
||||
query: 'quarterly report 2024',
|
||||
searchScope: 'all',
|
||||
documentList: 'document_list_reference_from_chat',
|
||||
documentPaths: ['/Shared Documents/file1.docx', '/Documents/file2.pdf'],
|
||||
fileNames: ['uploaded_file1.docx', 'uploaded_file2.pdf'],
|
||||
includeMetadata: true,
|
||||
includeSubfolders: false // Default to false for better navigation UX
|
||||
});
|
||||
|
||||
// Load connections and examples on mount
|
||||
useEffect(() => {
|
||||
loadConnections();
|
||||
loadExamples();
|
||||
}, []);
|
||||
|
||||
// Update connection reference when selected connection changes
|
||||
useEffect(() => {
|
||||
setFormData(prev => ({ ...prev, connectionReference: selectedConnection }));
|
||||
}, [selectedConnection]);
|
||||
|
||||
const loadConnections = async () => {
|
||||
try {
|
||||
const conns = await getConnections();
|
||||
setConnections(conns);
|
||||
if (conns.length > 0) {
|
||||
setSelectedConnection(conns[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load connections:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadExamples = async () => {
|
||||
try {
|
||||
const exampleData = await getExamples();
|
||||
setExamples(exampleData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load examples:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async (connectionId: string) => {
|
||||
try {
|
||||
const result = await testConnection(connectionId);
|
||||
setTestResults((prev: any) => ({ ...prev, [connectionId]: result }));
|
||||
} catch (error) {
|
||||
console.error('Connection test failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDebugTokens = async () => {
|
||||
try {
|
||||
const result = await debugTokens();
|
||||
setTokenDebugInfo(result);
|
||||
} catch (error) {
|
||||
console.error('Token debug failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDebugTokenDetails = async () => {
|
||||
try {
|
||||
const result = await debugTokenDetails();
|
||||
setTokenDebugInfo(result);
|
||||
} catch (error) {
|
||||
console.error('Token details debug failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormChange = (field: keyof FormData, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleArrayInputChange = (field: 'folderPaths' | 'documentPaths' | 'fileNames', value: string) => {
|
||||
const array = value.split('\n').filter(item => item.trim() !== '');
|
||||
setFormData(prev => ({ ...prev, [field]: array }));
|
||||
};
|
||||
|
||||
const loadExample = (operation: TestOperation) => {
|
||||
const exampleKey = operation === 'list' ? 'listDocuments' :
|
||||
operation === 'find' ? 'findDocuments' :
|
||||
operation === 'read' ? 'readDocuments' : 'uploadDocuments';
|
||||
|
||||
if (examples[exampleKey]) {
|
||||
const example = examples[exampleKey];
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
connectionReference: selectedConnection || prev.connectionReference,
|
||||
siteUrl: example.siteUrl || prev.siteUrl,
|
||||
folderPaths: example.folderPaths || prev.folderPaths,
|
||||
query: example.query || prev.query,
|
||||
searchScope: example.searchScope || prev.searchScope,
|
||||
documentList: example.documentList || prev.documentList,
|
||||
documentPaths: example.documentPaths || prev.documentPaths,
|
||||
fileNames: example.fileNames || prev.fileNames,
|
||||
includeMetadata: example.includeMetadata !== undefined ? example.includeMetadata : prev.includeMetadata,
|
||||
includeSubfolders: example.includeSubfolders !== undefined ? example.includeSubfolders : prev.includeSubfolders
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const executeTest = async () => {
|
||||
if (!selectedConnection) {
|
||||
alert('Please select a connection first');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the selected connection to build proper reference
|
||||
const selectedConn = connections.find(conn => conn.id === selectedConnection);
|
||||
if (!selectedConn) {
|
||||
alert('Selected connection not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build connection reference in expected format: connection:{authority}:{username}:{id}
|
||||
const connectionReference = `connection:${selectedConn.authority}:${selectedConn.externalUsername}:${selectedConn.id}`;
|
||||
|
||||
try {
|
||||
let result: SharePointResponse;
|
||||
|
||||
switch (activeTab) {
|
||||
case 'list':
|
||||
const listRequest: SharePointListRequest = {
|
||||
connectionReference: connectionReference,
|
||||
siteUrl: formData.siteUrl,
|
||||
folderPaths: formData.folderPaths,
|
||||
includeSubfolders: formData.includeSubfolders,
|
||||
expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }]
|
||||
};
|
||||
result = await listDocuments(listRequest);
|
||||
break;
|
||||
|
||||
case 'find':
|
||||
const findRequest: SharePointFindRequest = {
|
||||
connectionReference: connectionReference,
|
||||
siteUrl: formData.siteUrl,
|
||||
query: formData.query,
|
||||
searchScope: formData.searchScope,
|
||||
expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }]
|
||||
};
|
||||
result = await findDocuments(findRequest);
|
||||
break;
|
||||
|
||||
case 'read':
|
||||
const readRequest: SharePointReadRequest = {
|
||||
documentList: formData.documentList,
|
||||
connectionReference: connectionReference,
|
||||
siteUrl: formData.siteUrl,
|
||||
documentPaths: formData.documentPaths,
|
||||
includeMetadata: formData.includeMetadata,
|
||||
expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }]
|
||||
};
|
||||
result = await readDocuments(readRequest);
|
||||
break;
|
||||
|
||||
case 'upload':
|
||||
const uploadRequest: SharePointUploadRequest = {
|
||||
connectionReference: connectionReference,
|
||||
siteUrl: formData.siteUrl,
|
||||
documentPaths: formData.folderPaths,
|
||||
documentList: formData.documentList,
|
||||
fileNames: formData.fileNames,
|
||||
expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }]
|
||||
};
|
||||
result = await uploadDocuments(uploadRequest);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Invalid operation');
|
||||
}
|
||||
|
||||
console.log('Test result:', result);
|
||||
} catch (error) {
|
||||
console.error('Test execution failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderConnectionCard = (connection: SharePointConnection) => {
|
||||
const testResult = testResults[connection.id];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={connection.id}
|
||||
className={`${styles.connectionCard} ${selectedConnection === connection.id ? styles.active : ''}`}
|
||||
onClick={() => setSelectedConnection(connection.id)}
|
||||
>
|
||||
<div className={styles.connectionInfo}>
|
||||
<div className={styles.connectionName}>
|
||||
{connection.externalUsername || connection.id}
|
||||
</div>
|
||||
<div className={styles.connectionStatus}>
|
||||
<span className={`${styles.statusBadge} ${styles[connection.status]}`}>
|
||||
{connection.status}
|
||||
</span>
|
||||
{connection.externalEmail && (
|
||||
<span style={{ marginLeft: '8px' }}>{connection.externalEmail}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.connectionActions}>
|
||||
<button
|
||||
className={styles.button}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleTestConnection(connection.id);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
{testResult && (
|
||||
<span className={testResult.success ? styles.successMessage : styles.errorMessage}>
|
||||
{testResult.success ? '✓' : '✗'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTestForm = () => {
|
||||
return (
|
||||
<div className={styles.requestPanel}>
|
||||
<div className={styles.panelTitle}>Request Configuration</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Connection</label>
|
||||
<select
|
||||
className={styles.select}
|
||||
value={selectedConnection}
|
||||
onChange={(e) => setSelectedConnection(e.target.value)}
|
||||
>
|
||||
<option value="">Select a connection</option>
|
||||
{connections.map(conn => (
|
||||
<option key={conn.id} value={conn.id}>
|
||||
{conn.externalUsername || conn.id} ({conn.status})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>SharePoint Site URL</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
value={formData.siteUrl}
|
||||
onChange={(e) => handleFormChange('siteUrl', e.target.value)}
|
||||
placeholder="https://your-tenant.sharepoint.com/sites/your-site"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{activeTab === 'list' && (
|
||||
<>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Folder Paths (one per line)</label>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
value={formData.folderPaths.join('\n')}
|
||||
onChange={(e) => handleArrayInputChange('folderPaths', e.target.value)}
|
||||
placeholder="/Shared Documents /Documents"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.includeSubfolders}
|
||||
onChange={(e) => handleFormChange('includeSubfolders', e.target.checked)}
|
||||
/>
|
||||
Include Subfolders
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'find' && (
|
||||
<>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Search Query</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
value={formData.query}
|
||||
onChange={(e) => handleFormChange('query', e.target.value)}
|
||||
placeholder="quarterly report 2024"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Search Scope</label>
|
||||
<select
|
||||
className={styles.select}
|
||||
value={formData.searchScope}
|
||||
onChange={(e) => handleFormChange('searchScope', e.target.value)}
|
||||
>
|
||||
<option value="all">All</option>
|
||||
<option value="documents">Documents Only</option>
|
||||
<option value="pages">Pages Only</option>
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'read' && (
|
||||
<>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Document List Reference</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
value={formData.documentList}
|
||||
onChange={(e) => handleFormChange('documentList', e.target.value)}
|
||||
placeholder="document_list_reference_from_chat"
|
||||
/>
|
||||
<div className={styles.helpText}>
|
||||
This should be a reference from a chat session or document management system
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Document Paths (one per line)</label>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
value={formData.documentPaths.join('\n')}
|
||||
onChange={(e) => handleArrayInputChange('documentPaths', e.target.value)}
|
||||
placeholder="/Shared Documents/file1.docx /Documents/file2.pdf"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.includeMetadata}
|
||||
onChange={(e) => handleFormChange('includeMetadata', e.target.checked)}
|
||||
/>
|
||||
Include Metadata
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'upload' && (
|
||||
<>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Document List Reference</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
value={formData.documentList}
|
||||
onChange={(e) => handleFormChange('documentList', e.target.value)}
|
||||
placeholder="document_list_reference_from_chat"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Upload Destination Paths (one per line)</label>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
value={formData.folderPaths.join('\n')}
|
||||
onChange={(e) => handleArrayInputChange('folderPaths', e.target.value)}
|
||||
placeholder="/Shared Documents/ /Documents/"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>File Names (one per line)</label>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
value={formData.fileNames.join('\n')}
|
||||
onChange={(e) => handleArrayInputChange('fileNames', e.target.value)}
|
||||
placeholder="uploaded_file1.docx uploaded_file2.pdf"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={styles.buttonGroup}>
|
||||
<button
|
||||
className={`${styles.button} ${styles.primary}`}
|
||||
onClick={executeTest}
|
||||
disabled={isLoading || !selectedConnection}
|
||||
>
|
||||
{isLoading ? 'Testing...' : `Test ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`}
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.button} ${styles.exampleButton}`}
|
||||
onClick={() => loadExample(activeTab)}
|
||||
>
|
||||
Load Example
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.requestPreview}>
|
||||
<strong>Request Preview:</strong>
|
||||
{JSON.stringify({
|
||||
endpoint: `/api/test-sharepoint/${activeTab === 'list' ? 'list-documents' :
|
||||
activeTab === 'find' ? 'find-documents' :
|
||||
activeTab === 'read' ? 'read-documents' : 'upload-documents'}`,
|
||||
method: 'POST',
|
||||
body: activeTab === 'list' ? {
|
||||
connectionReference: selectedConnection ?
|
||||
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
|
||||
'No connection selected',
|
||||
siteUrl: formData.siteUrl,
|
||||
folderPaths: formData.folderPaths,
|
||||
includeSubfolders: formData.includeSubfolders
|
||||
} : activeTab === 'find' ? {
|
||||
connectionReference: selectedConnection ?
|
||||
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
|
||||
'No connection selected',
|
||||
siteUrl: formData.siteUrl,
|
||||
query: formData.query,
|
||||
searchScope: formData.searchScope
|
||||
} : activeTab === 'read' ? {
|
||||
documentList: formData.documentList,
|
||||
connectionReference: selectedConnection ?
|
||||
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
|
||||
'No connection selected',
|
||||
siteUrl: formData.siteUrl,
|
||||
documentPaths: formData.documentPaths,
|
||||
includeMetadata: formData.includeMetadata
|
||||
} : {
|
||||
connectionReference: selectedConnection ?
|
||||
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
|
||||
'No connection selected',
|
||||
siteUrl: formData.siteUrl,
|
||||
documentPaths: formData.folderPaths,
|
||||
documentList: formData.documentList,
|
||||
fileNames: formData.fileNames
|
||||
}
|
||||
}, null, 2)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderResponse = () => {
|
||||
return (
|
||||
<div className={styles.responsePanel}>
|
||||
<div className={styles.panelTitle}>Response</div>
|
||||
|
||||
{error && (
|
||||
<div className={styles.errorMessage}>
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lastResponse && (
|
||||
<div className={`${styles.responseArea} ${lastResponse.success ? styles.success : styles.error}`}>
|
||||
{JSON.stringify(lastResponse, null, 2)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!lastResponse && !error && (
|
||||
<div className={styles.responseArea}>
|
||||
No response yet. Execute a test to see results here.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.title}>SharePoint Method Testing</h1>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>
|
||||
Microsoft Connections ({connections.length})
|
||||
</h2>
|
||||
|
||||
<div className={styles.buttonGroup} style={{ marginBottom: '15px' }}>
|
||||
<button
|
||||
className={`${styles.button} ${styles.exampleButton}`}
|
||||
onClick={handleDebugTokens}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Debug Authentication Tokens
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.button} ${styles.exampleButton}`}
|
||||
onClick={handleDebugTokenDetails}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Debug Token Details
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{tokenDebugInfo && (
|
||||
<div className={styles.responseArea} style={{ marginBottom: '15px' }}>
|
||||
<strong>Token Debug Info:</strong>
|
||||
<pre>{JSON.stringify(tokenDebugInfo, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{connections.length === 0 ? (
|
||||
<div className={styles.loading}>
|
||||
{isLoading ? 'Loading connections...' : 'No Microsoft connections found. Please create a connection first.'}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.connectionsGrid}>
|
||||
{connections.map(renderConnectionCard)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>SharePoint Operations Testing</h2>
|
||||
|
||||
<div className={styles.tabs}>
|
||||
<button
|
||||
className={`${styles.tab} ${activeTab === 'list' ? styles.active : ''}`}
|
||||
onClick={() => setActiveTab('list')}
|
||||
>
|
||||
List Documents
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tab} ${activeTab === 'find' ? styles.active : ''}`}
|
||||
onClick={() => setActiveTab('find')}
|
||||
>
|
||||
Find Documents
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tab} ${activeTab === 'read' ? styles.active : ''}`}
|
||||
onClick={() => setActiveTab('read')}
|
||||
>
|
||||
Read Documents
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tab} ${activeTab === 'upload' ? styles.active : ''}`}
|
||||
onClick={() => setActiveTab('upload')}
|
||||
>
|
||||
Upload Documents
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.tabContent}>
|
||||
<div className={styles.testArea}>
|
||||
{renderTestForm()}
|
||||
{renderResponse()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TestSharepoint;
|
||||
20
src/pages/Home/Workflows.tsx
Normal file
20
src/pages/Home/Workflows.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import styles from './HomeStyles/Workflows.module.css'
|
||||
import sharedStyles from './HomeStyles/pages.module.css'
|
||||
|
||||
function Workflows () {
|
||||
return (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
<h1 className={sharedStyles.pageTitle}>Workflows</h1>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
|
||||
<div className={sharedStyles.contentArea}>
|
||||
{/* Workflow content will go here */}
|
||||
<p>Workflow management coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Workflows;
|
||||
|
|
@ -1,27 +1,42 @@
|
|||
.container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-bg);
|
||||
|
||||
font-family: "DM Sans", sans-serif;
|
||||
color: #181818;
|
||||
}
|
||||
|
||||
.leftPanel {
|
||||
.mainContent {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 3rem;
|
||||
background-color: var(--color-bg);
|
||||
background-color: #181818;
|
||||
}
|
||||
|
||||
.rightPanel {
|
||||
.loginSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.logoText {
|
||||
|
||||
font-size: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
letter-spacing: -0.5px;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: 2rem;
|
||||
.logoPower {
|
||||
color: #E5E7EB;
|
||||
}
|
||||
|
||||
.logoOn {
|
||||
color: #F25843;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
|
|
@ -29,17 +44,24 @@
|
|||
}
|
||||
|
||||
.loginBox {
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
|
||||
|
||||
background-color: #181818;
|
||||
width: 25%;
|
||||
height: auto;
|
||||
|
||||
margin-top: 5%;
|
||||
padding: 2rem;
|
||||
|
||||
border-radius: 25px;
|
||||
border: 1px solid rgba(199, 197, 178, 0.15); /* washed-out color */
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
|
||||
0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
font-family: "DM Sans", sans-serif;
|
||||
color: #E5E7EB;
|
||||
}
|
||||
|
||||
.loginForm {
|
||||
|
|
@ -48,64 +70,142 @@
|
|||
gap: 1rem;
|
||||
}
|
||||
|
||||
.floatingLabelInput {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #C7C5B2;
|
||||
font-size: 1rem;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s ease;
|
||||
background-color: transparent;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.focusedLabel {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: -8px;
|
||||
transform: translateY(0);
|
||||
color: #F25843;
|
||||
font-size: 0.85rem;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s ease;
|
||||
background-color: #181818;
|
||||
padding: 0 4px;
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 8px;
|
||||
border-radius: 25px;
|
||||
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
background-color: #181818;
|
||||
color: #C7C5B2;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-secondary);
|
||||
box-shadow: 0 0 0 2px rgba(63, 81, 181, 0.1);
|
||||
border-color: #F25843;
|
||||
box-shadow: 0 0 0 2px rgba(242, 88, 67, 0.1);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--color-gray);
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* Fix browser autocomplete styling */
|
||||
.input:-webkit-autofill,
|
||||
.input:-webkit-autofill:hover,
|
||||
.input:-webkit-autofill:focus,
|
||||
.input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 30px #181818 inset !important;
|
||||
-webkit-text-fill-color: #E5E7EB !important;
|
||||
background-color: #181818 !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
/* Ensure label background matches when autofilled */
|
||||
.input:-webkit-autofill + .label,
|
||||
.input:-webkit-autofill + .focusedLabel {
|
||||
background-color: #181818 !important;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
font-size: 0.8rem;
|
||||
color: #E5E7EB;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
border-radius: 25px;
|
||||
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.primaryButton {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
.buttonContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.primaryButton:hover {
|
||||
.microsoftIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.loginButton {
|
||||
background-color: #F25843;
|
||||
color: #E5E7EB;
|
||||
}
|
||||
|
||||
.loginButton:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.microsoftButton {
|
||||
background-color: var(--color-text);
|
||||
color: var(--color-bg);
|
||||
background-color: #C7C5B2;
|
||||
color: #181818;
|
||||
}
|
||||
|
||||
.microsoftButton:hover {
|
||||
background-color: var(--color-gray);
|
||||
background-color: #D9D7C6;
|
||||
}
|
||||
|
||||
.googleButton {
|
||||
background-color: #C7C5B2;
|
||||
color: #181818;
|
||||
}
|
||||
|
||||
.googleButton:hover {
|
||||
background-color: #D9D7C6;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.divider::before,
|
||||
|
|
@ -117,9 +217,8 @@
|
|||
|
||||
.divider span {
|
||||
padding: 0 1rem;
|
||||
color: var(--color-gray);
|
||||
font-size: 0.9rem;
|
||||
font-family: var(--font-family);
|
||||
color: #E5E7EB;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.registerLink {
|
||||
|
|
@ -127,13 +226,11 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.registerLink span {
|
||||
color: var(--color-gray);
|
||||
font-size: 0.9rem;
|
||||
font-family: var(--font-family);
|
||||
color: #E5E7EB;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.textButton {
|
||||
|
|
@ -156,24 +253,12 @@ button:disabled {
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.rightContent {
|
||||
max-width: 80%;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.rightContent img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--color-red);
|
||||
background-color: var(--color-red-disabled);
|
||||
border: 1px solid var(--color-red);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue