feat: completude de l'editeur Excel - mise en forme, sortie, concurrence, cache, outils IA #156
CI / lint (push) Successful in 2m36s
CI / security (push) Successful in 1m49s
CI / test (push) Successful in 4m33s
CI / build (push) Successful in 2m39s
CI / e2e (push) Successful in 15m45s

L'editeur lisait les styles mais ne les ecrivait pas, exportait la feuille
entiere et laissait le dernier-ecrivain gagner entre processus.

- A8 : bouton Mise en forme (gras/italique/souligne, alignements, couleurs via
  selecteur natif, formats de nombre, fusion, volets figes, largeur/hauteur) et
  nouvelle route PUT .../xlsx/style (verrou, backup, swap atomique, garde de
  perte, If-Match) ; A9 : decision "pas de moteur de formule" annoncee dans
  l'UI ; A10 : undo/redo unifie, piles par fichier conservees au re-rendu
- A11 : export de la selection + Markdown/HTML/impression et recherche sur
  toutes les feuilles ; A12 : concurrence optimiste (If-Match -> 409 reparable,
  retry qui relit) ; A13 : cache des metadonnees par (chemin, mtime, taille)
- A14 : outils IA .xlsm/.csv + search_workbook, analyze_range,
  edit_xlsx_structure
- tests : test_xlsx_styles.py (17), test_spreadsheet_tools.py (34),
  TestOptimisticConcurrency/TestMetaCache, JSDOM xlsx-viewer 107/107
This commit is contained in:
2026-09-30 07:04:47 -04:00
parent 1bacfd69d9
commit c5c225a68e
32 changed files with 4615 additions and 302 deletions
+1108 -94
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -55,6 +55,9 @@ export function buildCommandBar({ sheets, isCsv, readOnly, editable }) {
}
if (editable) {
actionGroups.push(`<span class="xlsx-cmd-group" data-group="insert">
<button class="btn-action" id="xlsx-format-btn" title="${escapeHtml(t("xlsx.format_btn"))}">
<i data-lucide="paintbrush" style="width:14px;height:14px"></i>
</button>
<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>
@@ -68,6 +71,16 @@ export function buildCommandBar({ sheets, isCsv, readOnly, editable }) {
<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>
<span class="export-dropdown xlsx-export-dropdown">
<button class="btn-action" id="xlsx-export-btn" type="button" title="${escapeHtml(t("xlsx.export_title"))}">
<i data-lucide="file-down" style="width:14px;height:14px"></i> ${escapeHtml(t("xlsx.export_btn"))}
</button>
<div class="export-menu xlsx-export-menu" id="xlsx-export-menu" style="display:none">
<button class="export-menu-item" type="button" data-xlsx-export="md">${escapeHtml(t("xlsx.export_md"))}</button>
<button class="export-menu-item" type="button" data-xlsx-export="html">${escapeHtml(t("xlsx.export_html"))}</button>
<button class="export-menu-item" type="button" data-xlsx-export="print">${escapeHtml(t("xlsx.print"))}</button>
</div>
</span>
<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>');
+52 -2
View File
@@ -1848,13 +1848,59 @@
"xlsx.load_error": "Could not load the remaining rows",
"xlsx.formula_bar_placeholder": "Active cell content",
"xlsx.active_cell": "Cell",
"xlsx.find_placeholder": "Search in the sheet…",
"xlsx.name_box_label": "Name Box — jump to a cell or a range",
"xlsx.name_box_hint": "Type a reference (B12, A1:B3) then Enter to jump there",
"xlsx.name_box_invalid": "Unknown reference: {ref}",
"xlsx.copy": "Copy",
"xlsx.cut": "Cut",
"xlsx.paste": "Paste",
"xlsx.paste_empty": "Clipboard empty or unreadable",
"xlsx.paste_out_of_grid": "Part of the pasted block is outside the displayed grid and was skipped",
"xlsx.fn_suggestions": "SUM;AVERAGE;COUNT;COUNTA;MIN;MAX;IF;VLOOKUP;XLOOKUP;SUMIF;SUMIFS;COUNTIF;CONCAT;LEFT;RIGHT;MID;TODAY;NOW;ROUND;ABS;SQRT",
"xlsx.formula_stored_as_text": "{n} value(s) starting with “=” or “@” saved as text — turn on the f(x) button to write real formulas",
"xlsx.find_placeholder": "Search in the workbook…",
"xlsx.find_prev": "Previous",
"xlsx.find_next": "Next",
"xlsx.find_case": "Match case",
"xlsx.find_no_match": "No match",
"xlsx.find_count": "{index}/{count}",
"xlsx.csv_export": "Export the sheet as CSV",
"xlsx.find_count_sheets": "{index}/{count} · {sheets} sheets",
"xlsx.csv_export": "Export the sheet or the selection as CSV",
"xlsx.export_btn": "Export",
"xlsx.export_title": "Export the selection or the sheet",
"xlsx.export_md": "Export as Markdown",
"xlsx.export_html": "Export as HTML",
"xlsx.print": "Print",
"xlsx.export_scope_sheet": "the sheet",
"xlsx.export_scope_selection": "the selection",
"xlsx.export_done": "Exported {scope}",
"xlsx.format_btn": "Formatting",
"xlsx.format_bold": "Bold",
"xlsx.format_italic": "Italic",
"xlsx.format_underline": "Underline",
"xlsx.format_clear": "Clear formatting",
"xlsx.format_align_left": "Align left",
"xlsx.format_align_center": "Center",
"xlsx.format_align_right": "Align right",
"xlsx.format_font_color": "Text colour",
"xlsx.format_fill_color": "Fill colour",
"xlsx.format_num_general": "General format",
"xlsx.format_num_number": "Number (2 decimals)",
"xlsx.format_num_percent": "Percentage",
"xlsx.format_num_currency": "Currency (€)",
"xlsx.format_num_date": "Date (DD/MM/YYYY)",
"xlsx.format_num_text": "Text",
"xlsx.format_merge": "Merge cells",
"xlsx.format_unmerge": "Unmerge cells",
"xlsx.format_merge_needs_range": "Select at least two cells to merge",
"xlsx.format_freeze": "Freeze panes here",
"xlsx.format_unfreeze": "Unfreeze panes",
"xlsx.format_col_width": "Column width…",
"xlsx.format_col_prompt": "Width of column {col} (0 to 255)",
"xlsx.format_row_height": "Row height…",
"xlsx.format_row_prompt": "Height of row {row} (0 to 409)",
"xlsx.format_applied": "Formatting saved",
"xlsx.format_error": "Formatting failed",
"xlsx.sort_asc": "Sort column A→Z",
"xlsx.sort_desc": "Sort column Z→A",
"xlsx.sort_applied": "Sort applied on {col} — display only, the workbook is unchanged",
@@ -1905,6 +1951,7 @@
"xlsx.dashboard_nr_ref": "Reference",
"xlsx.dashboard_hint": "Select a range or open the AI assistant to analyse this data.",
"xlsx.feature_cached_values": "cached values",
"xlsx.feature_cached_values_unverified": "cached values could not be verified (workbook too large)",
"xlsx.feature_slicers": "slicers and timelines",
"xlsx.feature_form_controls": "form controls",
"xlsx.feature_connections": "connections and queries",
@@ -2080,6 +2127,9 @@
"ai.step.xlsx_read": "Workbook read: {value}",
"ai.step.xlsx_update": "Cells edited: {value}",
"ai.step.xlsx_append": "Rows appended: {value}",
"ai.step.xlsx_search": "Workbook searched: {value}",
"ai.step.xlsx_analyze": "Range analysed: {value}",
"ai.step.xlsx_structure": "Structure changed: {value}",
"ai.step.docx_create": "Word document proposed: {value}",
"ai.step.csv_create": "CSV file proposed: {value}",
"ai.step.pdf_create": "PDF document proposed: {value}",
+52 -2
View File
@@ -1848,13 +1848,59 @@
"xlsx.load_error": "Chargement de la suite impossible",
"xlsx.formula_bar_placeholder": "Contenu de la cellule active",
"xlsx.active_cell": "Cellule",
"xlsx.find_placeholder": "Rechercher dans la feuille…",
"xlsx.name_box_label": "Zone Nom — atteindre une cellule ou une plage",
"xlsx.name_box_hint": "Saisissez une référence (B12, A1:B3) puis Entrée pour y aller",
"xlsx.name_box_invalid": "Référence inconnue : {ref}",
"xlsx.copy": "Copier",
"xlsx.cut": "Couper",
"xlsx.paste": "Coller",
"xlsx.paste_empty": "Presse-papiers vide ou illisible",
"xlsx.paste_out_of_grid": "Une partie du collage dépasse la grille affichée et a été ignorée",
"xlsx.fn_suggestions": "SOMME;MOYENNE;NB;NBVAL;MIN;MAX;SI;RECHERCHEV;RECHERCHEX;SOMME.SI;SOMME.SI.ENS;NB.SI;CONCAT;GAUCHE;DROITE;STXT;AUJOURDHUI;MAINTENANT;ARRONDI;ABS;RACINE",
"xlsx.formula_stored_as_text": "{n} valeur(s) commençant par « = » ou « @ » enregistrée(s) comme texte — activez le bouton f(x) pour écrire de vraies formules",
"xlsx.find_placeholder": "Rechercher dans le classeur…",
"xlsx.find_prev": "Précédent",
"xlsx.find_next": "Suivant",
"xlsx.find_case": "Respecter la casse",
"xlsx.find_no_match": "Aucune correspondance",
"xlsx.find_count": "{index}/{count}",
"xlsx.csv_export": "Exporter la feuille en CSV",
"xlsx.find_count_sheets": "{index}/{count} · {sheets} feuilles",
"xlsx.csv_export": "Exporter la feuille ou la sélection en CSV",
"xlsx.export_btn": "Exporter",
"xlsx.export_title": "Exporter la sélection ou la feuille",
"xlsx.export_md": "Exporter en Markdown",
"xlsx.export_html": "Exporter en HTML",
"xlsx.print": "Imprimer",
"xlsx.export_scope_sheet": "la feuille",
"xlsx.export_scope_selection": "la sélection",
"xlsx.export_done": "Export de {scope} terminé",
"xlsx.format_btn": "Mise en forme",
"xlsx.format_bold": "Gras",
"xlsx.format_italic": "Italique",
"xlsx.format_underline": "Souligné",
"xlsx.format_clear": "Effacer la mise en forme",
"xlsx.format_align_left": "Aligner à gauche",
"xlsx.format_align_center": "Centrer",
"xlsx.format_align_right": "Aligner à droite",
"xlsx.format_font_color": "Couleur du texte",
"xlsx.format_fill_color": "Couleur de fond",
"xlsx.format_num_general": "Format général",
"xlsx.format_num_number": "Nombre (2 décimales)",
"xlsx.format_num_percent": "Pourcentage",
"xlsx.format_num_currency": "Monétaire (€)",
"xlsx.format_num_date": "Date (JJ/MM/AAAA)",
"xlsx.format_num_text": "Texte",
"xlsx.format_merge": "Fusionner les cellules",
"xlsx.format_unmerge": "Annuler la fusion",
"xlsx.format_merge_needs_range": "Sélectionnez au moins deux cellules pour fusionner",
"xlsx.format_freeze": "Figer les volets ici",
"xlsx.format_unfreeze": "Libérer les volets",
"xlsx.format_col_width": "Largeur de colonne…",
"xlsx.format_col_prompt": "Largeur de la colonne {col} (0 à 255)",
"xlsx.format_row_height": "Hauteur de ligne…",
"xlsx.format_row_prompt": "Hauteur de la ligne {row} (0 à 409)",
"xlsx.format_applied": "Mise en forme enregistrée",
"xlsx.format_error": "Échec de la mise en forme",
"xlsx.sort_asc": "Trier la colonne A→Z",
"xlsx.sort_desc": "Trier la colonne Z→A",
"xlsx.sort_applied": "Tri appliqué sur {col} — l'affichage seul, le classeur n'est pas modifié",
@@ -1905,6 +1951,7 @@
"xlsx.dashboard_nr_ref": "Référence",
"xlsx.dashboard_hint": "Sélectionnez une plage ou ouvrez l'assistant IA pour analyser ces données.",
"xlsx.feature_cached_values": "valeurs calculées",
"xlsx.feature_cached_values_unverified": "valeurs calculées non vérifiables (classeur trop volumineux)",
"xlsx.feature_slicers": "segments et chronologies",
"xlsx.feature_form_controls": "contrôles de formulaire",
"xlsx.feature_connections": "connexions et requêtes",
@@ -2080,6 +2127,9 @@
"ai.step.xlsx_read": "Classeur lu : {value}",
"ai.step.xlsx_update": "Cellules modifiées : {value}",
"ai.step.xlsx_append": "Lignes ajoutées : {value}",
"ai.step.xlsx_search": "Recherche dans le classeur : {value}",
"ai.step.xlsx_analyze": "Plage analysée : {value}",
"ai.step.xlsx_structure": "Structure modifiée : {value}",
"ai.step.docx_create": "Document Word proposé : {value}",
"ai.step.csv_create": "Fichier CSV proposé : {value}",
"ai.step.pdf_create": "Document PDF proposé : {value}",
+74 -1
View File
@@ -11172,8 +11172,11 @@ body.desktop-mode .editor-container {
gap: 6px;
margin-bottom: 8px;
}
/* #156-A7 — the Name Box is editable (“Atteindre”): a plain input styled like
the read-only badge it replaced. */
.xlsx-active-cell {
min-width: 52px;
width: 84px;
box-sizing: border-box;
padding: 4px 8px;
border: 1px solid var(--border);
border-radius: 4px;
@@ -11184,6 +11187,10 @@ body.desktop-mode .editor-container {
text-align: center;
font-variant-numeric: tabular-nums;
}
.xlsx-active-cell:focus {
outline: 2px solid var(--accent, #4a90d9);
outline-offset: -2px;
}
.xlsx-formula-sep {
width: 14px;
height: 14px;
@@ -11253,6 +11260,72 @@ body.desktop-mode .editor-container {
outline-offset: 1px;
}
/* #156-A11 — export menu (CSV stays a button; MD/HTML/Print live here). */
.xlsx-export-dropdown {
position: relative;
display: inline-block;
}
.xlsx-export-menu {
position: absolute;
right: 0;
top: 100%;
z-index: 1000;
min-width: 200px;
padding: 4px;
background: var(--bg, #fff);
border: 1px solid var(--border);
border-radius: 6px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
}
.xlsx-export-menu .export-menu-item {
display: block;
width: 100%;
padding: 8px 10px;
border: none;
border-radius: 4px;
background: none;
color: var(--text-primary);
cursor: pointer;
text-align: left;
font-size: 13px;
}
.xlsx-export-menu .export-menu-item:hover {
background: var(--surface-hover, rgba(127, 127, 127, 0.12));
}
.xlsx-print-frame {
position: fixed;
width: 0;
height: 0;
border: 0;
}
/* #156-A8 — formatting menu: the colour rows pair a label with the native
picker (data-driven colours, no hardcoded swatch). */
.xlsx-format-menu {
min-width: 230px;
max-height: 70vh;
overflow-y: auto;
}
.xlsx-format-colour {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 10px;
color: var(--text-primary);
font-size: 13px;
cursor: pointer;
}
.xlsx-format-colour input[type="color"] {
width: 34px;
height: 22px;
padding: 0;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
cursor: pointer;
}
/* #153 A15 — freeze panes: frozen rows sit UNDER the sticky thead, frozen
columns stay left. z-index mirrors thead (3) without covering it. */
.xlsx-table tr.xlsx-frozen-row td,