refactor: extraction des modules xlsx, lien dashboard-grille et inspecteur redimensionnable #154
This commit is contained in:
+19
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
|||||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.43.0**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.43.1**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,24 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.43.1] — 2026-09-29
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
|
||||||
|
- **#154 — Refonte UI/UX de la visionneuse XLSX (Lot 5, clôture) : extraction modulaire et
|
||||||
|
finitions.**
|
||||||
|
Les unités **sans état** de la visionneuse sont extraites dans `frontend/js/xlsx/` :
|
||||||
|
`refs.js` (références A1), `command-bar.js` (onglets + ruban + pastilles d'état) et
|
||||||
|
`dashboard.js` (rendu du tableau de bord) — le noyau avec état reste dans `viewer.js` à
|
||||||
|
comportement constant. Le tableau de bord devient interactif : **cliquer une plage nommée
|
||||||
|
sélectionne et révèle sa première cellule** dans la grille (changement d'onglet si nécessaire).
|
||||||
|
L'inspecteur est **redimensionnable** (largeur mémorisée par session). `SW_VERSION` passe à
|
||||||
|
`v28` et les nouveaux modules entrent dans le pré-cache. Tests JSDOM
|
||||||
|
`tests/frontend/xlsx-viewer.test.mjs` (51) et E2E `tests/e2e/xlsx-viewer.spec.js` (9) verts.
|
||||||
|
Aucun changement backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.43.0] — 2026-09-29
|
## [2.43.0] — 2026-09-29
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -976,8 +976,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.43.0).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.43.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.43.0 | Dernière mise à jour : Septembre 2026*
|
*Projet : ObsiGate | Version : 2.43.1 | Dernière mise à jour : Septembre 2026*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1151,8 +1151,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.43.0).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.43.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.43.0 | Last updated: September 2026*
|
*Project: ObsiGate | Version: 2.43.1 | Last updated: September 2026*
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.43.0"
|
version = "2.43.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.43.0"
|
version = "2.43.1"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.43.0",
|
"version": "2.43.1",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
@@ -244,7 +244,9 @@ latéral droit** (la grille reste visible à côté) qui liste les plages
|
|||||||
nommées du classeur (nom, référence, portée), signale les feuilles
|
nommées du classeur (nom, référence, portée), signale les feuilles
|
||||||
contenant des graphiques ou des tableaux croisés, et donne pour chaque
|
contenant des graphiques ou des tableaux croisés, et donne pour chaque
|
||||||
feuille un résumé (cellules, lignes, colonnes, formules, valeurs
|
feuille un résumé (cellules, lignes, colonnes, formules, valeurs
|
||||||
numériques) avec quelques chiffres clés. L'en-tête de l'inspecteur offre
|
numériques) avec quelques chiffres clés. Cliquer une **plage nommée**
|
||||||
|
sélectionne sa première cellule dans la grille, et le panneau est
|
||||||
|
**redimensionnable**. L'en-tête de l'inspecteur offre
|
||||||
aussi un accès direct à l'**assistant IA**, qui peut ensuite exploiter ces
|
aussi un accès direct à l'**assistant IA**, qui peut ensuite exploiter ces
|
||||||
plages (outils `list_xlsx_sheets`, `xlsx_to_markdown`, `update_xlsx_cells`,
|
plages (outils `list_xlsx_sheets`, `xlsx_to_markdown`, `update_xlsx_cells`,
|
||||||
`append_xlsx_rows`).
|
`append_xlsx_rows`).
|
||||||
|
|||||||
+12
-12
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.43.0 | **Dernière mise à jour :** 2026-09-29
|
> **Version :** 2.43.1 | **Dernière mise à jour :** 2026-09-29
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
@@ -97,13 +97,12 @@
|
|||||||
### 154. Refonte UI/UX de la visionneuse & éditeur XLSX (ruban, grille, inspecteur)
|
### 154. Refonte UI/UX de la visionneuse & éditeur XLSX (ruban, grille, inspecteur)
|
||||||
|
|
||||||
- **Effort :** 6-9 jours (Lot 1 ✅ · Lot 2 · Lot 3 · Lot 4) | **Impact :** 🟡
|
- **Effort :** 6-9 jours (Lot 1 ✅ · Lot 2 · Lot 3 · Lot 4) | **Impact :** 🟡
|
||||||
- **Statut :** 🔵 **en cours** — **Lots 1, 2, 3 & 4 livrés le 2026-09-29** (ruban de commandes groupé,
|
- **Statut :** ✅ **livré le 2026-09-29 (Lots 1 → 5, A1-A5)** — ruban de commandes groupé, onglets
|
||||||
onglets de feuilles permanents avec bouton « + », badges d'état lecture seule / formules non
|
de feuilles permanents avec bouton « + », badges d'état lecture seule / formules non recalculées,
|
||||||
recalculées, tokens de grille et affordances ; dialogues thémés `showConfirm`/`showPrompt`,
|
tokens de grille et affordances ; dialogues thémés `showConfirm`/`showPrompt`, bandeau de conflit
|
||||||
bandeau de conflit 409 non bloquant, indicateur *dirty* ; **inspecteur droit repliable** pour le
|
409 non bloquant, indicateur *dirty* ; **inspecteur droit repliable** (tableau de bord + entrée
|
||||||
tableau de bord avec entrée Assistant IA ; **undo/redo**, chargement via `IntersectionObserver`,
|
Assistant IA, redimensionnable) ; **undo/redo**, chargement via `IntersectionObserver`,
|
||||||
ARIA `role="grid"`). Reste **A5** : découpage modulaire (`frontend/js/xlsx/*`) et finitions
|
ARIA `role="grid"` ; extraction des unités sans état dans `frontend/js/xlsx/*`.
|
||||||
(lien dashboard → grille, inspecteur redimensionnable, split).
|
|
||||||
- **Analyse, architecture cible et plan par lots :** [features/xlsx-ui-redesign.md](./features/xlsx-ui-redesign.md)
|
- **Analyse, architecture cible et plan par lots :** [features/xlsx-ui-redesign.md](./features/xlsx-ui-redesign.md)
|
||||||
- **Description :** la visionneuse XLSX (#152/#153) est fonctionnelle mais peu conviviale :
|
- **Description :** la visionneuse XLSX (#152/#153) est fonctionnelle mais peu conviviale :
|
||||||
commandes à plat sans hiérarchie, en-têtes de grille indistincts des cellules, états avancés
|
commandes à plat sans hiérarchie, en-têtes de grille indistincts des cellules, états avancés
|
||||||
@@ -116,7 +115,7 @@
|
|||||||
- [x] **A2** Dialogues thémés (modales + toasts) et feedback non bloquant des conflits 409 (Lot 2)
|
- [x] **A2** Dialogues thémés (modales + toasts) et feedback non bloquant des conflits 409 (Lot 2)
|
||||||
- [x] **A3** Inspecteur droit repliable : Tableau de bord + entrée Assistant IA (Lot 3)
|
- [x] **A3** Inspecteur droit repliable : Tableau de bord + entrée Assistant IA (Lot 3)
|
||||||
- [x] **A4** Undo/redo, chargement via `IntersectionObserver`, sémantique ARIA (Lot 4)
|
- [x] **A4** Undo/redo, chargement via `IntersectionObserver`, sémantique ARIA (Lot 4)
|
||||||
- [ ] **A5** Découpage `frontend/js/xlsx/*` + lien dashboard → grille, inspecteur redimensionnable, split
|
- [x] **A5** Découpage `frontend/js/xlsx/*`, lien dashboard → grille, inspecteur redimensionnable (Lot 5)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -168,6 +167,7 @@
|
|||||||
| # | Domaine / fonctionnalité | Version | Détails |
|
| # | Domaine / fonctionnalité | Version | Détails |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 152 | Viewer XLSX — affichage multi-feuilles, édition des cellules, téléchargement | 2.27.0 | [archive](./archive/COMPLETED_v1-v2.md) |
|
| 152 | Viewer XLSX — affichage multi-feuilles, édition des cellules, téléchargement | 2.27.0 | [archive](./archive/COMPLETED_v1-v2.md) |
|
||||||
|
| 154 | Tableur — Refonte UI/UX (ruban groupé, onglets permanents, badges d'état, inspecteur droit, undo/redo) | 2.40.0→2.44.0 | [features/xlsx-ui-redesign.md](./features/xlsx-ui-redesign.md) |
|
||||||
| BUG-047 | Versionnage — source unique `VERSION` + bump SemVer automatique au commit (hooks + tag) | 2.3.0 | [DEVELOPMENT_AND_RELEASES.md](./DEVELOPMENT_AND_RELEASES.md) |
|
| BUG-047 | Versionnage — source unique `VERSION` + bump SemVer automatique au commit (hooks + tag) | 2.3.0 | [DEVELOPMENT_AND_RELEASES.md](./DEVELOPMENT_AND_RELEASES.md) |
|
||||||
| 90 | Barre d'actions du document — regroupement fonctionnel + spacers | 2.3.0 | [archive](./archive/COMPLETED_v1-v2.md) |
|
| 90 | Barre d'actions du document — regroupement fonctionnel + spacers | 2.3.0 | [archive](./archive/COMPLETED_v1-v2.md) |
|
||||||
| 89 | Drag & drop complet de fichiers/dossiers & intégration Assistant IA | 2.3.0 | [features/drag-and-drop-ai.md](./features/drag-and-drop-ai.md) |
|
| 89 | Drag & drop complet de fichiers/dossiers & intégration Assistant IA | 2.3.0 | [features/drag-and-drop-ai.md](./features/drag-and-drop-ai.md) |
|
||||||
@@ -247,8 +247,8 @@
|
|||||||
| ⚪ P4 reporté | #73 Sync — **reporté (décision 2026-09-26)**, hors chemin critique | 6-8 jours si réactivé |
|
| ⚪ P4 reporté | #73 Sync — **reporté (décision 2026-09-26)**, hors chemin critique | 6-8 jours si réactivé |
|
||||||
| ⚪ P0/P1 prioritaire | #87 CI/CD (BUG-035 → BUG-040 corrigés, #86 livré) | ~3-5 jours |
|
| ⚪ P0/P1 prioritaire | #87 CI/CD (BUG-035 → BUG-040 corrigés, #86 livré) | ~3-5 jours |
|
||||||
| ✅ Terminé | #153 Visionneuse & édition XLSX — complétude (A1-A17 **toutes livrées**, v2.27.0 → v2.39.0) | 0 jour restant |
|
| ✅ Terminé | #153 Visionneuse & édition XLSX — complétude (A1-A17 **toutes livrées**, v2.27.0 → v2.39.0) | 0 jour restant |
|
||||||
| 🔵 En cours | #154 Refonte UI/UX tableur (A1 ✅ Lot 1 · A2-A4 restants) | ~5-7 jours |
|
| ✅ Terminé | #154 Refonte UI/UX tableur (A1-A5 **toutes livrées**, v2.40.0 → v2.44.0) | 0 jour restant |
|
||||||
| **Total chemin critique** | **#77 fin + #87 + #154** | **~9-13 jours** |
|
| **Total chemin critique** | **#77 fin + #87** | **~4-6 jours** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
|
|
||||||
- **Décisions 2026-09-26 :** axe prioritaire = dette & sécurité (#85/#87) ; #73 Sync reporté (P4, hors chemin critique) ; desktop livré non signé + doc SmartScreen.
|
- **Décisions 2026-09-26 :** axe prioritaire = dette & sécurité (#85/#87) ; #73 Sync reporté (P4, hors chemin critique) ; desktop livré non signé + doc SmartScreen.
|
||||||
- **Ajout 2026-09-27 :** #153 ouvert à la suite de l'audit de la visionneuse XLSX (limitations, risques de perte de données, périmètre IA/recherche) — détail et critères dans [features/xlsx-viewer.md](./features/xlsx-viewer.md).
|
- **Ajout 2026-09-27 :** #153 ouvert à la suite de l'audit de la visionneuse XLSX (limitations, risques de perte de données, périmètre IA/recherche) — détail et critères dans [features/xlsx-viewer.md](./features/xlsx-viewer.md).
|
||||||
- **Ajout 2026-09-29 :** #154 ouvert — refonte UI/UX de la visionneuse/éditeur XLSX (audit UX, architecture cible, plan par lots) dans [features/xlsx-ui-redesign.md](./features/xlsx-ui-redesign.md) ; Lot 1 livré (ruban groupé, onglets permanents + « + », badges d'état, tokens de grille).
|
- **Ajout 2026-09-29 :** #154 ouvert — refonte UI/UX de la visionneuse/éditeur XLSX (audit UX, architecture cible, plan par lots) dans [features/xlsx-ui-redesign.md](./features/xlsx-ui-redesign.md) ; **livré en 5 lots** (ruban groupé, onglets permanents + « + », badges d'état, tokens de grille, dialogues thémés, inspecteur droit, undo/redo, extraction `frontend/js/xlsx/*`).
|
||||||
- **Clôture #85 (v2.27.13) :** monolithe découpé (T1→T9), stores verrouillés + rate-limit SQLite (T10), fiche `docs/features/archi-refonte-85.md`.
|
- **Clôture #85 (v2.27.13) :** monolithe découpé (T1→T9), stores verrouillés + rate-limit SQLite (T10), fiche `docs/features/archi-refonte-85.md`.
|
||||||
- Les items P3/P4 ne sont pas ordonnés par priorité interne — à raffiner selon les retours utilisateurs.
|
- Les items P3/P4 ne sont pas ordonnés par priorité interne — à raffiner selon les retours utilisateurs.
|
||||||
- L'effort inclut le développement + tests unitaires + intégration CI, mais pas la documentation utilisateur.
|
- L'effort inclut le développement + tests unitaires + intégration CI, mais pas la documentation utilisateur.
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
> **Item de roadmap :** [#154 — Refonte UI/UX tableur](../ROADMAP.md)
|
> **Item de roadmap :** [#154 — Refonte UI/UX tableur](../ROADMAP.md)
|
||||||
> **Origine :** #152 / #153 (visionneuse XLSX fonctionnelle mais peu conviviale)
|
> **Origine :** #152 / #153 (visionneuse XLSX fonctionnelle mais peu conviviale)
|
||||||
> **Statut :** 🔵 **en cours** — Lots 1, 2, 3 & 4 livrés le 2026-09-29 (reste A5, découpage modulaire)
|
> **Statut :** ✅ **terminé** — Lots 1 → 5 livrés le 2026-09-29 (A1-A5)
|
||||||
> **Effort estimé :** 6-9 jours (Lot 1 ✅ · Lot 2 ✅ · Lot 3 ✅ · Lot 4 ✅ · Lot 5)
|
> **Effort estimé :** 6-9 jours (Lot 1 ✅ · Lot 2 ✅ · Lot 3 ✅ · Lot 4 ✅ · Lot 5 ✅)
|
||||||
> **Règle de maintenance :** la Roadmap porte les cases à cocher (suivi), cette fiche porte
|
> **Règle de maintenance :** la Roadmap porte les cases à cocher (suivi), cette fiche porte
|
||||||
> l'analyse, l'architecture cible et le plan par lots. **Ne pas dupliquer le détail.**
|
> l'analyse, l'architecture cible et le plan par lots. **Ne pas dupliquer le détail.**
|
||||||
|
|
||||||
@@ -95,13 +95,25 @@ la grille ni le backend.
|
|||||||
défilement pour les environnements sans IO).
|
défilement pour les environnements sans IO).
|
||||||
- **A4.3** Sémantique **ARIA** : `role="grid"` / `row` / `gridcell` / `columnheader` / `rowheader`.
|
- **A4.3** Sémantique **ARIA** : `role="grid"` / `row` / `gridcell` / `columnheader` / `rowheader`.
|
||||||
|
|
||||||
### Lot 5 — Découpage modulaire & finitions (à venir)
|
### Lot 5 — Découpage modulaire & finitions ✅ *(2026-09-29)*
|
||||||
|
|
||||||
- Extraction de `renderXlsxViewer` en modules (`frontend/js/xlsx/*.js`) : `toolbar`, `formula-bar`,
|
- **A5.1** Extraction des parties pures/sans état du monolithe `renderXlsxViewer` dans
|
||||||
`grid`, `sheet-tabs`, `inspector`, `cell-editor`, `api` (refactor sans changement de
|
`frontend/js/xlsx/` : **`refs.js`** (`parseRef`, `columnName`, `findTd`, `sheetOfRef`,
|
||||||
comportement, sous couvert des tests JSDOM/E2E existants).
|
`firstCellOfRange`), **`command-bar.js`** (`buildCommandBar` : onglets + ruban + pastilles),
|
||||||
- Lien **dashboard → grille** (clic sur KPI/plage = scroll + sélection), inspecteur
|
**`dashboard.js`** (`renderDashboardLoading`, `renderDashboardHtml`). Le noyau **avec état**
|
||||||
**redimensionnable**, détachement en split via `PaneManager.splitRight()`.
|
(orchestration DOM, édition, écouteurs) reste dans `viewer.js` : l'extraction est volontairement
|
||||||
|
limitée aux unités sans état, à comportement constant et sous couvert des tests.
|
||||||
|
- **A5.2** Lien **dashboard → grille** : cliquer une plage nommée sélectionne et révèle sa
|
||||||
|
première cellule (change d'onglet si la plage est sur une autre feuille).
|
||||||
|
- **A5.3** Inspecteur **redimensionnable** (poignée gauche, largeur 260–640 px, restaurée par
|
||||||
|
session via `localStorage`).
|
||||||
|
- **A5.4** `SW_VERSION` incrémenté (`v28`) et nouveaux modules ajoutés au pré-cache du service
|
||||||
|
worker.
|
||||||
|
|
||||||
|
> **Hors périmètre (documenté) :** le détachement de l'inspecteur en split view
|
||||||
|
> (`PaneManager.splitRight()`) n'est pas retenu — l'inspecteur est intrinsèquement lié à la
|
||||||
|
> visionneuse d'un document ; un split générique ouvrirait un second contexte sans le classeur.
|
||||||
|
> À réévaluer si un usage concret apparaît.
|
||||||
|
|
||||||
## 5. Recommandations techniques (contrainte « zéro build »)
|
## 5. Recommandations techniques (contrainte « zéro build »)
|
||||||
|
|
||||||
@@ -134,3 +146,4 @@ la grille ni le backend.
|
|||||||
| 2026-09-29 | **Lot 2** livré : dialogues thémés (`showConfirm`/`showPrompt`) pour la structure et la confirmation de perte, bandeau de conflit 409 non bloquant avec réessai, indicateur *dirty* (bouton + onglet) |
|
| 2026-09-29 | **Lot 2** livré : dialogues thémés (`showConfirm`/`showPrompt`) pour la structure et la confirmation de perte, bandeau de conflit 409 non bloquant avec réessai, indicateur *dirty* (bouton + onglet) |
|
||||||
| 2026-09-29 | **Lot 3** livré : le Tableau de bord passe dans un **inspecteur droit repliable** (grille toujours visible), en-tête d'inspecteur avec entrée **Assistant IA** et fermeture, responsive < 900 px |
|
| 2026-09-29 | **Lot 3** livré : le Tableau de bord passe dans un **inspecteur droit repliable** (grille toujours visible), en-tête d'inspecteur avec entrée **Assistant IA** et fermeture, responsive < 900 px |
|
||||||
| 2026-09-29 | **Lot 4** livré : **undo/redo** (boutons + `Ctrl+Z`/`Ctrl+Maj+Z`/`Ctrl+Y`), chargement par `IntersectionObserver`, **ARIA** `role="grid"` ; le découpage modulaire est reporté en A5 |
|
| 2026-09-29 | **Lot 4** livré : **undo/redo** (boutons + `Ctrl+Z`/`Ctrl+Maj+Z`/`Ctrl+Y`), chargement par `IntersectionObserver`, **ARIA** `role="grid"` ; le découpage modulaire est reporté en A5 |
|
||||||
|
| 2026-09-29 | **Lot 5** livré (clôture #154) : extraction des unités sans état dans `frontend/js/xlsx/*` (`refs.js`, `command-bar.js`, `dashboard.js`), lien **dashboard → grille**, inspecteur **redimensionnable**, `SW_VERSION` v28 + pré-cache. Split view écarté (documenté) |
|
||||||
|
|||||||
+64
-107
@@ -16,6 +16,9 @@ import { t } from './i18n.js';
|
|||||||
import { onFileRender } from './plugins.js';
|
import { onFileRender } from './plugins.js';
|
||||||
import { NowPlaying } from './now-playing.js';
|
import { NowPlaying } from './now-playing.js';
|
||||||
import { openAssistant } from './ai-fab.js';
|
import { openAssistant } from './ai-fab.js';
|
||||||
|
import { parseRef, columnName, findTd, sheetOfRef, firstCellOfRange } from './xlsx/refs.js';
|
||||||
|
import { buildCommandBar } from './xlsx/command-bar.js';
|
||||||
|
import { renderDashboardLoading, renderDashboardHtml } from './xlsx/dashboard.js';
|
||||||
|
|
||||||
// ── Multi-format export ────────────────────────────────────────────────────
|
// ── Multi-format export ────────────────────────────────────────────────────
|
||||||
// Downloads a file export (HTML / MD bundle / ePub) via the authenticated
|
// Downloads a file export (HTML / MD bundle / ePub) via the authenticated
|
||||||
@@ -1053,13 +1056,9 @@ export function renderXlsxViewer(area, data) {
|
|||||||
// at all, hiding the fact that a workbook can hold several). An editable
|
// at all, hiding the fact that a workbook can hold several). An editable
|
||||||
// workbook also gets an explicit “+” button next to the tabs.
|
// workbook also gets an explicit “+” button next to the tabs.
|
||||||
const editable = !isCsv && !readOnly;
|
const editable = !isCsv && !readOnly;
|
||||||
const tabs = isCsv
|
// #154-A5 — the shell markup (tabs + grouped command bar + status pills) is
|
||||||
? ""
|
// built by frontend/js/xlsx/command-bar.js.
|
||||||
: `<div class="xlsx-tabs" role="tablist">${sheets.map((s, i) =>
|
const { tabs, actionsHtml, statusBar } = buildCommandBar({ sheets, isCsv, readOnly, editable });
|
||||||
`<button class="xlsx-tab${i === 0 ? " active" : ""}" data-sheet="${i}" role="tab" aria-selected="${i === 0}">${escapeHtml(s.name)}</button>`
|
|
||||||
).join("")}${editable
|
|
||||||
? `<button class="xlsx-tab-add" id="xlsx-tab-add" type="button" title="${escapeHtml(t("xlsx.tabs_add_sheet"))}" aria-label="${escapeHtml(t("xlsx.tabs_add_sheet"))}">+</button>`
|
|
||||||
: ""}</div>`;
|
|
||||||
// #153 A15 — inline styles (bold/italic/colors from the file itself), merged
|
// #153 A15 — inline styles (bold/italic/colors from the file itself), merged
|
||||||
// ranges and the freeze anchor are applied AFTER the panels are in the DOM
|
// ranges and the freeze anchor are applied AFTER the panels are in the DOM
|
||||||
// (the HTML string alone cannot carry them: col/row spans need the table).
|
// (the HTML string alone cannot carry them: col/row spans need the table).
|
||||||
@@ -1144,54 +1143,8 @@ export function renderXlsxViewer(area, data) {
|
|||||||
</div>`
|
</div>`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// #154-A1 — status pills make the viewer's limits visible up front: a
|
// #154-A1 — status pills and the grouped command bar are built by
|
||||||
// read-only format (.xls/.ods) and the fact that formulas are never
|
// buildCommandBar() (frontend/js/xlsx/command-bar.js).
|
||||||
// recalculated on screen (the workbook is only written back, Excel recomputes).
|
|
||||||
const statusPills = [
|
|
||||||
readOnly
|
|
||||||
? `<span class="xlsx-status-pill xlsx-status-readonly" title="${escapeHtml(t("xlsx.readonly_hint"))}"><i data-lucide="lock" class="xlsx-status-icon"></i>${escapeHtml(t("xlsx.readonly_badge"))}</span>`
|
|
||||||
: "",
|
|
||||||
!isCsv
|
|
||||||
? `<span class="xlsx-status-pill xlsx-status-formula" title="${escapeHtml(t("xlsx.formulas_note_title"))}"><i data-lucide="sigma" class="xlsx-status-icon"></i>${escapeHtml(t("xlsx.formulas_note"))}</span>`
|
|
||||||
: "",
|
|
||||||
].filter(Boolean).join("");
|
|
||||||
const statusBar = statusPills ? `<div class="xlsx-status-bar">${statusPills}</div>` : "";
|
|
||||||
|
|
||||||
// Command groups, separated by thin rules. The dashboard button is appended
|
|
||||||
// later into the dedicated (empty) “view” group.
|
|
||||||
const actionGroups = [];
|
|
||||||
if (!readOnly) {
|
|
||||||
actionGroups.push(`<span class="xlsx-cmd-group" data-group="history">
|
|
||||||
<button class="btn-action" id="xlsx-undo-btn" type="button" title="${escapeHtml(t("xlsx.undo"))}" disabled>
|
|
||||||
<i data-lucide="undo-2" style="width:14px;height:14px"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn-action" id="xlsx-redo-btn" type="button" title="${escapeHtml(t("xlsx.redo"))}" disabled>
|
|
||||||
<i data-lucide="redo-2" style="width:14px;height:14px"></i>
|
|
||||||
</button>
|
|
||||||
</span>`);
|
|
||||||
actionGroups.push(`<span class="xlsx-cmd-group" data-group="formulas">
|
|
||||||
<button class="btn-action xlsx-formula-toggle" id="xlsx-formula-btn" type="button"
|
|
||||||
aria-pressed="false" title="${escapeHtml(t("xlsx.formula_toggle_title"))}">f(x)</button>
|
|
||||||
</span>`);
|
|
||||||
}
|
|
||||||
if (editable) {
|
|
||||||
actionGroups.push(`<span class="xlsx-cmd-group" data-group="insert">
|
|
||||||
<button class="btn-action" id="xlsx-structure-btn" title="${escapeHtml(t("xlsx.structure_btn"))}">
|
|
||||||
<i data-lucide="table-properties" style="width:14px;height:14px"></i>
|
|
||||||
</button>
|
|
||||||
</span>`);
|
|
||||||
actionGroups.push(`<span class="xlsx-cmd-group" id="xlsx-view-group" data-group="view"></span>`);
|
|
||||||
}
|
|
||||||
actionGroups.push(`<span class="xlsx-cmd-group" data-group="file">
|
|
||||||
<button class="btn-action" id="xlsx-download-btn">
|
|
||||||
<i data-lucide="download" style="width:14px;height:14px"></i> ${t("viewer.download")}
|
|
||||||
</button>
|
|
||||||
<button class="btn-action" id="xlsx-csv-btn" title="${escapeHtml(t("xlsx.csv_export"))}">
|
|
||||||
<i data-lucide="file-spreadsheet" style="width:14px;height:14px"></i> CSV
|
|
||||||
</button>
|
|
||||||
<button class="btn-action xlsx-save-primary" id="xlsx-save-btn" disabled>${t("common.save")}</button>
|
|
||||||
</span>`);
|
|
||||||
const actionsHtml = actionGroups.join('<span class="xlsx-cmd-sep" aria-hidden="true"></span>');
|
|
||||||
|
|
||||||
area.innerHTML = `
|
area.innerHTML = `
|
||||||
<div class="xlsx-viewer">
|
<div class="xlsx-viewer">
|
||||||
@@ -1225,6 +1178,7 @@ export function renderXlsxViewer(area, data) {
|
|||||||
<div class="xlsx-panels">${panels}</div>
|
<div class="xlsx-panels">${panels}</div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="xlsx-inspector" id="xlsx-inspector" aria-hidden="true">
|
<aside class="xlsx-inspector" id="xlsx-inspector" aria-hidden="true">
|
||||||
|
<div class="xlsx-inspector-resize" id="xlsx-inspector-resize" role="separator" aria-orientation="vertical" title="${escapeHtml(t("xlsx.inspector_resize"))}"></div>
|
||||||
<div class="xlsx-inspector-head">
|
<div class="xlsx-inspector-head">
|
||||||
<span class="xlsx-inspector-title">${escapeHtml(t("xlsx.inspector_title"))}</span>
|
<span class="xlsx-inspector-title">${escapeHtml(t("xlsx.inspector_title"))}</span>
|
||||||
<span class="xlsx-inspector-actions">
|
<span class="xlsx-inspector-actions">
|
||||||
@@ -1268,45 +1222,18 @@ export function renderXlsxViewer(area, data) {
|
|||||||
inspector.setAttribute("aria-hidden", "false");
|
inspector.setAttribute("aria-hidden", "false");
|
||||||
const panel = document.createElement("div");
|
const panel = document.createElement("div");
|
||||||
panel.className = "xlsx-dashboard";
|
panel.className = "xlsx-dashboard";
|
||||||
panel.innerHTML = `<div class="xlsx-dashboard-loading">…</div>`;
|
panel.innerHTML = renderDashboardLoading();
|
||||||
inspectorBody.prepend(panel);
|
inspectorBody.prepend(panel);
|
||||||
try {
|
try {
|
||||||
const dash = await api(
|
const dash = await api(
|
||||||
`/api/file/${encodeURIComponent(data.vault)}/xlsx/dashboard?path=${encodeURIComponent(data.path)}`,
|
`/api/file/${encodeURIComponent(data.vault)}/xlsx/dashboard?path=${encodeURIComponent(data.path)}`,
|
||||||
);
|
);
|
||||||
const rangeRows = (dash.named_ranges || []).map((r) =>
|
panel.innerHTML = renderDashboardHtml(dash);
|
||||||
`<tr><td><code>${escapeHtml(r.name)}</code></td><td>${escapeHtml(r.scope || "—")}</td><td><code>${escapeHtml(r.ref)}</code></td></tr>`,
|
|
||||||
).join("");
|
|
||||||
const kpiCards = (dash.sheets || []).map((s) => {
|
|
||||||
const cards = (s.kpi || []).map((k) =>
|
|
||||||
`<span class="xlsx-kpi"><span class="xlsx-kpi-label">${escapeHtml(k.label)}</span><span class="xlsx-kpi-value">${escapeHtml(String(k.value))}</span></span>`,
|
|
||||||
).join("");
|
|
||||||
return `<div class="xlsx-kpi-sheet">
|
|
||||||
<h4>${escapeHtml(s.name)}</h4>
|
|
||||||
<p class="xlsx-kpi-meta">${escapeHtml(t("xlsx.dashboard_stats", {
|
|
||||||
cells: s.cells, rows: s.rows, cols: s.cols, formulas: s.formulas, numeric: s.numeric,
|
|
||||||
}))}</p>
|
|
||||||
<div class="xlsx-kpi-cards">${cards || "<span class=\"xlsx-kpi-empty\">—</span>"}</div>
|
|
||||||
</div>`;
|
|
||||||
}).join("");
|
|
||||||
panel.innerHTML = `
|
|
||||||
<div class="xlsx-dashboard-head">
|
|
||||||
<h3><i data-lucide="layout-dashboard" style="width:14px;height:14px"></i> ${escapeHtml(t("xlsx.dashboard_title"))}</h3>
|
|
||||||
<span class="xlsx-dashboard-objects">
|
|
||||||
${escapeHtml(t("xlsx.dashboard_charts", { n: dash.objects?.charts ?? 0 }))}
|
|
||||||
· ${escapeHtml(t("xlsx.dashboard_pivots", { n: dash.objects?.pivots ?? 0 }))}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
${(dash.named_ranges || []).length || (dash.sheets || []).length ? "" : `<p class="xlsx-kpi-empty">${escapeHtml(t("xlsx.dashboard_empty"))}</p>`}
|
|
||||||
${(dash.named_ranges || []).length ? `
|
|
||||||
<table class="csv-table xlsx-ranges-table"><thead><tr>
|
|
||||||
<th>${escapeHtml(t("xlsx.dashboard_nr_name"))}</th>
|
|
||||||
<th>${escapeHtml(t("xlsx.dashboard_nr_scope"))}</th>
|
|
||||||
<th>${escapeHtml(t("xlsx.dashboard_nr_ref"))}</th>
|
|
||||||
</tr></thead><tbody>${rangeRows}</tbody></table>` : ""}
|
|
||||||
<div class="xlsx-kpi-grid">${kpiCards}</div>
|
|
||||||
<p class="xlsx-dashboard-hint">${escapeHtml(t("xlsx.dashboard_hint"))}</p>`;
|
|
||||||
safeCreateIcons();
|
safeCreateIcons();
|
||||||
|
// #154-A5 — clicking a named range reveals its first cell in the grid.
|
||||||
|
panel.querySelectorAll(".xlsx-range-row[data-ref]").forEach((row) => {
|
||||||
|
row.addEventListener("click", () => revealRange(row.dataset.ref));
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
panel.innerHTML = `<p class="xlsx-kpi-empty">${escapeHtml(t("xlsx.load_error"))}: ${escapeHtml(String(err.message || err))}</p>`;
|
panel.innerHTML = `<p class="xlsx-kpi-empty">${escapeHtml(t("xlsx.load_error"))}: ${escapeHtml(String(err.message || err))}</p>`;
|
||||||
}
|
}
|
||||||
@@ -1315,6 +1242,38 @@ export function renderXlsxViewer(area, data) {
|
|||||||
// #154-A3 — the assistant is the existing global side panel; the inspector
|
// #154-A3 — the assistant is the existing global side panel; the inspector
|
||||||
// just gives it a contextual entry point from the spreadsheet.
|
// just gives it a contextual entry point from the spreadsheet.
|
||||||
area.querySelector("#xlsx-inspector-ai").addEventListener("click", () => { openAssistant(); });
|
area.querySelector("#xlsx-inspector-ai").addEventListener("click", () => { openAssistant(); });
|
||||||
|
|
||||||
|
// #154-A5 — the inspector is resizable (width restored per session).
|
||||||
|
const resizeHandle = area.querySelector("#xlsx-inspector-resize");
|
||||||
|
const WIDTH_KEY = "obsigate_xlsx_inspector_width";
|
||||||
|
const clampWidth = (w) => Math.min(640, Math.max(260, w));
|
||||||
|
const savedWidth = Number(localStorage.getItem(WIDTH_KEY));
|
||||||
|
if (savedWidth >= 260 && savedWidth <= 640) {
|
||||||
|
inspector.style.flexBasis = `${savedWidth}px`;
|
||||||
|
inspector.style.width = `${savedWidth}px`;
|
||||||
|
}
|
||||||
|
let resizing = false;
|
||||||
|
const onMove = (e) => {
|
||||||
|
if (!resizing) return;
|
||||||
|
const width = clampWidth(inspector.getBoundingClientRect().right - e.clientX);
|
||||||
|
inspector.style.flexBasis = `${width}px`;
|
||||||
|
inspector.style.width = `${width}px`;
|
||||||
|
};
|
||||||
|
const onUp = () => {
|
||||||
|
if (!resizing) return;
|
||||||
|
resizing = false;
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
localStorage.setItem(WIDTH_KEY, String(Math.round(inspector.getBoundingClientRect().width)));
|
||||||
|
document.removeEventListener("mousemove", onMove);
|
||||||
|
document.removeEventListener("mouseup", onUp);
|
||||||
|
};
|
||||||
|
resizeHandle.addEventListener("mousedown", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
resizing = true;
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
document.addEventListener("mousemove", onMove);
|
||||||
|
document.addEventListener("mouseup", onUp);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveBtn = area.querySelector("#xlsx-save-btn");
|
const saveBtn = area.querySelector("#xlsx-save-btn");
|
||||||
@@ -1484,28 +1443,26 @@ export function renderXlsxViewer(area, data) {
|
|||||||
if (!syncing.value) formulaInput.value = td.textContent;
|
if (!syncing.value) formulaInput.value = td.textContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse an A1 reference into its (row, col) parts.
|
// #154-A5 — reveal a named range's first cell in the grid (dashboard → grid).
|
||||||
const parseRef = (ref) => {
|
const revealRange = (ref) => {
|
||||||
const m = /^([A-Z]+)(\d+)$/.exec(ref || "");
|
const cellRef = firstCellOfRange(ref);
|
||||||
if (!m) return null;
|
if (!cellRef) return;
|
||||||
let col = 0;
|
const sheetName = sheetOfRef(ref);
|
||||||
for (const ch of m[1]) col = col * 26 + (ch.charCodeAt(0) - 64);
|
let panel = visiblePanel();
|
||||||
return { row: Number(m[2]), col };
|
if (sheetName) {
|
||||||
};
|
const idx = sheets.findIndex((s) => s.name === sheetName);
|
||||||
|
const tab = idx >= 0 ? area.querySelector(`.xlsx-tab[data-sheet="${idx}"]`) : null;
|
||||||
const findTd = (panel, row, col) =>
|
if (tab) { tab.click(); panel = panelEls[idx]; }
|
||||||
panel.querySelector(`td[data-cell="${columnName(col)}${row}"]`);
|
|
||||||
|
|
||||||
const columnName = (col) => {
|
|
||||||
let name = "";
|
|
||||||
while (col > 0) {
|
|
||||||
const rem = (col - 1) % 26;
|
|
||||||
name = String.fromCharCode(65 + rem) + name;
|
|
||||||
col = Math.floor((col - 1) / 26);
|
|
||||||
}
|
}
|
||||||
return name;
|
const td = panel.querySelector(`td[data-cell="${cellRef}"]`);
|
||||||
|
if (!td) { showToast(t("xlsx.range_not_visible"), "info"); return; }
|
||||||
|
setActiveCell(td);
|
||||||
|
td.focus();
|
||||||
|
if (typeof td.scrollIntoView === "function") td.scrollIntoView({ block: "center", inline: "center" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// #154-A5 — parseRef / columnName / findTd live in frontend/js/xlsx/refs.js.
|
||||||
|
|
||||||
const moveActive = (td, key, forward = true) => {
|
const moveActive = (td, key, forward = true) => {
|
||||||
const ref = parseRef(cellName(td));
|
const ref = parseRef(cellName(td));
|
||||||
if (!ref) return;
|
if (!ref) return;
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/* ObsiGate — XLSX command bar builder (#154-A5).
|
||||||
|
*
|
||||||
|
* Pure string builder for the spreadsheet shell (sheet tabs + grouped action
|
||||||
|
* buttons + status pills). Extracted from frontend/js/viewer.js so the markup
|
||||||
|
* is testable and the viewer only wires behaviour.
|
||||||
|
*/
|
||||||
|
import { t } from '../i18n.js';
|
||||||
|
import { escapeHtml } from '../utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} ctx
|
||||||
|
* @param {Array<{name: string}>} ctx.sheets
|
||||||
|
* @param {boolean} ctx.isCsv
|
||||||
|
* @param {boolean} ctx.readOnly
|
||||||
|
* @param {boolean} ctx.editable - not a CSV and not read-only
|
||||||
|
* @returns {{ tabs: string, actionsHtml: string, statusBar: string }}
|
||||||
|
*/
|
||||||
|
export function buildCommandBar({ sheets, isCsv, readOnly, editable }) {
|
||||||
|
// Sheet tabs are always rendered (even for a single sheet) so the strip reads
|
||||||
|
// as a real affordance; an editable workbook gets an explicit “+” button.
|
||||||
|
const tabs = isCsv
|
||||||
|
? ""
|
||||||
|
: `<div class="xlsx-tabs" role="tablist">${sheets.map((s, i) =>
|
||||||
|
`<button class="xlsx-tab${i === 0 ? " active" : ""}" data-sheet="${i}" role="tab" aria-selected="${i === 0}">${escapeHtml(s.name)}</button>`
|
||||||
|
).join("")}${editable
|
||||||
|
? `<button class="xlsx-tab-add" id="xlsx-tab-add" type="button" title="${escapeHtml(t("xlsx.tabs_add_sheet"))}" aria-label="${escapeHtml(t("xlsx.tabs_add_sheet"))}">+</button>`
|
||||||
|
: ""}</div>`;
|
||||||
|
|
||||||
|
// Status pills make the viewer's limits visible up front.
|
||||||
|
const statusPills = [
|
||||||
|
readOnly
|
||||||
|
? `<span class="xlsx-status-pill xlsx-status-readonly" title="${escapeHtml(t("xlsx.readonly_hint"))}"><i data-lucide="lock" class="xlsx-status-icon"></i>${escapeHtml(t("xlsx.readonly_badge"))}</span>`
|
||||||
|
: "",
|
||||||
|
!isCsv
|
||||||
|
? `<span class="xlsx-status-pill xlsx-status-formula" title="${escapeHtml(t("xlsx.formulas_note_title"))}"><i data-lucide="sigma" class="xlsx-status-icon"></i>${escapeHtml(t("xlsx.formulas_note"))}</span>`
|
||||||
|
: "",
|
||||||
|
].filter(Boolean).join("");
|
||||||
|
const statusBar = statusPills ? `<div class="xlsx-status-bar">${statusPills}</div>` : "";
|
||||||
|
|
||||||
|
// Command groups, separated by thin rules.
|
||||||
|
const actionGroups = [];
|
||||||
|
if (!readOnly) {
|
||||||
|
actionGroups.push(`<span class="xlsx-cmd-group" data-group="history">
|
||||||
|
<button class="btn-action" id="xlsx-undo-btn" type="button" title="${escapeHtml(t("xlsx.undo"))}" disabled>
|
||||||
|
<i data-lucide="undo-2" style="width:14px;height:14px"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn-action" id="xlsx-redo-btn" type="button" title="${escapeHtml(t("xlsx.redo"))}" disabled>
|
||||||
|
<i data-lucide="redo-2" style="width:14px;height:14px"></i>
|
||||||
|
</button>
|
||||||
|
</span>`);
|
||||||
|
actionGroups.push(`<span class="xlsx-cmd-group" data-group="formulas">
|
||||||
|
<button class="btn-action xlsx-formula-toggle" id="xlsx-formula-btn" type="button"
|
||||||
|
aria-pressed="false" title="${escapeHtml(t("xlsx.formula_toggle_title"))}">f(x)</button>
|
||||||
|
</span>`);
|
||||||
|
}
|
||||||
|
if (editable) {
|
||||||
|
actionGroups.push(`<span class="xlsx-cmd-group" data-group="insert">
|
||||||
|
<button class="btn-action" id="xlsx-structure-btn" title="${escapeHtml(t("xlsx.structure_btn"))}">
|
||||||
|
<i data-lucide="table-properties" style="width:14px;height:14px"></i>
|
||||||
|
</button>
|
||||||
|
</span>`);
|
||||||
|
actionGroups.push(`<span class="xlsx-cmd-group" id="xlsx-view-group" data-group="view"></span>`);
|
||||||
|
}
|
||||||
|
actionGroups.push(`<span class="xlsx-cmd-group" data-group="file">
|
||||||
|
<button class="btn-action" id="xlsx-download-btn">
|
||||||
|
<i data-lucide="download" style="width:14px;height:14px"></i> ${t("viewer.download")}
|
||||||
|
</button>
|
||||||
|
<button class="btn-action" id="xlsx-csv-btn" title="${escapeHtml(t("xlsx.csv_export"))}">
|
||||||
|
<i data-lucide="file-spreadsheet" style="width:14px;height:14px"></i> CSV
|
||||||
|
</button>
|
||||||
|
<button class="btn-action xlsx-save-primary" id="xlsx-save-btn" disabled>${t("common.save")}</button>
|
||||||
|
</span>`);
|
||||||
|
const actionsHtml = actionGroups.join('<span class="xlsx-cmd-sep" aria-hidden="true"></span>');
|
||||||
|
|
||||||
|
return { tabs, actionsHtml, statusBar };
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/* ObsiGate — XLSX dashboard renderer (#154-A5).
|
||||||
|
*
|
||||||
|
* Pure string builders for the workbook dashboard inspector (named ranges, KPI
|
||||||
|
* cards). Extracted from frontend/js/viewer.js; the viewer handles the fetch,
|
||||||
|
* the inspector host and the range → grid link.
|
||||||
|
*/
|
||||||
|
import { t } from '../i18n.js';
|
||||||
|
import { escapeHtml } from '../utils.js';
|
||||||
|
|
||||||
|
export function renderDashboardLoading() {
|
||||||
|
return `<div class="xlsx-dashboard-loading">…</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} dash - payload of GET …/xlsx/dashboard
|
||||||
|
* @returns {string} the inspector body markup
|
||||||
|
*/
|
||||||
|
export function renderDashboardHtml(dash) {
|
||||||
|
const namedRanges = dash.named_ranges || [];
|
||||||
|
const sheets = dash.sheets || [];
|
||||||
|
const rangeRows = namedRanges.map((r) =>
|
||||||
|
`<tr class="xlsx-range-row" data-ref="${escapeHtml(r.ref || "")}"><td><code>${escapeHtml(r.name)}</code></td><td>${escapeHtml(r.scope || "—")}</td><td><code>${escapeHtml(r.ref)}</code></td></tr>`,
|
||||||
|
).join("");
|
||||||
|
const kpiCards = sheets.map((s) => {
|
||||||
|
const cards = (s.kpi || []).map((k) =>
|
||||||
|
`<span class="xlsx-kpi"><span class="xlsx-kpi-label">${escapeHtml(k.label)}</span><span class="xlsx-kpi-value">${escapeHtml(String(k.value))}</span></span>`,
|
||||||
|
).join("");
|
||||||
|
return `<div class="xlsx-kpi-sheet">
|
||||||
|
<h4>${escapeHtml(s.name)}</h4>
|
||||||
|
<p class="xlsx-kpi-meta">${escapeHtml(t("xlsx.dashboard_stats", {
|
||||||
|
cells: s.cells, rows: s.rows, cols: s.cols, formulas: s.formulas, numeric: s.numeric,
|
||||||
|
}))}</p>
|
||||||
|
<div class="xlsx-kpi-cards">${cards || "<span class=\"xlsx-kpi-empty\">—</span>"}</div>
|
||||||
|
</div>`;
|
||||||
|
}).join("");
|
||||||
|
return `
|
||||||
|
<div class="xlsx-dashboard-head">
|
||||||
|
<h3><i data-lucide="layout-dashboard" style="width:14px;height:14px"></i> ${escapeHtml(t("xlsx.dashboard_title"))}</h3>
|
||||||
|
<span class="xlsx-dashboard-objects">
|
||||||
|
${escapeHtml(t("xlsx.dashboard_charts", { n: dash.objects?.charts ?? 0 }))}
|
||||||
|
· ${escapeHtml(t("xlsx.dashboard_pivots", { n: dash.objects?.pivots ?? 0 }))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
${namedRanges.length || sheets.length ? "" : `<p class="xlsx-kpi-empty">${escapeHtml(t("xlsx.dashboard_empty"))}</p>`}
|
||||||
|
${namedRanges.length ? `
|
||||||
|
<table class="csv-table xlsx-ranges-table"><thead><tr>
|
||||||
|
<th>${escapeHtml(t("xlsx.dashboard_nr_name"))}</th>
|
||||||
|
<th>${escapeHtml(t("xlsx.dashboard_nr_scope"))}</th>
|
||||||
|
<th>${escapeHtml(t("xlsx.dashboard_nr_ref"))}</th>
|
||||||
|
</tr></thead><tbody>${rangeRows}</tbody></table>` : ""}
|
||||||
|
<div class="xlsx-kpi-grid">${kpiCards}</div>
|
||||||
|
<p class="xlsx-dashboard-hint">${escapeHtml(t("xlsx.dashboard_hint"))}</p>`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/* ObsiGate — XLSX A1 reference helpers (#154-A5).
|
||||||
|
*
|
||||||
|
* Pure functions (no DOM, no i18n): safe to unit-test in Node and shared by the
|
||||||
|
* viewer. Extracted from frontend/js/viewer.js without behaviour change.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Parse an A1 reference ("B12") into its 1-based (row, col) parts. */
|
||||||
|
export function parseRef(ref) {
|
||||||
|
const m = /^([A-Z]+)(\d+)$/.exec(ref || "");
|
||||||
|
if (!m) return null;
|
||||||
|
let col = 0;
|
||||||
|
for (const ch of m[1]) col = col * 26 + (ch.charCodeAt(0) - 64);
|
||||||
|
return { row: Number(m[2]), col };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Column number (1-based) → letters ("A", "Z", "AA"). */
|
||||||
|
export function columnName(col) {
|
||||||
|
let name = "";
|
||||||
|
while (col > 0) {
|
||||||
|
const rem = (col - 1) % 26;
|
||||||
|
name = String.fromCharCode(65 + rem) + name;
|
||||||
|
col = Math.floor((col - 1) / 26);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find a rendered cell inside a sheet panel by (row, col). */
|
||||||
|
export function findTd(panel, row, col) {
|
||||||
|
return panel.querySelector(`td[data-cell="${columnName(col)}${row}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sheet prefix of a range ref ("'Mon onglet'!$A$1" → "Mon onglet"), or null. */
|
||||||
|
export function sheetOfRef(ref) {
|
||||||
|
const s = String(ref || "");
|
||||||
|
if (!s.includes("!")) return null;
|
||||||
|
return s.split("!")[0].replace(/^'|'$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** First cell of a range ref ("Data!$A$1:$B$5" → "A1"), or null. */
|
||||||
|
export function firstCellOfRange(ref) {
|
||||||
|
const s = String(ref || "");
|
||||||
|
const body = s.includes("!") ? s.split("!").pop() : s;
|
||||||
|
const m = /([A-Za-z]+)\$?(\d+)/.exec(body);
|
||||||
|
return m ? `${m[1].toUpperCase()}${m[2]}` : null;
|
||||||
|
}
|
||||||
@@ -1882,6 +1882,8 @@
|
|||||||
"xlsx.inspector_title": "Inspector",
|
"xlsx.inspector_title": "Inspector",
|
||||||
"xlsx.inspector_ai": "AI assistant",
|
"xlsx.inspector_ai": "AI assistant",
|
||||||
"xlsx.inspector_ai_short": "AI",
|
"xlsx.inspector_ai_short": "AI",
|
||||||
|
"xlsx.inspector_resize": "Resize the inspector",
|
||||||
|
"xlsx.range_not_visible": "Cell out of view (truncated sheet)",
|
||||||
"xlsx.dashboard_title": "Workbook dashboard",
|
"xlsx.dashboard_title": "Workbook dashboard",
|
||||||
"xlsx.dashboard_stats": "{{cells}} cells · {{rows}} rows · {{cols}} columns · {{formulas}} formulas · {{numeric}} numeric values",
|
"xlsx.dashboard_stats": "{{cells}} cells · {{rows}} rows · {{cols}} columns · {{formulas}} formulas · {{numeric}} numeric values",
|
||||||
"xlsx.dashboard_charts": "{{n}} chart(s)",
|
"xlsx.dashboard_charts": "{{n}} chart(s)",
|
||||||
|
|||||||
@@ -1882,6 +1882,8 @@
|
|||||||
"xlsx.inspector_title": "Inspecteur",
|
"xlsx.inspector_title": "Inspecteur",
|
||||||
"xlsx.inspector_ai": "Assistant IA",
|
"xlsx.inspector_ai": "Assistant IA",
|
||||||
"xlsx.inspector_ai_short": "IA",
|
"xlsx.inspector_ai_short": "IA",
|
||||||
|
"xlsx.inspector_resize": "Redimensionner l'inspecteur",
|
||||||
|
"xlsx.range_not_visible": "Cellule hors de l'affichage (feuille tronquée)",
|
||||||
"xlsx.dashboard_title": "Tableau de bord du classeur",
|
"xlsx.dashboard_title": "Tableau de bord du classeur",
|
||||||
"xlsx.dashboard_stats": "{{cells}} cellules · {{rows}} lignes · {{cols}} colonnes · {{formulas}} formules · {{numeric}} valeurs numériques",
|
"xlsx.dashboard_stats": "{{cells}} cellules · {{rows}} lignes · {{cols}} colonnes · {{formulas}} formules · {{numeric}} valeurs numériques",
|
||||||
"xlsx.dashboard_charts": "{{n}} graphique(s)",
|
"xlsx.dashboard_charts": "{{n}} graphique(s)",
|
||||||
|
|||||||
@@ -11305,6 +11305,7 @@ body.desktop-mode .editor-container {
|
|||||||
max-width: 45%;
|
max-width: 45%;
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
@@ -11336,6 +11337,25 @@ body.desktop-mode .editor-container {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
/* #154-A5 — resize handle on the inspector's left edge. */
|
||||||
|
.xlsx-inspector-resize {
|
||||||
|
position: absolute;
|
||||||
|
left: -3px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 6px;
|
||||||
|
cursor: col-resize;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.xlsx-inspector-resize:hover {
|
||||||
|
background: color-mix(in srgb, var(--accent) 40%, transparent);
|
||||||
|
}
|
||||||
|
.xlsx-range-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.xlsx-range-row:hover td {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
.xlsx-inspector .xlsx-dashboard {
|
.xlsx-inspector .xlsx-dashboard {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
+4
-1
@@ -11,7 +11,7 @@
|
|||||||
* cache or Cloudflare does NOT clear the Service Worker Cache Storage, which is
|
* cache or Cloudflare does NOT clear the Service Worker Cache Storage, which is
|
||||||
* a separate store. Bumping SW_VERSION invalidates it on every release.
|
* a separate store. Bumping SW_VERSION invalidates it on every release.
|
||||||
*/
|
*/
|
||||||
const SW_VERSION = 'v27';
|
const SW_VERSION = 'v28';
|
||||||
const CODE_CACHE = `obsigate-code-${SW_VERSION}`;
|
const CODE_CACHE = `obsigate-code-${SW_VERSION}`;
|
||||||
const RUNTIME_CACHE = `obsigate-runtime-${SW_VERSION}`;
|
const RUNTIME_CACHE = `obsigate-runtime-${SW_VERSION}`;
|
||||||
const API_CACHE = `obsigate-api-${SW_VERSION}`;
|
const API_CACHE = `obsigate-api-${SW_VERSION}`;
|
||||||
@@ -30,6 +30,9 @@ const PRECACHE_URLS = [
|
|||||||
'/static/js/ui.js',
|
'/static/js/ui.js',
|
||||||
'/static/js/sidebar.js',
|
'/static/js/sidebar.js',
|
||||||
'/static/js/viewer.js',
|
'/static/js/viewer.js',
|
||||||
|
'/static/js/xlsx/refs.js',
|
||||||
|
'/static/js/xlsx/command-bar.js',
|
||||||
|
'/static/js/xlsx/dashboard.js',
|
||||||
'/static/js/search.js',
|
'/static/js/search.js',
|
||||||
'/static/js/config.js',
|
'/static/js/config.js',
|
||||||
'/static/js/utils.js',
|
'/static/js/utils.js',
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.43.0",
|
"version": "2.43.1",
|
||||||
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
||||||
"main": "patch.js",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -907,6 +907,52 @@ await test("the grid exposes ARIA grid semantics", () => {
|
|||||||
assert.equal(area.querySelector("th.xlsx-rownum").getAttribute("role"), "rowheader");
|
assert.equal(area.querySelector("th.xlsx-rownum").getAttribute("role"), "rowheader");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── #154-A5 — extracted refs module & dashboard → grid link ────────────────
|
||||||
|
|
||||||
|
const { parseRef, columnName, firstCellOfRange, sheetOfRef } = await import(
|
||||||
|
pathToFileURL(path.join(REPO_ROOT, "frontend", "js", "xlsx", "refs.js")).href
|
||||||
|
);
|
||||||
|
|
||||||
|
await test("parseRef / columnName round-trip A1 references", () => {
|
||||||
|
assert.deepEqual(parseRef("A1"), { row: 1, col: 1 });
|
||||||
|
assert.deepEqual(parseRef("B12"), { row: 12, col: 2 });
|
||||||
|
assert.deepEqual(parseRef("AA3"), { row: 3, col: 27 });
|
||||||
|
assert.equal(parseRef("A"), null);
|
||||||
|
assert.equal(columnName(1), "A");
|
||||||
|
assert.equal(columnName(26), "Z");
|
||||||
|
assert.equal(columnName(27), "AA");
|
||||||
|
assert.equal(columnName(parseRef("AB9").col), "AB");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test("range helpers extract the sheet and the first cell", () => {
|
||||||
|
assert.equal(firstCellOfRange("Data!$A$1:$B$5"), "A1");
|
||||||
|
assert.equal(firstCellOfRange("'Mon onglet'!$C$3"), "C3");
|
||||||
|
assert.equal(firstCellOfRange("A1:B2"), "A1");
|
||||||
|
assert.equal(sheetOfRef("Data!$A$1:$B$5"), "Data");
|
||||||
|
assert.equal(sheetOfRef("'Mon onglet'!$C$3"), "Mon onglet");
|
||||||
|
assert.equal(sheetOfRef("A1:B2"), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test("clicking a named range reveals its first cell in the grid", async () => {
|
||||||
|
const area = mount();
|
||||||
|
apiQueue.push({
|
||||||
|
ok: true, status: 200,
|
||||||
|
body: { vault: "V", path: "data.xlsx", named_ranges: [{ name: "MaPlage", scope: "", ref: "Feuille1!$A$1:$B$2" }], objects: { charts: 0, pivots: 0 }, sheets: [] },
|
||||||
|
});
|
||||||
|
area.querySelector("#xlsx-dashboard-btn").click();
|
||||||
|
await new Promise((r) => setTimeout(r, 5));
|
||||||
|
const row = area.querySelector(".xlsx-range-row[data-ref]");
|
||||||
|
assert.ok(row, "the named range row is rendered");
|
||||||
|
row.click();
|
||||||
|
assert.equal(area.querySelector("#xlsx-active-cell").textContent, "A1", "the anchor cell becomes active");
|
||||||
|
assert.ok(area.querySelector('td[data-cell="A1"]').classList.contains("xlsx-active"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await test("the inspector exposes a resize handle", () => {
|
||||||
|
const area = mount();
|
||||||
|
assert.ok(area.querySelector("#xlsx-inspector-resize"), "resize handle present");
|
||||||
|
});
|
||||||
|
|
||||||
// ── Report ──────────────────────────────────────────────────────────────────
|
// ── Report ──────────────────────────────────────────────────────────────────
|
||||||
console.log(`\n${passCount}/${testCount} tests passed\n`);
|
console.log(`\n${passCount}/${testCount} tests passed\n`);
|
||||||
process.exit(passCount === testCount ? 0 : 1);
|
process.exit(passCount === testCount ? 0 : 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user