fix(search,excalidraw,e2e): recherche cassee + corruption .excalidraw.md + tests E2E #78
CI / lint (push) Successful in 54s
CI / security (push) Successful in 38s
CI / test (push) Successful in 1m7s
CI / build (push) Successful in 33s
CI / e2e (push) Failing after 10m52s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

- frontend/js/search.js : onSearchFilter appelee avec (vault, query, results) — renvoyait undefined, cassait le rendu et basculait sur la recherche offline (0 resultat). Regression plugins depuis 30f2df3.
- frontend/excalidraw-editor.html + js/excalidraw-viewer.js : ignorer onChange au montage (plus d'auto-save spurieux qui reecrivait .excalidraw.md en JSON brut) + preservation du format Obsidian frontmatter + compressed-json a la sauvegarde.
- frontend/js/app.js : __OBSIGATE_BOOTED pose apres init() (splash + E2E attendent les handlers lies).
- tests/e2e : goHome/login attendent le boot ; excalidraw.spec.js reecrit avec les selecteurs reels (modal, context menu, canvas.first()).
- ROADMAP : C8 (creation via menu contextuel) marque fait.
- Verifie : 86/86 E2E chromium-desktop, 599 backend, ruff, validate-imports, JSDOM.
This commit is contained in:
2026-09-10 22:51:08 -04:00
parent 4f99d5ce61
commit 0cbbbf260a
8 changed files with 116 additions and 140 deletions
+28 -11
View File
@@ -102,7 +102,7 @@
<script type="module">
import React from "https://esm.sh/react@^19.2.0?target=es2022";
import { createRoot } from "https://esm.sh/react-dom@^19.2.0/client?target=es2022";
import { decompressFromBase64 } from "https://esm.sh/[email protected]";
import { decompressFromBase64, compressToBase64 } from "https://esm.sh/[email protected]";
import * as ExcalidrawLib from "https://esm.sh/@excalidraw/[email protected]";
window.React = React;
@@ -116,6 +116,12 @@
let isDirty = false;
let currentTheme = "light";
let saveTimeout = null;
// Excalidraw fires onChange on initial mount; ignore those so simply
// opening a diagram does not trigger a spurious autosave (which would
// rewrite a .excalidraw.md file as raw JSON and destroy its format).
let ignoreChanges = true;
// Source format: "md" for the Obsidian .excalidraw.md (compressed-json).
let sourceIsMd = false;
// --- DOM refs ---
const loadingEl = document.getElementById("loading");
@@ -153,16 +159,23 @@
const elements = excalidrawAPI.getSceneElements();
const appState = excalidrawAPI.getAppState();
const files = excalidrawAPI.getFiles();
sendToParent({
type: "save",
data: {
type: "excalidraw",
version: 2,
elements: elements,
appState: appState,
files: files,
},
});
const data = {
type: "excalidraw",
version: 2,
elements: elements,
appState: appState,
files: files,
};
// Preserve the Obsidian .excalidraw.md format: ship the compressed
// scene so the parent can rewrite the frontmatter + compressed-json block.
if (sourceIsMd) {
try {
data.compressed = compressToBase64(JSON.stringify(data));
} catch (e) {
console.error("Failed to compress Excalidraw scene:", e);
}
}
sendToParent({ type: "save", data });
markClean();
btnSave.textContent = "💾 Saved!";
setTimeout(() => { btnSave.textContent = "💾 Save"; }, 1200);
@@ -191,10 +204,13 @@
if (theme === "dark") {
api.updateScene({ appState: { theme: "dark" } });
}
// Stop ignoring changes once the initial mount settles.
setTimeout(() => { ignoreChanges = false; }, 800);
}
}, [theme]);
const onChange = React.useCallback((elements, state, files) => {
if (ignoreChanges) return;
markDirty();
}, []);
@@ -225,6 +241,7 @@
let elements, appState, files;
// Handle compressed .excalidraw.md format
if (msg.data.compressed) {
sourceIsMd = true;
try {
const decompressed = decompressFromBase64(msg.data.compressed);
const parsed = JSON.parse(decompressed);
+3 -2
View File
@@ -113,13 +113,14 @@ async function init() {
initAIFab();
}
document.addEventListener("DOMContentLoaded", () => {
document.addEventListener("DOMContentLoaded", async () => {
try {
init();
await init();
Sync.init();
} finally {
// Signale au splash de démarrage (index.html #boot-splash) que l'app est
// initialisée : le splash est retiré et le squelette stylé révélé.
// Set AFTER init() resolves so every handler (search, tabs, …) is bound.
window.__OBSIGATE_BOOTED = true;
if (typeof window.__obsigateBootReady === "function") window.__obsigateBootReady();
}
+13 -2
View File
@@ -114,11 +114,22 @@ export function renderExcalidraw(container, data, vaultName, filePath, opts = {}
*/
async function _saveExcalidrawFile(state, excalidrawData) {
try {
const jsonStr = JSON.stringify(excalidrawData, null, 2);
const isMd = (state.path || '').toLowerCase().endsWith('.excalidraw.md');
let content;
if (isMd && excalidrawData && excalidrawData.compressed) {
// Preserve the Obsidian Excalidraw plugin format: frontmatter +
// compressed-json block. Writing raw JSON here would destroy the
// .excalidraw.md file (the plugin would no longer recognize it).
content = `---\nexcalidraw-plugin: parsed\n---\n\n\`\`\`compressed-json\n${excalidrawData.compressed}\n\`\`\`\n`;
} else {
// Transport-only field must not leak into the pure JSON format.
const { compressed, ...scene } = excalidrawData || {};
content = JSON.stringify(scene, null, 2);
}
await api(`/api/file/${encodeURIComponent(state.vault)}/save?path=${encodeURIComponent(state.path)}&backup=false`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: jsonStr }),
body: JSON.stringify({ content }),
});
// Toast is handled by the caller or we show a quick one
showToast('Excalidraw saved', 'success');
+1 -1
View File
@@ -804,7 +804,7 @@ export async function performAdvancedSearch(query, vaultFilter, tagFilter, offse
state.advancedSearchTotal = data.total;
state.advancedSearchOffset = ofs;
// Plugin hook: filter search results
data.results = await onSearchFilter(state.vault || "default", data.results);
data.results = await onSearchFilter(state.vault || "default", query, data.results);
renderAdvancedSearchResults(data, query, tagFilter);
} catch (err) {
clearTimeout(timeoutId);