feat: completude de l'editeur Excel - mise en forme, sortie, concurrence, cache, outils IA #156
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:
@@ -42,6 +42,7 @@ from backend.schemas import (
|
||||
XlsxSheetWindowResponse,
|
||||
)
|
||||
from backend.services.files import read_raw_file
|
||||
from backend.services.mutations import file_revision
|
||||
from backend.services.paths import resolve_safe_path
|
||||
from backend.services.vaults import browse_directory, get_vault_root
|
||||
|
||||
@@ -226,7 +227,7 @@ def api_file_xlsx_dashboard(
|
||||
)
|
||||
def api_file_xlsx_sheet(
|
||||
vault_name: str,
|
||||
path: str = Query(..., description="Relative path to the .xlsx file"),
|
||||
path: str = Query(..., description="Relative path to the .xlsx/.xlsm file"),
|
||||
sheet: str = Query(..., description="Sheet name (as shown in the viewer tab)"),
|
||||
offset: int = Query(0, ge=0, description="0-based index of the first row to return"),
|
||||
limit: int = Query(
|
||||
@@ -234,7 +235,7 @@ def api_file_xlsx_sheet(
|
||||
),
|
||||
current_user=Depends(require_auth),
|
||||
):
|
||||
"""Return a window of rows of one sheet of an .xlsx workbook (#153 A9).
|
||||
"""Return a window of rows of one sheet of an .xlsx/.xlsm workbook (#153 A9).
|
||||
|
||||
Backs the viewer's lazy loading: instead of every sheet in a single JSON
|
||||
payload, the client asks for the block it is about to display. The row
|
||||
@@ -258,7 +259,7 @@ def api_file_xlsx_sheet(
|
||||
|
||||
Raises:
|
||||
HTTPException: 403 (vault access), 404 (vault, file or sheet unknown),
|
||||
415 (not an .xlsx file), 500 (unreadable workbook).
|
||||
415 (not an .xlsx/.xlsm file), 500 (unreadable workbook).
|
||||
"""
|
||||
if not check_vault_access(vault_name, current_user):
|
||||
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
||||
@@ -269,8 +270,12 @@ def api_file_xlsx_sheet(
|
||||
file_path = resolve_safe_path(Path(vault_data["path"]), path)
|
||||
if not file_path.is_file():
|
||||
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
||||
if file_path.suffix.lower() != ".xlsx":
|
||||
raise HTTPException(status_code=415, detail="Le fichier n'est pas un classeur .xlsx")
|
||||
# BUG-097 — a .xlsm rides the same editable viewer (and its lazy loading),
|
||||
# so its row windows must be servable too; other formats stay refused.
|
||||
if file_path.suffix.lower() not in (".xlsx", ".xlsm"):
|
||||
raise HTTPException(
|
||||
status_code=415, detail="Le fichier n'est pas un classeur .xlsx/.xlsm"
|
||||
)
|
||||
|
||||
# Import tardif : openpyxl n'est chargé que si un .xlsx est réellement demandé.
|
||||
from backend.xlsx_reader import read_sheet_window
|
||||
@@ -368,6 +373,10 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
|
||||
"is_markdown": False,
|
||||
"is_xlsx": True,
|
||||
"xlsx_sheets": sheets,
|
||||
# #156-A12 — optimistic-concurrency token: the viewer sends it
|
||||
# back as `if_match` so another writer cannot be overwritten in
|
||||
# silence (409 `conflict` instead).
|
||||
"xlsx_revision": file_revision(file_path),
|
||||
# #153 A1 — parts a save would drop; the viewer warns and asks
|
||||
# for an explicit confirmation before forcing the write.
|
||||
"xlsx_lossy_features": inspect_workbook(file_path),
|
||||
@@ -507,6 +516,7 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
|
||||
"is_markdown": False,
|
||||
"is_xlsx": True,
|
||||
"xlsx_sheets": sheets,
|
||||
"xlsx_revision": file_revision(file_path),
|
||||
# Macros are NOT lossy for .xlsm: keep_vba re-serializes them
|
||||
# (an empty LOSSY probe is what makes the save gate pass).
|
||||
"xlsx_lossy_features": [],
|
||||
@@ -554,6 +564,8 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
|
||||
"title": file_path.name, "tags": [], "frontmatter": {},
|
||||
"html": html, "raw_length": len(raw), "extension": ext,
|
||||
"is_markdown": False, "is_csv": True,
|
||||
# #156-A12 — same stale-write guard as the workbooks.
|
||||
"xlsx_revision": file_revision(file_path),
|
||||
}
|
||||
|
||||
# === JSON: syntax-highlighted display ===
|
||||
|
||||
Reference in New Issue
Block a user