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
|
```bash
|
||||||
# Build local de l'image + démarrage
|
# Build local de l'image + démarrage
|
||||||
docker-compose up -d --build
|
docker compose up -d --build
|
||||||
|
|
||||||
# Vérifier les logs
|
# 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.
|
> **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.
|
* 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.
|
* 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() {
|
async function refreshSidebarTreePreservingState() {
|
||||||
// 1. Capture expanded states
|
// 1. Capture expanded states
|
||||||
const expandedVaults = Array.from(document.querySelectorAll(".vault-item"))
|
const expandedVaults = Array.from(document.querySelectorAll(".vault-item"))
|
||||||
@ -2265,7 +2391,7 @@
|
|||||||
const selectedItem = document.querySelector(".tree-item.path-selected");
|
const selectedItem = document.querySelector(".tree-item.path-selected");
|
||||||
const selectedState = selectedItem ? { vault: selectedItem.dataset.vault, path: selectedItem.dataset.path } : null;
|
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 {
|
try {
|
||||||
const vaults = await api("/api/vaults");
|
const vaults = await api("/api/vaults");
|
||||||
allVaults = vaults;
|
allVaults = vaults;
|
||||||
@ -2279,29 +2405,30 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Soft vault refresh failed, falling back to full reload", e);
|
console.warn("Soft vault refresh failed, falling back to full reload", e);
|
||||||
await loadVaults();
|
await loadVaults();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Refresh expanded vaults
|
// 3. Incrementally update expanded vaults (no DOM wipe)
|
||||||
// If we didn't wipe the tree, we only need to call loadDirectory to update the children
|
|
||||||
for (const vName of expandedVaults) {
|
for (const vName of expandedVaults) {
|
||||||
const container = document.getElementById(`vault-children-${vName}`);
|
const container = document.getElementById(`vault-children-${vName}`);
|
||||||
if (container) {
|
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);
|
expandedDirs.sort((a, b) => a.path.split("/").length - b.path.split("/").length);
|
||||||
for (const dir of expandedDirs) {
|
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}`);
|
const container = document.getElementById(`dir-${dir.vault}-${dir.path}`);
|
||||||
if (dItem && container) {
|
if (container) {
|
||||||
// If it was already expanded but currently has its old content, loadDirectory will update it
|
|
||||||
try {
|
try {
|
||||||
await loadDirectory(dir.vault, dir.path, container);
|
await incrementalLoadDirectory(dir.vault, dir.path, container);
|
||||||
container.classList.remove("collapsed");
|
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]");
|
const chev = dItem.querySelector("[data-lucide]");
|
||||||
if (chev) chev.setAttribute("data-lucide", "chevron-down");
|
if (chev) chev.setAttribute("data-lucide", "chevron-down");
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to refresh directory ${dir.vault}/${dir.path}`, e);
|
console.error(`Failed to refresh directory ${dir.vault}/${dir.path}`, e);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user