feat(excalidraw): support complet des fichiers .excalidraw (#78)
CI / lint (push) Failing after 9s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 13s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled

- Backend: ajout .excalidraw dans SUPPORTED_EXTENSIONS + squelette JSON creation
- Backend: détection is_excalidraw dans api_file() + FileContentResponse
- Frontend: icône pen-tool dans EXT_ICONS
- Frontend: ajout dans la modale de création (ui.js)
- Frontend: excalidraw-editor.html (iframe autonome, React+Excalidraw via esm.sh)
- Frontend: excalidraw-viewer.js (postMessage, auto-save 2s, thème)
- Frontend: dispatch dans viewer.js (renderFile)
- Tests: 6 tests (détection, création squelette, fallback, tree, roundtrip)
- 359/359 tests passent
This commit is contained in:
2026-07-29 10:55:00 -04:00
parent ff38dd086e
commit 1b5319fde1
8 changed files with 664 additions and 1 deletions
+1
View File
@@ -63,6 +63,7 @@ SUPPORTED_EXTENSIONS = {
".java", ".c", ".cpp", ".h", ".hpp", ".cs", ".go", ".rs", ".rb",
".php", ".sql", ".r", ".m", ".swift", ".kt",
".dockerfile", ".makefile", ".cmake",
".excalidraw",
}
+44 -1
View File
@@ -118,6 +118,15 @@ class FileContentResponse(BaseModel):
is_markdown: bool = Field(description="Whether the file is markdown")
unsupported: bool | None = Field(default=False, description="True for binary/unsupported files")
size_bytes: int | None = Field(default=None, description="File size in bytes (for unsupported files)")
is_pdf: bool | None = Field(default=None, description="True for PDF files")
is_image: bool | None = Field(default=None, description="True for image files")
is_csv: bool | None = Field(default=None, description="True for CSV files")
is_json: bool | None = Field(default=None, description="True for JSON files")
is_excalidraw: bool | None = Field(default=None, description="True for Excalidraw diagram files")
excalidraw_data: dict[str, Any] | None = Field(default=None, description="Excalidraw diagram data (elements, appState, files)")
pdf_metadata: dict[str, Any] | None = Field(default=None, description="PDF metadata")
pdf_toc: list[dict[str, Any]] | None = Field(default=None, description="PDF table of contents")
image_mime: str | None = Field(default=None, description="MIME type for image files")
class FileRawResponse(BaseModel):
@@ -1793,8 +1802,13 @@ async def api_file_create(
# Create parent directories if needed
file_path.parent.mkdir(parents=True, exist_ok=True)
# For .excalidraw files, inject skeleton JSON if content is empty
content = body.content
if ext == ".excalidraw" and not content.strip():
content = '{"type":"excalidraw","version":2,"elements":[],"appState":{"viewBackgroundColor":"#ffffff"},"files":{}}'
# Create file with initial content
file_path.write_text(body.content, encoding="utf-8")
file_path.write_text(content, encoding="utf-8")
logger.info(f"File created: {vault_name}/{body.path}")
# Update index
@@ -2489,6 +2503,35 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
"is_markdown": False, "is_json": True,
}
# === Excalidraw: diagram editor ===
if ext == ".excalidraw":
import json as json_mod
try:
parsed = json_mod.loads(raw)
except json_mod.JSONDecodeError:
parsed = None
if parsed and parsed.get("type") == "excalidraw":
return {
"vault": vault_name,
"path": path,
"title": parsed.get("appState", {}).get("name") or file_path.name,
"tags": [],
"frontmatter": {},
"html": "",
"raw_length": len(raw),
"extension": ext,
"is_markdown": False,
"is_excalidraw": True,
"excalidraw_data": {
"elements": parsed.get("elements", []),
"appState": parsed.get("appState", {}),
"files": parsed.get("files", {}),
},
}
else:
# Not a valid Excalidraw file — fall through to text viewer
pass
# === Plain text / other readable files ===
TEXT_EXTENSIONS = {".txt", ".log", ".yml", ".yaml", ".toml", ".ini", ".cfg",
".sh", ".bash", ".py", ".js", ".ts", ".html", ".css",