feat: upload Excel via explorateur de fichier dans le VWB
- Bouton "Parcourir..." ouvre l'explorateur natif du navigateur - Upload vers /api/v3/upload-excel, sauvegarde dans data/uploads/ - Nom de table auto-suggéré depuis le nom du fichier Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ Auteur : Dom, Claude — 16 mars 2026
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
@@ -726,3 +727,51 @@ def get_dag_status(workflow_id: str):
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Upload Excel — explorateur de fichier côté navigateur
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UPLOAD_DIR = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||||
|
'data', 'uploads'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_v3_bp.route('/upload-excel', methods=['POST'])
|
||||||
|
def upload_excel():
|
||||||
|
"""Reçoit un fichier Excel uploadé depuis le navigateur.
|
||||||
|
|
||||||
|
Sauvegarde dans data/uploads/ et retourne le chemin serveur +
|
||||||
|
un nom de table suggéré basé sur le nom du fichier.
|
||||||
|
"""
|
||||||
|
if 'file' not in request.files:
|
||||||
|
return jsonify({'error': 'Aucun fichier reçu'}), 400
|
||||||
|
|
||||||
|
file = request.files['file']
|
||||||
|
if not file.filename:
|
||||||
|
return jsonify({'error': 'Nom de fichier vide'}), 400
|
||||||
|
|
||||||
|
# Vérifier l'extension
|
||||||
|
ext = os.path.splitext(file.filename)[1].lower()
|
||||||
|
if ext not in ('.xlsx', '.xls'):
|
||||||
|
return jsonify({'error': f'Format non supporté : {ext}'}), 400
|
||||||
|
|
||||||
|
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||||
|
save_path = os.path.join(UPLOAD_DIR, file.filename)
|
||||||
|
file.save(save_path)
|
||||||
|
|
||||||
|
# Nom de table suggéré à partir du nom du fichier
|
||||||
|
import re as _re
|
||||||
|
base = os.path.splitext(file.filename)[0]
|
||||||
|
suggested = _re.sub(r'[^a-zA-Z0-9_]', '_', base).strip('_').lower()
|
||||||
|
if suggested and suggested[0].isdigit():
|
||||||
|
suggested = 't_' + suggested
|
||||||
|
|
||||||
|
logger.info(f"Excel uploadé : {save_path} → table suggérée : {suggested}")
|
||||||
|
return jsonify({
|
||||||
|
'path': save_path,
|
||||||
|
'filename': file.filename,
|
||||||
|
'suggested_table': suggested,
|
||||||
|
})
|
||||||
|
|||||||
@@ -1033,13 +1033,55 @@ export default function PropertiesPanel({ step, onUpdateParams, onDelete }: Prop
|
|||||||
<span className="icon">📥</span> Import Excel
|
<span className="icon">📥</span> Import Excel
|
||||||
</div>
|
</div>
|
||||||
<div className="prop-field">
|
<div className="prop-field">
|
||||||
<label>Chemin du fichier Excel</label>
|
<label>Fichier Excel</label>
|
||||||
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={String(params.file_path || '')}
|
value={String(params.file_path || '')}
|
||||||
onChange={(e) => updateParam('file_path', e.target.value)}
|
onChange={(e) => updateParam('file_path', e.target.value)}
|
||||||
placeholder="/chemin/vers/fichier.xlsx"
|
placeholder="Cliquez sur Parcourir..."
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
<label style={{
|
||||||
|
padding: '6px 14px',
|
||||||
|
background: '#1976d2',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '13px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}>
|
||||||
|
Parcourir...
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/v3/upload-excel', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.path) {
|
||||||
|
updateParam('file_path', data.path);
|
||||||
|
if (!params.table_name) {
|
||||||
|
updateParam('table_name', data.suggested_table || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Upload failed:', err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small className="field-hint">Formats acceptés : .xlsx, .xls</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="prop-field">
|
<div className="prop-field">
|
||||||
<label>Nom de la table (optionnel)</label>
|
<label>Nom de la table (optionnel)</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user