fix(editeur): #93 garde-fous de session d'edition inline (onglets, ecriture externe) + hooks de versionnage documentes
CI / lint (push) Successful in 1m27s
CI / security (push) Successful in 57s
CI / test (push) Successful in 2m52s
CI / build (push) Successful in 53s
CI / e2e (push) Successful in 10m45s

Une session d'edition inline (Editer/Forge) qui possede la zone de contenu etait detruite par deux chemins qui vident cette zone : activation d'onglet (TabManager.activate, PaneTabManager.activate) -> detachInlineEditor() avant le placeholder ; evenement SSE index_updated -> reloadExternalWrite() recharge le tampon de l'editeur au lieu de re-rendre la vue lecture. Nouveau helper queryEditor() ; closeEditor() remet le conteneur dans la modale avant de restaurer en-tete/pied/marque. docs/CONTRIBUTING.md documente scripts/install-hooks.sh.
This commit is contained in:
2026-09-16 11:43:41 -04:00
parent 168261964e
commit 788d84a2bf
19 changed files with 155 additions and 38 deletions
+11
View File
@@ -69,6 +69,17 @@ export function isInlineEditorActive() {
return !!(container && container.classList.contains(INLINE_CONTAINER_CLASS));
}
/**
* Query an element inside the editor container, wherever it currently lives
* (modal or content area). Use it instead of `modal.querySelector(...)` for the
* header/footer/brand, which move with the container in inline mode (#93).
*/
export function queryEditor(selector) {
const container = getEditorContainer();
const scope = container || getEditorModal();
return scope ? scope.querySelector(selector) : null;
}
/**
* Move the editor container into `area`, replacing the rendered document.
* @returns {boolean} true when the container was mounted inline.
+4 -1
View File
@@ -10,7 +10,7 @@
*/
import { state } from './state.js';
import { api } from './auth.js';
import { escapeHtml, getFileIcon, safeCreateIcons } from './utils.js';
import { escapeHtml, getFileIcon, safeCreateIcons, detachInlineEditor } from './utils.js';
const PANE_MAX = 4;
@@ -154,6 +154,9 @@ function createPaneTabManager(paneId) {
async activate(tabId) {
if (this._activeTabId === tabId && this._tabs.length > 0) return;
// #93 — an inline edition session owns this pane's content area: release
// it before the load placeholder / read view wipes it off the DOM.
detachInlineEditor();
if (this._activeTabId && this._tabCache[this._activeTabId])
this._saveCurrentTabState();
+6 -2
View File
@@ -6,7 +6,7 @@ import { api, AuthManager } from './auth.js';
import { loadVaults, loadTags, refreshTagsForContext, refreshSidebarTreePreservingState } from './sidebar.js';
import { loadRecentFiles, initAboutModal } from './config.js';
import { openFile } from './viewer.js';
import { closeEditor } from './utils.js';
import { closeEditor, reloadExternalWrite } from './utils.js';
import { initOffline, syncFileIndexFromServer } from './offline.js';
import { initThemes } from './themes.js';
@@ -166,7 +166,11 @@ export const IndexUpdateManager = (() => {
if (state.currentVault && state.currentPath) {
const changed = (data.changes || []).some((c) => c.vault === state.currentVault && c.path === state.currentPath);
if (changed) {
openFile(state.currentVault, state.currentPath);
// #93 — an inline edition session (or Forge) owns the content area:
// re-render the read view below it would destroy the editor mid-
// session. Reload the editor buffer from disk instead (same path as
// an external AI write).
reloadExternalWrite(state.currentVault, state.currentPath);
}
}
} catch (err) {
+6 -2
View File
@@ -2,7 +2,7 @@
import { api, AuthManager, initLoginForm } from './auth.js';
import { state } from './state.js';
import { openFile, showWelcome, renderFile, el } from './viewer.js';
import { safeCreateIcons, getFileIcon, escapeHtml } from './utils.js';
import { safeCreateIcons, getFileIcon, escapeHtml, detachInlineEditor } from './utils.js';
import { syncActiveFileTreeItem, refreshSidebarTreePreservingState } from './sidebar.js';
import { GraphViewManager } from './graph.js';
import { DashboardConflictsWidget } from './dashboard.js';
@@ -2014,7 +2014,11 @@ export const TabManager = {
/** Activate a specific tab */
async activate(tabId) {
if (this._activeTabId === tabId && this._tabs.length > 0) return;
// #93 — an inline edition session owns this content area: release it before
// the load placeholder / read view wipes it off the DOM.
detachInlineEditor();
// Save current tab state
if (this._activeTabId && this._tabCache[this._activeTabId]) {
this._saveCurrentTabState();
+14 -13
View File
@@ -6,9 +6,11 @@ import { createAIToolbar } from './ai.js';
import { startCollab, stopCollab } from './collab.js';
import {
getInlineHostArea,
getEditorContainer,
mountEditorInline,
unmountEditorInline,
isInlineEditorActive,
queryEditor,
} from './editor-inline.js';
// ---------------------------------------------------------------------------
@@ -372,9 +374,9 @@ async function openEditor(vaultName, filePath) {
const bodyEl = document.getElementById("editor-body");
// Restore header/footer (may be hidden by Forge)
var headerEl = modal.querySelector(".editor-header");
var footerEl = modal.querySelector(".editor-footer");
var container = modal.querySelector(".editor-container");
var headerEl = queryEditor(".editor-header");
var footerEl = queryEditor(".editor-footer");
var container = getEditorContainer();
if (headerEl) headerEl.style.display = "";
if (footerEl) footerEl.style.display = "";
if (container) container.style.maxHeight = "";
@@ -580,14 +582,6 @@ function closeEditor() {
const modal = document.getElementById("editor-modal");
if (!modal) return;
modal.classList.remove("active");
// Restore brand text
var brand = modal.querySelector('.editor-brand');
if (brand) brand.textContent = 'ObsiGate';
// Restore header/footer (may be hidden by Forge)
var headerEl = modal.querySelector(".editor-header");
var footerEl = modal.querySelector(".editor-footer");
if (headerEl) headerEl.style.display = "";
if (footerEl) footerEl.style.display = "";
stopCollab();
if (state.editorView) {
state.editorView.destroy();
@@ -598,9 +592,16 @@ function closeEditor() {
// Remove forge iframe if present
var forgeFrame = document.getElementById("forge-iframe");
if (forgeFrame) forgeFrame.remove();
// #93 — give the editor container back to the modal; an inline session also
// releases the content area, whose read view must be re-rendered.
// #93 — hand the editor container back to the modal BEFORE restoring the
// header/footer/brand: they live inside the container and therefore travel
// with it in inline mode (a `modal.querySelector` would miss them).
const host = unmountEditorInline();
var brand = queryEditor('.editor-brand');
if (brand) brand.textContent = 'ObsiGate';
var headerEl = queryEditor(".editor-header");
var footerEl = queryEditor(".editor-footer");
if (headerEl) headerEl.style.display = "";
if (footerEl) footerEl.style.display = "";
_invalidateActiveTabCache(vault, path);
state.editorVault = null;
state.editorPath = null;
+4 -3
View File
@@ -2,7 +2,7 @@
import { api, AuthManager } from './auth.js';
import { state } from './state.js';
import { escapeHtml, safeCreateIcons, safeHighlight, getFileIcon, openEditor, copyToClipboard, activateInlineEditor, detachInlineEditor } from './utils.js';
import { isInlineEditorActive } from './editor-inline.js';
import { isInlineEditorActive, queryEditor } from './editor-inline.js';
import { TabManager, closeMobileSidebar, ContextMenuManager, RightSidebarManager, showToast, buildFrontmatterCard } from './ui.js';
import { syncActiveFileTreeItem, searchByTag, TagFilterService, refreshSidebarTreePreservingState, focusPathInSidebar } from './sidebar.js';
import { AutocompleteDropdown, performAdvancedSearch } from './search.js';
@@ -768,8 +768,9 @@ export function renderFile(data) {
state.editorVault = data.vault;
state.editorPath = data.path;
}
// Update header: show "Forge" brand + filename
var brand = modal.querySelector(".editor-brand");
// Update header: show "Forge" brand + filename (the header travels with the
// container in inline mode — query it wherever it lives, #93)
var brand = queryEditor(".editor-brand");
var titleInp = document.getElementById("editor-title-input");
var fileInp = document.getElementById("editor-filename-input");
if (brand) brand.textContent = t("viewer.forge_brand");