Restaure et termine le lot tableur resté non committé. Il n'existait que dans un `git stash` (24 fichiers suivis) et en fichiers non suivis (menus.js, formula.js, color-picker.js, tests, fiches) : la 2.53.3 livrée ne le contenait donc pas. Le stash, créé avant le commit BUG-108, n'a jamais été restauré. #180 Redimensionnement au curseur (en-tête de bord miroité + classe de repaint), grille thémée sur les 15 thèmes et les modes contraste élevé / sépia, menu Fichier au niveau des onglets, barre épinglée pleine largeur. #181 Modèle de saisie Google Sheets : sélection ≠ édition (caret masqué sans quitter contenteditable, double-clic / F2 / première frappe qui remplace, Entrée contextuelle, Échap qui restaure) + inventaire priorisé des écarts. #182 Désélection fiable (clic simple sans dépendre du focus), contours de plage non empilés (sélecteur de classes sans point), couleurs texte/fond sur une plage, clic droit qui préserve la sélection multiple. #183 Panneau de couleurs façon Google Sheets : palette 8 × 10, STANDARD, PERSONNALISÉ, coche selon la luminance, sortie `#rrggbb` (une valeur HSL était rejetée par normHex). #184 Peinture de format complète (toutes propriétés, source sans format = réinitialisation), sélection multi-lignes/colonnes depuis les marges, grille étendue : colonnes A → Z d'emblée, lignes ajoutées PAR BLOCS DE 100 au défilement jusqu'à 1000 — matérialiser 1000 lignes d'un coup = ~26 000 cellules câblées par feuille, ce qui épuisait le tas de la suite JSDOM ; index de cellules `ref → td`, court-circuits formule/styles, marqueur data-wired. #185 Barre de menus : les 10 menus Google Sheets (161 entrées, 125 câblées, 36 annoncées indisponibles), ruban façon Sheets, grille unie 1 px dérivée du thème. #186 « Créer un fichier » propose .xlsx et construit un vrai classeur OPC (openpyxl) au lieu d'une charge utile texte illisible. Corrections trouvées en restaurant et en exerçant le lot : - fuite mémoire : écouteur `click` anonyme posé sur #content-area à chaque rendu, jamais retiré — sa fermeture retenait la grille précédente en entier ; - sorties Markdown / HTML / Imprimer de la barre de menus inertes (`data-xlsx-export` jamais réparti, seul `data-xlsx-action` l'était) ; - collision de classe `.xlsx-structure-menu` entre la barre de menus et le menu Structure de la barre d'outils (toute requête tombait sur un nœud masqué) ; - curseur col/row-resize absent quand le pointeur est sur la table elle-même ; - l'export emportait les lignes et colonnes vides du quadrillage (CSV, Markdown, HTML, impression) ; - clic extérieur avalé par la grâce de 250 ms du menu contextuel (destinée au seul appui long tactile) ; - une entrée indisponible laissait la barre de menus ouverte. Tests : pytest 1595 passés / 2 ignorés ; ruff et mypy 0 erreur ; bandit 0 ; 33 suites frontend vertes (xlsx-viewer 163/163, xlsx-menus 19/19, xlsx-formula 14/14) ; E2E complet 132 passés / 12 ignorés ; E2E xlsx-viewer 19/19.
96 lines
4.9 KiB
Markdown
96 lines
4.9 KiB
Markdown
# #186 — Création d'un fichier Excel depuis « Créer un fichier »
|
|
|
|
> **Statut :** ✅ livré le 2026-10-05 | **Effort :** 0,5 jour | **Impact :** 🟢
|
|
> **Références :** [Roadmap](../ROADMAP.md) · [Changelog](../../CHANGELOG.md) ·
|
|
> [Guide Recherche, PDF, Excel & Excalidraw](../GUIDES/RECHERCHE_PDF_EXCALIDRAW.md) ·
|
|
> [#153 — Visionneuse XLSX](./xlsx-viewer.md)
|
|
|
|
## 1. Constat
|
|
|
|
ObsiGate sait **lire et éditer** les classeurs depuis #152/#153, et l'assistant IA sait
|
|
en **produire** un (`create_xlsx`, #92). Mais la modale « Créer un fichier » — celle
|
|
du menu contextuel d'un vault / d'un dossier et de la palette de commandes —
|
|
ne proposait que des formats **texte** :
|
|
|
|
`.md`, `.txt`, `.py`, `.js`, `.json`, `.yaml`, `.sh`, `.ps1` (+ Excalidraw).
|
|
|
|
Démarrer un classeur depuis l'UI imposait donc de passer par l'IA (avec
|
|
confirmation) ou de déposer un fichier existant dans le vault. Le point de départ
|
|
naturel manquait alors que l'éditeur complet était disponible.
|
|
|
|
## 2. Livré (A1-A3)
|
|
|
|
### A1 — Option Excel dans la liste des types
|
|
|
|
`frontend/js/ui.js`, `FileOperations.showCreateFileModal()` : ajout de
|
|
`<option value=".xlsx">` dans le `<select id="file-ext-select">`, après `.json`
|
|
(ordre : formats texte, puis formats enrichis, comme `.excalidraw`).
|
|
|
|
Au passage, les **dix libellés** du sélecteur passent en i18n
|
|
(`modal.ext_md`, `modal.ext_txt`, …, `modal.ext_xlsx`) : ils étaient des chaînes
|
|
françaises codées en dur, ce qui exposait un texte non traduit en mode EN et
|
|
violait la règle « tout texte d'interface existe en FR et en EN ». Les deux
|
|
locales sont alignées (`tests/test_i18n_parity.py`).
|
|
|
|
Aucun changement pour les autres formats : le nom saisi sans extension reçoit
|
|
celle choisie (`name.endsWith(ext) ? name : name + ext`), donc un nom déjà
|
|
suffiché n'est jamais doublé.
|
|
|
|
### A2 — Un vrai classeur côté backend
|
|
|
|
Le point non évident : `POST /api/file/{vault}` **écrit du texte**
|
|
(`file_path.write_text(content)`), et la modale poste `content: ""`. Un `.xlsx`
|
|
n'est pas un fichier texte mais une **archive OPC** (un ZIP) : écrire la charge
|
|
utile vide produisait un fichier de 0 octet que ni `openpyxl`, ni la visionneuse,
|
|
ni Excel n'ouvrent — l'utilisateur aurait vu une erreur à l'ouverture, ou pire,
|
|
un fichier corrompu.
|
|
|
|
`backend/services/mutations.py` :
|
|
|
|
- `_empty_xlsx_bytes()` construit l'archive avec openpyxl (une feuille `Feuille1`,
|
|
le titre par défaut déjà utilisé par l'outil IA `create_xlsx`) — import paresseux,
|
|
comme le reste du module qui garde openpyxl optionnel à l'import ;
|
|
- `create_file()` écrit ces octets (`write_bytes`) pour cette extension et ignore
|
|
`content`, qui n'a pas de sens pour un classeur ; `size` renvoie le nombre
|
|
d'octets réellement écrits (et non `len(content)`, qui aurait valu 0) ;
|
|
- même garde-fou que `.excalidraw` : le squelette est injecté par le **service**,
|
|
donc la palette, le glisser-déposer, l'IA et l'API en bénéficient, pas seulement
|
|
la modale.
|
|
|
|
`openpyxl` reste importé à la demande : un vault sans tableur ne paie pas l'import.
|
|
|
|
### A3 — Tests
|
|
|
|
- `tests/test_api_main.py::TestFileCRUD` — `test_create_xlsx_empty_workbook` : le
|
|
fichier créé est relu par `openpyxl` (`sheetnames == ["Feuille1"]`) **et** servi
|
|
par `GET /api/file/{vault}` avec `is_xlsx: true` (donc lisible par la visionneuse) ;
|
|
`test_create_xlsx_in_new_subdir` couvre la création dans un dossier inexistant
|
|
(auto-créé). Non-régression : sans A2, l'archive serait vide et les deux tests
|
|
échoueraient.
|
|
- `tests/e2e/xlsx-viewer.spec.js` — bloc « création depuis l'UI » : l'option
|
|
`.xlsx` existe et son libellé vient des locales, la création passe par le menu
|
|
contextuel d'un dossier, et le classeur s'ouvre dans la grille (`.xlsx-table` +
|
|
onglet `Feuille1`) ; l'API est interrogée pour confirmer `is_xlsx`. Le fichier est
|
|
supprimé en fin de test (le run ne doit pas salir `test_vault/`).
|
|
|
|
## 3. Limites assumées
|
|
|
|
- **Le nom de feuille est `Feuille1`**, en dur. Un vrai *nouveau classeur* de
|
|
Google Sheets le demande ; ici le classeur s'ouvre sur sa seule feuille et
|
|
l'utilisateur la renomme via le menu Structure. La modale reste celle d'un
|
|
éditeur de texte, volontairement simple.
|
|
- **`.csv` reste absent de la liste**, alors que le backend l'accepte
|
|
(`SUPPORTED_EXTENSIONS`) et que la visionneuse sait l'éditer comme tableur
|
|
(#156-A14). L'écart est réel mais hors périmètre de #186 ; le même correctif
|
|
s'appliquerait (une ligne dans le `<select>`).
|
|
- **`frontend/app.js` (monolithe obsolète, non chargé par `index.html`)**
|
|
duplique l'ancienne liste sans `.xlsx`. Le code vivant est dans `frontend/js/`;
|
|
ce fichier n'a pas été touché.
|
|
- L'outil IA `create_xlsx` reste le chemin pour un classeur **rempli** : la modale
|
|
crée une grille vierge, comme un `.md` vierge.
|
|
|
|
## 4. Historique
|
|
|
|
| Date | Événement |
|
|
|---|---|
|
|
| 2026-10-05 | #186 ouvert puis livré dans la foulée : option `.xlsx` + squelette openpyxl dans `create_file()` | |