feat: import Excel → SQLite + boucle données → UI dans le VWB
- ExcelImporter : import .xlsx → SQLite auto (détection types, batch insert)
- DBIterator : lecture ligne par ligne avec filtre/tri/limite
- VWB actions : "Importer Excel" + "Pour chaque ligne" dans la palette
- DAG executor : pré-exécution import, boucle foreach avec injection
${current_row.colonne} dans les étapes dépendantes
- 36 tests unitaires Excel/DB (tous passent)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1025,6 +1025,94 @@ export default function PropertiesPanel({ step, onUpdateParams, onDelete }: Prop
|
||||
</>
|
||||
);
|
||||
|
||||
// === BOUCLE DONNÉES (Data Loop) ===
|
||||
case 'import_excel':
|
||||
return (
|
||||
<>
|
||||
<div className="prop-section-title">
|
||||
<span className="icon">📥</span> Import Excel
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Chemin du fichier Excel</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(params.file_path || '')}
|
||||
onChange={(e) => updateParam('file_path', e.target.value)}
|
||||
placeholder="/chemin/vers/fichier.xlsx"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Nom de la table (optionnel)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(params.table_name || '')}
|
||||
onChange={(e) => updateParam('table_name', e.target.value)}
|
||||
placeholder="Auto-detect depuis le nom du fichier"
|
||||
/>
|
||||
<small className="field-hint">Si vide, le nom de la table sera derive du nom du fichier Excel</small>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Feuille (optionnel)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(params.sheet_name || '')}
|
||||
onChange={(e) => updateParam('sheet_name', e.target.value)}
|
||||
placeholder="Premiere feuille par defaut"
|
||||
/>
|
||||
<small className="field-hint">Nom ou index (0, 1, 2...) de la feuille a importer</small>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'db_foreach':
|
||||
return (
|
||||
<>
|
||||
<div className="prop-section-title">
|
||||
<span className="icon">🔄</span> Boucle sur table
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Table</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(params.table_name || '')}
|
||||
onChange={(e) => updateParam('table_name', e.target.value)}
|
||||
placeholder="nom_table"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Filtre WHERE (optionnel)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(params.where_clause || '')}
|
||||
onChange={(e) => updateParam('where_clause', e.target.value)}
|
||||
placeholder="Ex: statut = 'actif' AND age > 18"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Tri ORDER BY (optionnel)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(params.order_by || '')}
|
||||
onChange={(e) => updateParam('order_by', e.target.value)}
|
||||
placeholder="Ex: nom ASC, date DESC"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Limite (optionnel)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={params.limit !== undefined && params.limit !== null ? Number(params.limit) : ''}
|
||||
onChange={(e) => updateParam('limit', e.target.value ? Number(e.target.value) : null)}
|
||||
min="1"
|
||||
placeholder="Toutes les lignes"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-info">
|
||||
Les colonnes sont accessibles via <code>{'${current_row.nom_colonne}'}</code> dans les etapes suivantes.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// === VALIDATION ===
|
||||
case 'verify_element_exists':
|
||||
return (
|
||||
|
||||
@@ -13,9 +13,11 @@ function StepNode({ data, selected }: StepNodeProps) {
|
||||
const step = data.step;
|
||||
const action = ACTIONS.find(a => a.type === step.action_type);
|
||||
const isConditional = step.action_type === 'visual_condition' || step.action_type === 'loop_visual';
|
||||
const isDataLoop = step.action_type === 'db_foreach';
|
||||
const isImport = step.action_type === 'import_excel';
|
||||
|
||||
return (
|
||||
<div className={`step-node ${selected ? 'selected' : ''} ${isConditional ? 'conditional' : ''}`}>
|
||||
<div className={`step-node ${selected ? 'selected' : ''} ${isConditional ? 'conditional' : ''} ${isDataLoop ? 'data-loop' : ''} ${isImport ? 'data-import' : ''}`}>
|
||||
{/* Entrée: haut */}
|
||||
<Handle
|
||||
type="target"
|
||||
@@ -62,6 +64,22 @@ function StepNode({ data, selected }: StepNodeProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aperçu import Excel */}
|
||||
{step.action_type === 'import_excel' && typeof step.parameters?.file_path === 'string' && step.parameters.file_path.length > 0 && (
|
||||
<div className="step-node-params">
|
||||
{`📄 ${String(step.parameters.file_path).split('/').pop()?.split('\\').pop() || String(step.parameters.file_path)}`}
|
||||
{step.parameters.table_name ? ` → ${String(step.parameters.table_name)}` : ''}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aperçu boucle db_foreach */}
|
||||
{step.action_type === 'db_foreach' && typeof step.parameters?.table_name === 'string' && step.parameters.table_name.length > 0 && (
|
||||
<div className="step-node-params">
|
||||
{`🗃️ ${String(step.parameters.table_name)}`}
|
||||
{step.parameters.limit ? ` (max ${String(step.parameters.limit)})` : ''}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!step.anchor_id && action?.needsAnchor && (
|
||||
<div className="step-node-warning">
|
||||
Ancre requise
|
||||
|
||||
@@ -47,6 +47,8 @@ export type ActionType =
|
||||
| 'ai_custom'
|
||||
| 'db_save_data'
|
||||
| 'db_read_data'
|
||||
| 'import_excel'
|
||||
| 'db_foreach'
|
||||
| 'verify_element_exists'
|
||||
| 'verify_text_content'
|
||||
// === DAG LLM — étapes IA exécutées via le DAGExecutor ===
|
||||
@@ -104,6 +106,10 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
{ type: 'db_save_data', label: 'Sauvegarder en BDD', icon: '💿', category: 'data', needsAnchor: false, params: ['table', 'data'] },
|
||||
{ type: 'db_read_data', label: 'Lire depuis BDD', icon: '📖', category: 'data', needsAnchor: false, params: ['query', 'variable_name'] },
|
||||
|
||||
// === BOUCLE DONNÉES (Data Loop) ===
|
||||
{ type: 'import_excel', label: 'Importer Excel', icon: '📥', category: 'data', needsAnchor: false, params: ['file_path', 'table_name', 'sheet_name'] },
|
||||
{ type: 'db_foreach', label: 'Pour chaque ligne', icon: '🔄', category: 'data', needsAnchor: false, params: ['table_name', 'where_clause', 'order_by', 'limit'] },
|
||||
|
||||
// === DAG LLM — Actions IA via DAGExecutor (parallèle, Ollama) ===
|
||||
{ type: 'llm_analyze', label: 'Analyser texte', icon: '🔬', category: 'llm', needsAnchor: false, params: ['text', 'instruction', 'model'] },
|
||||
{ type: 'llm_translate', label: 'Traduire', icon: '🌐', category: 'llm', needsAnchor: false, params: ['text', 'target_lang', 'model'] },
|
||||
|
||||
Reference in New Issue
Block a user