[Coffre Secrets] Phase 1c UI : noeud virtuel Secrets sous Home-user dans sidebar.js (patron Partage #196)
This commit is contained in:
@@ -4,6 +4,7 @@ import { api, AuthManager } from './auth.js';
|
|||||||
import { populateCustomDropdown, TabManager, closeMobileSidebar, ContextMenuManager } from './ui.js';
|
import { populateCustomDropdown, TabManager, closeMobileSidebar, ContextMenuManager } from './ui.js';
|
||||||
import { _populateRecentVaultFilter, switchSidebarTab, filterAIHistory, filterRecentFiles } from './config.js';
|
import { _populateRecentVaultFilter, switchSidebarTab, filterAIHistory, filterRecentFiles } from './config.js';
|
||||||
import { el, icon, getVaultIcon, smallBadge, attachTreeItemActionButton, attachTreeItemLongPress, showWelcome, appendHighlightedText, filterSavedSearches } from './viewer.js';
|
import { el, icon, getVaultIcon, smallBadge, attachTreeItemActionButton, attachTreeItemLongPress, showWelcome, appendHighlightedText, filterSavedSearches } from './viewer.js';
|
||||||
|
import { openCoffresView, openCoffresNewSecret, openCoffresAcces, openCoffresJournal, getCoffresForSidebar, invalidateCoffresCache } from './coffres.js';
|
||||||
import { performAdvancedSearch } from './search.js';
|
import { performAdvancedSearch } from './search.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
import { filterChatMessages as _filterChat } from './filechat.js';
|
import { filterChatMessages as _filterChat } from './filechat.js';
|
||||||
@@ -46,6 +47,168 @@ function _sharedIcon(size = 16) {
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Coffre Secrets — nœud virtuel « Secrets » sous le Home (Phase 1c, #19).
|
||||||
|
// Patron exact du dossier virtuel « Partage » (#196) : inséré à la racine
|
||||||
|
// du Home de l'utilisateur courant seulement, sans dossier sur disque, sans
|
||||||
|
// passer par l'indexeur. Contrairement à « Partage », le nœud est permanent
|
||||||
|
// (même sans coffre) : cliquer dessus ouvre la vue Secrets, qui crée le
|
||||||
|
// coffre personnel à la première ouverture si besoin.
|
||||||
|
// AUCUNE action de fichier n'est attachée à ce nœud ni à ses enfants :
|
||||||
|
// ni attachTreeItemActionButton, ni ContextMenuManager (qui proposerait
|
||||||
|
// renommer/déplacer/supprimer). Le menu ⋮ est un menu propre ci-dessous.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Icône cadenas (U+1F510) du nœud Secrets, distincte de la maison et du diamant. */
|
||||||
|
function _secretsIcon() {
|
||||||
|
const s = document.createElement("span");
|
||||||
|
s.className = "coffres-icon-lock";
|
||||||
|
s.textContent = "\u{1F510}";
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isOwnHome(vaultName) {
|
||||||
|
const me = (AuthManager.getUser() || {}).username;
|
||||||
|
return !!me && vaultName === `home-${me}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Construit le nœud Secrets + son conteneur d'enfants (coffres). */
|
||||||
|
async function _buildSecretsNode(vaultName) {
|
||||||
|
const pair = document.createDocumentFragment();
|
||||||
|
const item = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": "Secrets", "data-type": "virtual-secrets" }, [icon("chevron-right", 14), _secretsIcon(), el("span", { class: "tree-item-text" }, [document.createTextNode(t("coffres.secrets"))])]);
|
||||||
|
const sub = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-Secrets` });
|
||||||
|
item.addEventListener("click", async () => {
|
||||||
|
scrollTreeItemIntoView(item, false);
|
||||||
|
if (sub.classList.contains("collapsed")) {
|
||||||
|
if (sub.children.length === 0) await _fillSecretsNode(item, sub, vaultName);
|
||||||
|
sub.classList.remove("collapsed");
|
||||||
|
const chev = item.querySelector("[data-lucide]");
|
||||||
|
if (chev) chev.setAttribute("data-lucide", "chevron-down");
|
||||||
|
safeCreateIcons();
|
||||||
|
} else {
|
||||||
|
sub.classList.add("collapsed");
|
||||||
|
const chev = item.querySelector("[data-lucide]");
|
||||||
|
if (chev) chev.setAttribute("data-lucide", "chevron-right");
|
||||||
|
safeCreateIcons();
|
||||||
|
}
|
||||||
|
await openCoffresView();
|
||||||
|
closeMobileSidebar();
|
||||||
|
});
|
||||||
|
item.addEventListener("contextmenu", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
_openSecretsMenu(e.clientX, e.clientY);
|
||||||
|
});
|
||||||
|
const actionBtn = el("button", { class: "tree-item-action-btn", type: "button", title: t("coffres.secrets") }, [document.createTextNode("\u22EE")]);
|
||||||
|
actionBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const r = actionBtn.getBoundingClientRect();
|
||||||
|
_openSecretsMenu(Math.min(r.right, window.innerWidth - 16), r.bottom);
|
||||||
|
});
|
||||||
|
item.appendChild(actionBtn);
|
||||||
|
pair.appendChild(item);
|
||||||
|
pair.appendChild(sub);
|
||||||
|
// Compteur et enfants, en arrière-plan : le nœud existe même si l'API échoue.
|
||||||
|
_fillSecretsNode(item, sub, vaultName).catch(() => {});
|
||||||
|
return pair;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remplit le compteur et les enfants (coffres) d'un nœud Secrets existant. */
|
||||||
|
async function _fillSecretsNode(item, sub, vaultName) {
|
||||||
|
const data = await getCoffresForSidebar();
|
||||||
|
item.querySelectorAll(".badge-small, .coffres-alert-dot").forEach((n) => n.remove());
|
||||||
|
if (!data.disabled && data.personnel) {
|
||||||
|
if (!data.personnel.kek_configuree) {
|
||||||
|
const dot = document.createElement("span");
|
||||||
|
dot.className = "coffres-alert-dot";
|
||||||
|
dot.textContent = "\u26A0";
|
||||||
|
dot.title = t("coffres.kek_absente");
|
||||||
|
item.appendChild(dot);
|
||||||
|
} else {
|
||||||
|
item.appendChild(smallBadge(data.personnel.nb_secrets));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sub.innerHTML = "";
|
||||||
|
const coffres = (data.coffres || []).slice().sort((a, b) => {
|
||||||
|
const aPers = a.proprietaire === (AuthManager.getUser() || {}).username && a.perimetre === "personnel" ? 0 : 1;
|
||||||
|
const bPers = b.proprietaire === (AuthManager.getUser() || {}).username && b.perimetre === "personnel" ? 0 : 1;
|
||||||
|
return aPers - bPers || String(a.nom).localeCompare(String(b.nom));
|
||||||
|
});
|
||||||
|
coffres.forEach((c) => {
|
||||||
|
const me = (AuthManager.getUser() || {}).username;
|
||||||
|
const label = c.proprietaire === me ? c.nom : `${c.nom} (${c.proprietaire})`;
|
||||||
|
const child = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": `Secrets/${c.slug}`, "data-type": "virtual-coffre", "data-coffre-slug": c.slug }, [icon("lock", 16), el("span", { class: "tree-item-text" }, [document.createTextNode(label)]), smallBadge(c.nb_secrets)]);
|
||||||
|
child.addEventListener("click", async () => {
|
||||||
|
scrollTreeItemIntoView(child, false);
|
||||||
|
await openCoffresView(c.slug);
|
||||||
|
closeMobileSidebar();
|
||||||
|
});
|
||||||
|
child.addEventListener("contextmenu", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
_openSecretsMenu(e.clientX, e.clientY);
|
||||||
|
});
|
||||||
|
sub.appendChild(child);
|
||||||
|
});
|
||||||
|
safeCreateIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rafraîchit tous les nœuds Secrets affichés (après création, rotation, révocation…). */
|
||||||
|
async function _refreshSecretsNodes() {
|
||||||
|
const items = Array.from(document.querySelectorAll('[data-type="virtual-secrets"]'));
|
||||||
|
for (const item of items) {
|
||||||
|
const vaultName = item.dataset.vault;
|
||||||
|
const sub = item.nextElementSibling;
|
||||||
|
if (sub && sub.classList.contains("tree-children")) {
|
||||||
|
await _fillSecretsNode(item, sub, vaultName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.__obsigateSecretsListener) {
|
||||||
|
window.__obsigateSecretsListener = true;
|
||||||
|
document.addEventListener("obsigate:coffres-changed", () => {
|
||||||
|
invalidateCoffresCache();
|
||||||
|
_refreshSecretsNodes().catch(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Menu ⋮ du nœud Secrets : Ouvrir, Nouveau secret, Gérer les accès, Journal.
|
||||||
|
* Menu propre, jamais ContextMenuManager (pas d'action de fichier). */
|
||||||
|
function _openSecretsMenu(x, y) {
|
||||||
|
document.querySelectorAll(".coffres-context-menu").forEach((n) => n.remove());
|
||||||
|
const menu = el("div", { class: "coffres-context-menu" }, []);
|
||||||
|
const entries = [
|
||||||
|
[t("coffres.detail"), () => openCoffresView()],
|
||||||
|
[t("coffres.new_secret"), () => openCoffresNewSecret()],
|
||||||
|
[t("coffres.acces"), () => openCoffresAcces()],
|
||||||
|
[t("coffres.journal"), () => openCoffresJournal()],
|
||||||
|
];
|
||||||
|
entries.forEach(([label, run]) => {
|
||||||
|
const b = el("button", { class: "coffres-context-item", type: "button" }, [document.createTextNode(label)]);
|
||||||
|
b.addEventListener("click", async () => {
|
||||||
|
menu.remove();
|
||||||
|
await run();
|
||||||
|
closeMobileSidebar();
|
||||||
|
});
|
||||||
|
menu.appendChild(b);
|
||||||
|
});
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
const rect = menu.getBoundingClientRect();
|
||||||
|
menu.style.left = Math.max(8, Math.min(x, window.innerWidth - rect.width - 8)) + "px";
|
||||||
|
menu.style.top = Math.max(8, Math.min(y, window.innerHeight - rect.height - 8)) + "px";
|
||||||
|
const closer = (e) => {
|
||||||
|
if (!menu.contains(e.target)) {
|
||||||
|
menu.remove();
|
||||||
|
document.removeEventListener("click", closer, true);
|
||||||
|
document.removeEventListener("keydown", onKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKey = (e) => { if (e.key === "Escape") { menu.remove(); document.removeEventListener("click", closer, true); document.removeEventListener("keydown", onKey); } };
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener("click", closer, true);
|
||||||
|
document.addEventListener("keydown", onKey);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/** Fichier partagé (créé ou reçu) → icône share-2 bleue, sinon icône usuelle. */
|
/** Fichier partagé (créé ou reçu) → icône share-2 bleue, sinon icône usuelle. */
|
||||||
function sharedFileIconSync(sharedPaths, itemName, vaultName, itemPath) {
|
function sharedFileIconSync(sharedPaths, itemName, vaultName, itemPath) {
|
||||||
if (sharedPaths && sharedPaths.has(`${vaultName}::${itemPath}`)) return _sharedIcon();
|
if (sharedPaths && sharedPaths.has(`${vaultName}::${itemPath}`)) return _sharedIcon();
|
||||||
@@ -494,6 +657,12 @@ async function incrementalLoadDirectory(vaultName, dirPath, container) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Coffre Secrets (Phase 1c) : même nœud virtuel que dans loadDirectory.
|
||||||
|
if (me && vaultName === `home-${me}` && !dirPath) {
|
||||||
|
const pair = await _buildSecretsNode(vaultName);
|
||||||
|
if (pair) fragment.insertBefore(pair, fragment.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
// Replace container content in a single batch operation to avoid flash
|
// Replace container content in a single batch operation to avoid flash
|
||||||
container.textContent = "";
|
container.textContent = "";
|
||||||
container.appendChild(fragment);
|
container.appendChild(fragment);
|
||||||
@@ -838,6 +1007,14 @@ async function loadDirectory(vaultName, dirPath, container) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Coffre Secrets (Phase 1c) : nœud virtuel permanent en tête, à la racine
|
||||||
|
// du Home de l'utilisateur courant seulement, jamais sous un autre Home
|
||||||
|
// ni sous un vault de notes, et jamais comme un dossier physique.
|
||||||
|
if (me && vaultName === `home-${me}` && !dirPath) {
|
||||||
|
const pair = await _buildSecretsNode(vaultName);
|
||||||
|
if (pair) fragment.insertBefore(pair, fragment.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
container.appendChild(fragment);
|
container.appendChild(fragment);
|
||||||
safeCreateIcons();
|
safeCreateIcons();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user