Update README docker-compose syntax and add incremental sidebar refresh
Implement `incrementalLoadDirectory` to update tree items without full DOM rebuild. Modify `refreshSidebarTreePreservingState` to use incremental updates for expanded vaults and directories, preserving existing DOM state.
This commit is contained in:
parent
9ce95eda2d
commit
c65063fbca
@ -105,10 +105,10 @@ volumes:
|
||||
|
||||
```bash
|
||||
# Build local de l'image + démarrage
|
||||
docker-compose up -d --build
|
||||
docker compose up -d --build
|
||||
|
||||
# Vérifier les logs
|
||||
docker-compose logs -f obsigate
|
||||
docker compose logs -f obsigate
|
||||
```
|
||||
|
||||
> **Note** : ObsiGate est construit localement depuis le `Dockerfile` du projet. Sans build local, Docker essaiera de télécharger une image distante `obsigate:latest` qui n'existe pas forcément.
|
||||
|
||||
145
frontend/app.js
145
frontend/app.js
@ -2244,6 +2244,132 @@
|
||||
* Refreshes the sidebar tree while preserving the expanded state of vaults and folders.
|
||||
* Optimized to avoid a full sidebar wipe and minimize visible loading states.
|
||||
*/
|
||||
/**
|
||||
* Incrementally update a directory container without wiping existing DOM.
|
||||
* Only adds new items, removes deleted ones, and updates changed ones.
|
||||
*/
|
||||
async function incrementalLoadDirectory(vaultName, dirPath, container) {
|
||||
let data;
|
||||
try {
|
||||
const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`;
|
||||
data = await api(url);
|
||||
} catch (err) {
|
||||
// Server unavailable — keep existing content
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a map of existing DOM elements by path
|
||||
const existingItems = {};
|
||||
const existingChildren = {}; // path -> child container (for directories)
|
||||
for (let i = 0; i < container.children.length; i++) {
|
||||
const child = container.children[i];
|
||||
if (child.classList.contains("tree-item") && child.dataset.path) {
|
||||
existingItems[child.dataset.path] = child;
|
||||
// The next sibling should be the tree-children container for this directory
|
||||
if (i + 1 < container.children.length) {
|
||||
const next = container.children[i + 1];
|
||||
if (next.classList.contains("tree-children")) {
|
||||
existingChildren[child.dataset.path] = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
data.items.forEach((item) => {
|
||||
if (!shouldDisplayPath(item.path, vaultName)) return;
|
||||
|
||||
const existing = existingItems[item.path];
|
||||
|
||||
if (existing) {
|
||||
// Item already exists — reuse it, but update text/badge if needed
|
||||
const textEl = existing.querySelector(".tree-item-text");
|
||||
const displayName = item.type === "file" && item.name.match(/\.md$/i)
|
||||
? item.name.replace(/\.md$/i, "")
|
||||
: item.name;
|
||||
if (textEl && textEl.textContent !== displayName) {
|
||||
textEl.textContent = displayName;
|
||||
}
|
||||
// Update badge for directories
|
||||
if (item.type === "directory") {
|
||||
const badge = existing.querySelector(".badge-small");
|
||||
const newBadge = `(${item.children_count})`;
|
||||
if (badge && badge.textContent !== newBadge) {
|
||||
badge.textContent = newBadge;
|
||||
} else if (!badge) {
|
||||
existing.appendChild(smallBadge(item.children_count));
|
||||
}
|
||||
}
|
||||
fragment.appendChild(existing);
|
||||
// Also re-add the child container for directories
|
||||
if (item.type === "directory" && existingChildren[item.path]) {
|
||||
fragment.appendChild(existingChildren[item.path]);
|
||||
} else if (item.type === "directory") {
|
||||
// Directory existed but no child container — create one
|
||||
const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
|
||||
fragment.appendChild(subContainer);
|
||||
}
|
||||
} else {
|
||||
// New item — create it
|
||||
if (item.type === "directory") {
|
||||
const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon("chevron-right", 14), icon("folder", 16), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]);
|
||||
attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
|
||||
attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
|
||||
fragment.appendChild(dirItem);
|
||||
|
||||
const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
|
||||
fragment.appendChild(subContainer);
|
||||
|
||||
dirItem.addEventListener("click", async () => {
|
||||
scrollTreeItemIntoView(dirItem, false);
|
||||
if (subContainer.classList.contains("collapsed")) {
|
||||
if (subContainer.children.length === 0) {
|
||||
await loadDirectory(vaultName, item.path, subContainer);
|
||||
}
|
||||
subContainer.classList.remove("collapsed");
|
||||
const chev = dirItem.querySelector("[data-lucide]");
|
||||
if (chev) chev.setAttribute("data-lucide", "chevron-down");
|
||||
safeCreateIcons();
|
||||
} else {
|
||||
subContainer.classList.add("collapsed");
|
||||
const chev = dirItem.querySelector("[data-lucide]");
|
||||
if (chev) chev.setAttribute("data-lucide", "chevron-right");
|
||||
safeCreateIcons();
|
||||
}
|
||||
});
|
||||
|
||||
dirItem.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "directory", false);
|
||||
});
|
||||
} else {
|
||||
const fileIconName = getFileIcon(item.name);
|
||||
const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
|
||||
const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
|
||||
attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
|
||||
attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
|
||||
fileItem.addEventListener("click", () => {
|
||||
scrollTreeItemIntoView(fileItem, false);
|
||||
openFile(vaultName, item.path);
|
||||
closeMobileSidebar();
|
||||
});
|
||||
|
||||
fileItem.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "file", false);
|
||||
});
|
||||
|
||||
fragment.appendChild(fileItem);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Replace container content in a single batch operation to avoid flash
|
||||
container.textContent = "";
|
||||
container.appendChild(fragment);
|
||||
}
|
||||
|
||||
async function refreshSidebarTreePreservingState() {
|
||||
// 1. Capture expanded states
|
||||
const expandedVaults = Array.from(document.querySelectorAll(".vault-item"))
|
||||
@ -2265,7 +2391,7 @@
|
||||
const selectedItem = document.querySelector(".tree-item.path-selected");
|
||||
const selectedState = selectedItem ? { vault: selectedItem.dataset.vault, path: selectedItem.dataset.path } : null;
|
||||
|
||||
// 2. Soft update: load vaults to update names/counts without wiping the tree
|
||||
// 2. Soft update: vault names/counts without wiping the tree
|
||||
try {
|
||||
const vaults = await api("/api/vaults");
|
||||
allVaults = vaults;
|
||||
@ -2279,29 +2405,30 @@
|
||||
} catch (e) {
|
||||
console.warn("Soft vault refresh failed, falling back to full reload", e);
|
||||
await loadVaults();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Refresh expanded vaults
|
||||
// If we didn't wipe the tree, we only need to call loadDirectory to update the children
|
||||
// 3. Incrementally update expanded vaults (no DOM wipe)
|
||||
for (const vName of expandedVaults) {
|
||||
const container = document.getElementById(`vault-children-${vName}`);
|
||||
if (container) {
|
||||
await loadDirectory(vName, "", container);
|
||||
await incrementalLoadDirectory(vName, "", container);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Re-expand directories (parents first)
|
||||
// 4. Incrementally update expanded directories (parents first, no DOM wipe)
|
||||
expandedDirs.sort((a, b) => a.path.split("/").length - b.path.split("/").length);
|
||||
for (const dir of expandedDirs) {
|
||||
const dItem = document.querySelector(`.tree-item[data-vault="${CSS.escape(dir.vault)}"][data-path="${CSS.escape(dir.path)}"]`);
|
||||
const container = document.getElementById(`dir-${dir.vault}-${dir.path}`);
|
||||
if (dItem && container) {
|
||||
// If it was already expanded but currently has its old content, loadDirectory will update it
|
||||
if (container) {
|
||||
try {
|
||||
await loadDirectory(dir.vault, dir.path, container);
|
||||
await incrementalLoadDirectory(dir.vault, dir.path, container);
|
||||
container.classList.remove("collapsed");
|
||||
const dItem = document.querySelector(`.tree-item[data-vault="${CSS.escape(dir.vault)}"][data-path="${CSS.escape(dir.path)}"]`);
|
||||
if (dItem) {
|
||||
const chev = dItem.querySelector("[data-lucide]");
|
||||
if (chev) chev.setAttribute("data-lucide", "chevron-down");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to refresh directory ${dir.vault}/${dir.path}`, e);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user