- Ajout onglets Standard/VWB dans Executor pour permettre la navigation - Liste d'exécution scrollable (max 300px) - Synchronisation bidirectionnelle des variables avec le workflow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
630 lines
22 KiB
TypeScript
630 lines
22 KiB
TypeScript
/**
|
|
* Application principale Visual Workflow Builder V2
|
|
* Auteur : Dom, Alice, Kiro - 09 janvier 2026
|
|
*
|
|
* Interface moderne pour la création de workflows d'automatisation RPA
|
|
* avec sélection visuelle basée sur la vision et terminologie française.
|
|
*/
|
|
|
|
import { useState, useCallback, useMemo, useEffect, Component, ErrorInfo, ReactNode } from 'react';
|
|
import {
|
|
Box,
|
|
CssBaseline,
|
|
ThemeProvider,
|
|
createTheme,
|
|
AppBar,
|
|
Toolbar,
|
|
Typography,
|
|
Tabs,
|
|
Tab,
|
|
Drawer,
|
|
Alert,
|
|
AlertTitle,
|
|
Button,
|
|
} from '@mui/material';
|
|
import { Provider } from 'react-redux';
|
|
import { ReactFlowProvider } from '@xyflow/react';
|
|
import { store } from './store';
|
|
|
|
// Composants principaux
|
|
import Canvas from './components/Canvas';
|
|
import Palette from './components/Palette';
|
|
import PropertiesPanel from './components/PropertiesPanel';
|
|
import VariableManager from './components/VariableManager';
|
|
import DocumentationTab from './components/DocumentationTab';
|
|
import Validator from './components/Validator';
|
|
import Executor from './components/Executor';
|
|
import WorkflowManager from './components/WorkflowManager';
|
|
import ContextualHelp from './components/ContextualHelp';
|
|
import KeyboardShortcuts from './components/KeyboardShortcuts';
|
|
import AccessibilityProvider from './components/AccessibilityProvider';
|
|
import ConnectionIndicator from './components/ConnectionIndicator';
|
|
import TestCatalogLoader from './components/TestCatalogLoader';
|
|
import VWBIntegrationTest from './components/VWBIntegrationTest';
|
|
|
|
// Hooks personnalisés
|
|
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation';
|
|
import { useResponsiveLayout } from './hooks/useResponsiveLayout';
|
|
|
|
// Import des types partagés
|
|
import {
|
|
Step,
|
|
Variable,
|
|
StepTemplate,
|
|
StepCategory,
|
|
Position,
|
|
Workflow,
|
|
WorkflowConnection,
|
|
StepExecutionState,
|
|
} from './types';
|
|
|
|
// Error Boundary pour gérer les erreurs de l'application
|
|
interface ErrorBoundaryState {
|
|
hasError: boolean;
|
|
error?: Error;
|
|
errorInfo?: ErrorInfo;
|
|
}
|
|
|
|
class AppErrorBoundary extends Component<
|
|
{ children: ReactNode },
|
|
ErrorBoundaryState
|
|
> {
|
|
constructor(props: { children: ReactNode }) {
|
|
super(props);
|
|
this.state = { hasError: false };
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
// Met à jour l'état pour afficher l'interface d'erreur
|
|
return { hasError: true, error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
// Log l'erreur pour le debugging
|
|
console.error('Erreur capturée par Error Boundary:', error, errorInfo);
|
|
this.setState({
|
|
error,
|
|
errorInfo,
|
|
});
|
|
}
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
return (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
minHeight: '100vh',
|
|
p: 3,
|
|
}}
|
|
>
|
|
<Alert severity="error" sx={{ maxWidth: 600, mb: 2 }}>
|
|
<AlertTitle>Une erreur inattendue s'est produite</AlertTitle>
|
|
<Typography variant="body2" gutterBottom>
|
|
L'application a rencontré une erreur et ne peut pas continuer.
|
|
</Typography>
|
|
{this.state.error && (
|
|
<Typography variant="caption" component="pre" sx={{ mt: 1 }}>
|
|
{this.state.error.message}
|
|
</Typography>
|
|
)}
|
|
</Alert>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => window.location.reload()}
|
|
sx={{ mt: 2 }}
|
|
>
|
|
Recharger l'application
|
|
</Button>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
// Thème Material-UI personnalisé avec couleurs françaises - mémorisé pour éviter les re-créations
|
|
const theme = createTheme({
|
|
palette: {
|
|
primary: {
|
|
main: '#1976d2', // Bleu français
|
|
},
|
|
secondary: {
|
|
main: '#dc004e', // Rouge français
|
|
},
|
|
background: {
|
|
default: '#fafafa',
|
|
paper: '#ffffff',
|
|
},
|
|
},
|
|
typography: {
|
|
fontFamily: '"Segoe UI", "Roboto", "Helvetica", "Arial", sans-serif',
|
|
},
|
|
components: {
|
|
MuiAppBar: {
|
|
styleOverrides: {
|
|
root: {
|
|
backgroundColor: '#ffffff',
|
|
color: '#1976d2',
|
|
boxShadow: '0 1px 3px rgba(0,0,0,0.12)',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Composant principal de l'application
|
|
*/
|
|
function App() {
|
|
// États principaux
|
|
const [selectedStep, setSelectedStep] = useState<Step | null>(null);
|
|
const [variables, setVariables] = useState<Variable[]>([]);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [activeDocTab, setActiveDocTab] = useState(0);
|
|
const [isDocDrawerOpen, setIsDocDrawerOpen] = useState(false);
|
|
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false);
|
|
|
|
// Hooks d'accessibilité et responsivité
|
|
const responsiveLayout = useResponsiveLayout();
|
|
|
|
// État du workflow
|
|
const [workflow, setWorkflow] = useState<Workflow>({
|
|
id: 'workflow_1',
|
|
name: 'Nouveau Workflow',
|
|
description: 'Workflow créé avec le Visual Workflow Builder V2',
|
|
steps: [],
|
|
connections: [],
|
|
variables: [],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
// Configuration de la navigation au clavier
|
|
const keyboardNavigation = useKeyboardNavigation({
|
|
onStepSelect: (stepId: string) => {
|
|
const step = workflow.steps.find(s => s.id === stepId);
|
|
if (step) handleStepSelect(step);
|
|
},
|
|
onStepMove: (stepId: string, direction: 'up' | 'down' | 'left' | 'right') => {
|
|
const step = workflow.steps.find(s => s.id === stepId);
|
|
if (step) {
|
|
const moveDistance = 20;
|
|
const newPosition = { ...step.position };
|
|
|
|
switch (direction) {
|
|
case 'up': newPosition.y -= moveDistance; break;
|
|
case 'down': newPosition.y += moveDistance; break;
|
|
case 'left': newPosition.x -= moveDistance; break;
|
|
case 'right': newPosition.x += moveDistance; break;
|
|
}
|
|
|
|
handleStepMove(stepId, newPosition);
|
|
}
|
|
},
|
|
onStepDelete: (stepId: string) => handleStepDelete(stepId),
|
|
onSave: () => console.log('Sauvegarde via clavier'),
|
|
onHelp: () => setShowKeyboardShortcuts(true),
|
|
selectedStepId: selectedStep?.id,
|
|
availableStepIds: workflow.steps.map(s => s.id),
|
|
isEnabled: true,
|
|
});
|
|
|
|
// Gestionnaires d'événements optimisés avec useCallback
|
|
const handleStepSelect = useCallback((step: Step | null) => {
|
|
setSelectedStep(step);
|
|
}, []);
|
|
|
|
// Synchroniser selectedStep avec le workflow quand celui-ci change
|
|
// Ceci corrige le bug où les paramètres (visual_anchor) ne sont pas persistés dans l'UI
|
|
useEffect(() => {
|
|
if (selectedStep) {
|
|
const updatedStep = workflow.steps.find(s => s.id === selectedStep.id);
|
|
if (updatedStep) {
|
|
// Vérifier si les données ont changé avant de mettre à jour
|
|
const currentDataStr = JSON.stringify(selectedStep.data);
|
|
const updatedDataStr = JSON.stringify(updatedStep.data);
|
|
if (currentDataStr !== updatedDataStr) {
|
|
setSelectedStep(updatedStep);
|
|
}
|
|
}
|
|
}
|
|
}, [workflow.steps, selectedStep?.id]);
|
|
|
|
const handleStepMove = useCallback((stepId: string, position: Position) => {
|
|
console.log('Déplacement étape:', stepId, position);
|
|
setWorkflow(prev => ({
|
|
...prev,
|
|
steps: prev.steps.map(step =>
|
|
step.id === stepId ? { ...step, position } : step
|
|
),
|
|
updatedAt: new Date(),
|
|
}));
|
|
}, []);
|
|
|
|
const handleConnection = (source: string, target: string) => {
|
|
console.log('Connexion:', source, '->', target);
|
|
const newConnection: WorkflowConnection = {
|
|
id: `conn_${Date.now()}`,
|
|
source,
|
|
target,
|
|
};
|
|
setWorkflow(prev => ({
|
|
...prev,
|
|
connections: [...prev.connections, newConnection],
|
|
updatedAt: new Date(),
|
|
}));
|
|
};
|
|
|
|
const handleStepAdd = (stepData: Omit<Step, 'id'>) => {
|
|
console.log('Ajout étape:', stepData);
|
|
const newStep: Step = {
|
|
...stepData,
|
|
id: `step_${Date.now()}`,
|
|
executionState: StepExecutionState.IDLE,
|
|
validationErrors: [],
|
|
};
|
|
|
|
setWorkflow(prev => ({
|
|
...prev,
|
|
steps: [...prev.steps, newStep],
|
|
updatedAt: new Date(),
|
|
}));
|
|
};
|
|
|
|
const handleStepDelete = (stepId: string) => {
|
|
console.log('Suppression étape:', stepId);
|
|
setWorkflow(prev => ({
|
|
...prev,
|
|
steps: prev.steps.filter(step => step.id !== stepId),
|
|
connections: prev.connections.filter(conn =>
|
|
conn.source !== stepId && conn.target !== stepId
|
|
),
|
|
updatedAt: new Date(),
|
|
}));
|
|
|
|
// Désélectionner l'étape si elle était sélectionnée
|
|
if (selectedStep?.id === stepId) {
|
|
setSelectedStep(null);
|
|
}
|
|
};
|
|
|
|
const handleConnectionDelete = (connectionId: string) => {
|
|
console.log('Suppression connexion:', connectionId);
|
|
setWorkflow(prev => ({
|
|
...prev,
|
|
connections: prev.connections.filter(conn => conn.id !== connectionId),
|
|
updatedAt: new Date(),
|
|
}));
|
|
};
|
|
|
|
const handleParameterChange = (stepId: string, param: string, value: any) => {
|
|
console.log('Changement paramètre:', stepId, param, value);
|
|
setWorkflow(prev => ({
|
|
...prev,
|
|
steps: prev.steps.map(step =>
|
|
step.id === stepId
|
|
? {
|
|
...step,
|
|
data: {
|
|
...step.data,
|
|
parameters: {
|
|
...step.data.parameters,
|
|
[param]: value
|
|
}
|
|
}
|
|
}
|
|
: step
|
|
),
|
|
updatedAt: new Date(),
|
|
}));
|
|
};
|
|
|
|
const handleVisualSelection = (stepId: string) => {
|
|
console.log('Sélection visuelle pour étape:', stepId);
|
|
// La sélection visuelle est maintenant gérée par le VisualSelector dans PropertiesPanel
|
|
};
|
|
|
|
const handleStepHighlight = (stepId: string, highlight: boolean) => {
|
|
// Mise en évidence des étapes pour la validation
|
|
console.log('Mise en évidence étape:', stepId, highlight ? 'activée' : 'désactivée');
|
|
};
|
|
|
|
const handleStepStateChange = (stepId: string, state: StepExecutionState) => {
|
|
setWorkflow(prev => ({
|
|
...prev,
|
|
steps: prev.steps.map(step =>
|
|
step.id === stepId ? { ...step, executionState: state } : step
|
|
),
|
|
updatedAt: new Date(),
|
|
}));
|
|
};
|
|
|
|
const handleExecutionComplete = (success: boolean, summary: any) => {
|
|
console.log('Exécution terminée:', success ? 'Succès' : 'Échec', summary);
|
|
};
|
|
|
|
const handleWorkflowLoad = (loadedWorkflow: Workflow) => {
|
|
console.log('📥 [App] Chargement workflow:', {
|
|
name: loadedWorkflow.name,
|
|
stepsCount: loadedWorkflow.steps?.length || 0,
|
|
connectionsCount: loadedWorkflow.connections?.length || 0,
|
|
steps: loadedWorkflow.steps,
|
|
});
|
|
|
|
// S'assurer que les étapes ont des positions valides
|
|
const workflowWithPositions: Workflow = {
|
|
...loadedWorkflow,
|
|
steps: (loadedWorkflow.steps || []).map((step, index) => ({
|
|
...step,
|
|
position: step.position || { x: 100 + (index % 3) * 200, y: 100 + Math.floor(index / 3) * 150 },
|
|
})),
|
|
};
|
|
|
|
setWorkflow(workflowWithPositions);
|
|
// Charger les variables du workflow
|
|
setVariables(workflowWithPositions.variables || []);
|
|
setSelectedStep(null);
|
|
console.log('✅ [App] Workflow appliqué avec', workflowWithPositions.steps.length, 'étapes et', (workflowWithPositions.variables || []).length, 'variables');
|
|
};
|
|
|
|
const handleWorkflowSave = (savedWorkflow: Workflow) => {
|
|
setWorkflow(savedWorkflow);
|
|
console.log('Workflow sauvegardé:', savedWorkflow.name);
|
|
};
|
|
|
|
const handleStepDrag = (stepTemplate: StepTemplate) => {
|
|
console.log('Début drag étape:', stepTemplate);
|
|
// Cette fonction est appelée quand le drag commence depuis la palette
|
|
// La logique de drop est gérée dans le Canvas
|
|
};
|
|
|
|
// Gestion des variables
|
|
const handleVariableCreate = (variable: Omit<Variable, 'id'>) => {
|
|
const newVariable: Variable = {
|
|
...variable,
|
|
id: `var_${Date.now()}`,
|
|
};
|
|
const updatedVariables = [...variables, newVariable];
|
|
setVariables(updatedVariables);
|
|
// Synchroniser avec le workflow pour la sauvegarde
|
|
setWorkflow(prev => ({ ...prev, variables: updatedVariables, updatedAt: new Date() }));
|
|
};
|
|
|
|
const handleVariableUpdate = (id: string, updates: Partial<Variable>) => {
|
|
const updatedVariables = variables.map(v => v.id === id ? { ...v, ...updates } : v);
|
|
setVariables(updatedVariables);
|
|
// Synchroniser avec le workflow pour la sauvegarde
|
|
setWorkflow(prev => ({ ...prev, variables: updatedVariables, updatedAt: new Date() }));
|
|
};
|
|
|
|
const handleVariableDelete = (id: string) => {
|
|
const updatedVariables = variables.filter(v => v.id !== id);
|
|
setVariables(updatedVariables);
|
|
// Synchroniser avec le workflow pour la sauvegarde
|
|
setWorkflow(prev => ({ ...prev, variables: updatedVariables, updatedAt: new Date() }));
|
|
};
|
|
|
|
// Onglets de documentation
|
|
const docTabs = [
|
|
{ id: 'canvas', label: 'Canvas', toolName: 'canvas' },
|
|
{ id: 'palette', label: 'Palette', toolName: 'palette' },
|
|
{ id: 'properties', label: 'Propriétés', toolName: 'properties' },
|
|
{ id: 'test', label: 'Test Catalogue', toolName: 'test' },
|
|
{ id: 'vwb-test', label: 'Test VWB', toolName: 'vwb-test' },
|
|
];
|
|
|
|
// Mode 100% Visuel - Pas de catégories par défaut
|
|
// Seules les actions VisionOnly du catalogue sont utilisées
|
|
const defaultCategories: StepCategory[] = [];
|
|
|
|
return (
|
|
<AppErrorBoundary>
|
|
<AccessibilityProvider>
|
|
<Provider store={store}>
|
|
<ThemeProvider theme={theme}>
|
|
<ReactFlowProvider>
|
|
<CssBaseline />
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
|
{/* Barre d'application */}
|
|
<AppBar position="static" elevation={1}>
|
|
<Toolbar>
|
|
<Typography variant="h6" component="h1" sx={{ flexGrow: 1 }}>
|
|
Visual Workflow Builder V2
|
|
</Typography>
|
|
|
|
{/* Gestionnaire de workflows */}
|
|
<Box sx={{ mr: 2 }}>
|
|
<WorkflowManager
|
|
currentWorkflow={workflow}
|
|
onWorkflowLoad={handleWorkflowLoad}
|
|
onWorkflowSave={handleWorkflowSave}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Indicateur de connexion API */}
|
|
<Box sx={{ mr: 2 }}>
|
|
<ConnectionIndicator compact showRefreshButton />
|
|
</Box>
|
|
|
|
<Tabs
|
|
value={activeDocTab}
|
|
onChange={(_, newValue) => {
|
|
setActiveDocTab(newValue);
|
|
setIsDocDrawerOpen(true);
|
|
}}
|
|
textColor="inherit"
|
|
indicatorColor="secondary"
|
|
>
|
|
<Tab label="Aide Canvas" />
|
|
<Tab label="Aide Palette" />
|
|
<Tab label="Aide Propriétés" />
|
|
<Tab label="Test Catalogue" />
|
|
<Tab label="Test VWB" />
|
|
</Tabs>
|
|
</Toolbar>
|
|
</AppBar>
|
|
|
|
{/* Interface principale */}
|
|
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
|
{/* Palette d'étapes - Responsive */}
|
|
<Box
|
|
sx={responsiveLayout.getResponsiveStyles('palette')}
|
|
role="complementary"
|
|
aria-label="Palette d'étapes disponibles"
|
|
>
|
|
<Palette
|
|
categories={defaultCategories}
|
|
searchTerm={searchTerm}
|
|
onSearch={setSearchTerm}
|
|
onStepDrag={handleStepDrag}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Canvas principal */}
|
|
<Box
|
|
sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
|
role="main"
|
|
aria-label="Zone principale de création de workflow"
|
|
>
|
|
{/* Validateur et Exécuteur en haut */}
|
|
<Box sx={{
|
|
display: 'flex',
|
|
borderBottom: '1px solid #e0e0e0',
|
|
backgroundColor: '#ffffff',
|
|
flexDirection: responsiveLayout.isMobile ? 'column' : 'row'
|
|
}}
|
|
role="toolbar"
|
|
aria-label="Outils de validation et d'exécution"
|
|
>
|
|
{/* Validateur */}
|
|
<Box sx={{
|
|
flex: 1,
|
|
p: responsiveLayout.isMobile ? 1 : 2,
|
|
borderRight: responsiveLayout.isMobile ? 'none' : '1px solid #e0e0e0',
|
|
borderBottom: responsiveLayout.isMobile ? '1px solid #e0e0e0' : 'none'
|
|
}}
|
|
role="region"
|
|
aria-label="Validation du workflow"
|
|
>
|
|
<Validator
|
|
workflow={workflow}
|
|
variables={variables}
|
|
onStepHighlight={handleStepHighlight}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Exécuteur */}
|
|
<Box
|
|
sx={{ flex: 1, p: responsiveLayout.isMobile ? 1 : 2 }}
|
|
role="region"
|
|
aria-label="Exécution du workflow"
|
|
>
|
|
<Executor
|
|
workflow={workflow}
|
|
canExecute={workflow.steps.length > 0}
|
|
onStepStateChange={handleStepStateChange}
|
|
onExecutionComplete={handleExecutionComplete}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Canvas
|
|
workflow={workflow}
|
|
selectedStep={selectedStep}
|
|
onStepSelect={handleStepSelect}
|
|
onStepMove={handleStepMove}
|
|
onConnection={handleConnection}
|
|
onConnectionDelete={handleConnectionDelete}
|
|
onStepAdd={handleStepAdd}
|
|
onStepDelete={handleStepDelete}
|
|
/>
|
|
|
|
{/* Gestionnaire de variables - Responsive */}
|
|
<Box sx={{
|
|
...responsiveLayout.getResponsiveStyles('variables'),
|
|
borderTop: '1px solid #e0e0e0',
|
|
backgroundColor: '#ffffff',
|
|
p: responsiveLayout.isMobile ? 1 : 2,
|
|
overflow: 'auto',
|
|
}}
|
|
role="region"
|
|
aria-label="Gestionnaire de variables"
|
|
>
|
|
<VariableManager
|
|
variables={variables}
|
|
onVariableCreate={handleVariableCreate}
|
|
onVariableUpdate={handleVariableUpdate}
|
|
onVariableDelete={handleVariableDelete}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Panneau de propriétés - Responsive */}
|
|
<Box
|
|
sx={responsiveLayout.getResponsiveStyles('properties')}
|
|
role="complementary"
|
|
aria-label="Panneau de propriétés de l'étape sélectionnée"
|
|
>
|
|
<PropertiesPanel
|
|
selectedStep={selectedStep}
|
|
variables={variables}
|
|
onParameterChange={handleParameterChange}
|
|
onVisualSelection={handleVisualSelection}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Tiroir de documentation */}
|
|
<Drawer
|
|
anchor="right"
|
|
open={isDocDrawerOpen}
|
|
onClose={() => setIsDocDrawerOpen(false)}
|
|
sx={{
|
|
'& .MuiDrawer-paper': {
|
|
width: responsiveLayout.isMobile ? '100%' : 400,
|
|
boxSizing: 'border-box',
|
|
},
|
|
}}
|
|
>
|
|
{activeDocTab === 3 ? (
|
|
<TestCatalogLoader />
|
|
) : activeDocTab === 4 ? (
|
|
<VWBIntegrationTest />
|
|
) : (
|
|
<DocumentationTab
|
|
toolName={docTabs[activeDocTab]?.toolName || 'canvas'}
|
|
isActive={isDocDrawerOpen}
|
|
onActivate={() => setIsDocDrawerOpen(true)}
|
|
/>
|
|
)}
|
|
</Drawer>
|
|
|
|
{/* Aide contextuelle */}
|
|
<ContextualHelp
|
|
context={selectedStep ? 'properties' : 'canvas'}
|
|
selectedStepType={selectedStep?.type}
|
|
currentErrors={[]} // TODO: Intégrer avec le validateur
|
|
isVisible={!responsiveLayout.isMobile} // Masquer sur mobile pour éviter l'encombrement
|
|
/>
|
|
|
|
{/* Dialogue des raccourcis clavier */}
|
|
<KeyboardShortcuts
|
|
open={showKeyboardShortcuts}
|
|
onClose={() => setShowKeyboardShortcuts(false)}
|
|
shortcuts={keyboardNavigation.shortcuts}
|
|
/>
|
|
</Box>
|
|
</ReactFlowProvider>
|
|
</ThemeProvider>
|
|
</Provider>
|
|
</AccessibilityProvider>
|
|
</AppErrorBoundary>
|
|
);
|
|
}
|
|
|
|
export default App; |