feat: visibilité & recherche des partages dirigés #196
- icône share-2 bleue sur les fichiers partagés dans l'arborescence
- dossier virtuel Partage/ Ã la racine du home du destinataire (lecture seule /s/{token})
- recherche plein texte couvre les documents reçus (merge search_vaults, share_token)
- cache front invalidé à la création/révocation ; +2 tests non-fuite
This commit is contained in:
+127
-8
@@ -1,6 +1,6 @@
|
||||
import { state } from './state.js';
|
||||
import { safeCreateIcons, getFileIcon, flushIcons } from './utils.js';
|
||||
import { api } from './auth.js';
|
||||
import { api, AuthManager } from './auth.js';
|
||||
import { populateCustomDropdown, TabManager, closeMobileSidebar, ContextMenuManager } from './ui.js';
|
||||
import { _populateRecentVaultFilter, switchSidebarTab, filterAIHistory, filterRecentFiles } from './config.js';
|
||||
import { el, icon, getVaultIcon, smallBadge, attachTreeItemActionButton, attachTreeItemLongPress, showWelcome, appendHighlightedText, filterSavedSearches } from './viewer.js';
|
||||
@@ -8,6 +8,47 @@ import { performAdvancedSearch } from './search.js';
|
||||
import { t } from './i18n.js';
|
||||
import { filterChatMessages as _filterChat } from './filechat.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared-file decoration (#196) : fichier porteur d'un partage actif →
|
||||
// icône « share » dans l'arborescence ; dossier virtuel « Partage » dans le
|
||||
// home du destinataire.
|
||||
// ---------------------------------------------------------------------------
|
||||
let _sharedPaths = null; // Promise<Set<"vault::path">> — parts actives (créées ou reçues)
|
||||
|
||||
function loadSharedPaths() {
|
||||
if (!_sharedPaths) {
|
||||
_sharedPaths = api("/api/shares")
|
||||
.then(shares => new Set(shares.map(s => `${s.vault}::${s.path}`)))
|
||||
.catch(() => new Set());
|
||||
}
|
||||
return _sharedPaths;
|
||||
}
|
||||
|
||||
function invalidateSharedPaths() { _sharedPaths = null; }
|
||||
|
||||
/** Partages dirigés reçus par l'utilisateur courant (#196). */
|
||||
async function _loadReceivedShares() {
|
||||
const me = (AuthManager.getUser() || {}).username;
|
||||
if (!me) return [];
|
||||
try {
|
||||
const shares = await api("/api/shares");
|
||||
return shares.filter(s => s.created_by && s.created_by !== me);
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
/** Icône « partagé » (#196) — share-2 bleu. */
|
||||
function _sharedIcon(size = 16) {
|
||||
const i = icon("share-2", size);
|
||||
i.style.color = "var(--accent-blue, #3b82f6)";
|
||||
return i;
|
||||
}
|
||||
|
||||
/** Fichier partagé (créé ou reçu) → icône share-2 bleue, sinon icône usuelle. */
|
||||
function sharedFileIconSync(sharedPaths, itemName, vaultName, itemPath) {
|
||||
if (sharedPaths && sharedPaths.has(`${vaultName}::${itemPath}`)) return _sharedIcon();
|
||||
return icon(getFileIcon(itemName), 16);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vault context switching
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -306,7 +347,45 @@ async function incrementalLoadDirectory(vaultName, dirPath, container) {
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// #196 : icône « partagé » — résout le cache avant le rendu.
|
||||
const sharedPaths = await loadSharedPaths();
|
||||
|
||||
// #196 : dossier virtuel « Partage » à la racine du home du destinataire.
|
||||
const items = Array.isArray(data.items) ? data.items.slice() : [];
|
||||
const me = (AuthManager.getUser() || {}).username;
|
||||
if (me && vaultName === `home-${me}` && !dirPath) {
|
||||
const myShares = await _loadReceivedShares();
|
||||
if (myShares.length) items.unshift({ type: "virtual-share", shares: myShares });
|
||||
}
|
||||
data.items = items;
|
||||
|
||||
data.items.forEach((item) => {
|
||||
// #196 : dossier virtuel « Partage » (fichiers reçus via partage dirigé).
|
||||
if (item.type === "virtual-share") {
|
||||
const shareDir = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": "Partage", "data-type": "directory" }, [icon("chevron-right", 14), _sharedIcon(), el("span", { class: "tree-item-text" }, [document.createTextNode(t("sidebar.shared_folder"))]), smallBadge(item.shares.length)]);
|
||||
const shareSub = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-Partage` });
|
||||
item.shares.forEach((s) => {
|
||||
const name = (s.path || "").split("/").pop().replace(/\.md$/i, "");
|
||||
const fItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": `Partage/${s.path.split("/").pop()}`, "data-type": "file", "data-share-token": s.token }, [_sharedIcon(), el("span", { class: "tree-item-text" }, [document.createTextNode(name)])]);
|
||||
fItem.title = t("dashboard.shared_by", { user: s.created_by || "" });
|
||||
fItem.addEventListener("click", () => {
|
||||
scrollTreeItemIntoView(fItem, false);
|
||||
window.open(`/s/${s.token}`, "_blank");
|
||||
});
|
||||
shareSub.appendChild(fItem);
|
||||
});
|
||||
shareDir.addEventListener("click", () => {
|
||||
TabManager.openNav(vaultName, "Partage");
|
||||
const expanded = !shareSub.classList.contains("collapsed");
|
||||
shareSub.classList.toggle("collapsed", expanded);
|
||||
const chev = shareDir.querySelector("[data-lucide]");
|
||||
if (chev) chev.setAttribute("data-lucide", expanded ? "chevron-right" : "chevron-down");
|
||||
safeCreateIcons();
|
||||
});
|
||||
fragment.appendChild(shareDir);
|
||||
fragment.appendChild(shareSub);
|
||||
return;
|
||||
}
|
||||
if (!shouldDisplayPath(item.path, vaultName)) return;
|
||||
|
||||
const existing = existingItems[item.path];
|
||||
@@ -374,11 +453,11 @@ async function incrementalLoadDirectory(vaultName, dirPath, container) {
|
||||
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, "data-type": "file" }, [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 }));
|
||||
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, "data-type": "file" }, [sharedFileIconSync(sharedPaths, item.name, vaultName, item.path), 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);
|
||||
if (window.innerWidth <= 768) {
|
||||
@@ -629,9 +708,47 @@ async function loadDirectory(vaultName, dirPath, container) {
|
||||
}
|
||||
container.innerHTML = "";
|
||||
|
||||
// #196 : icône « partagé » — résout le cache avant le rendu synchrone.
|
||||
const sharedPaths = await loadSharedPaths();
|
||||
|
||||
// #196 : dossier virtuel « Partage » à la racine du home du destinataire.
|
||||
const items = Array.isArray(data.items) ? data.items.slice() : [];
|
||||
const me = (AuthManager.getUser() || {}).username;
|
||||
if (me && vaultName === `home-${me}` && !dirPath) {
|
||||
const myShares = await _loadReceivedShares();
|
||||
if (myShares.length) items.unshift({ type: "virtual-share", shares: myShares });
|
||||
}
|
||||
data.items = items;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
data.items.forEach((item) => {
|
||||
// #196 : dossier virtuel « Partage » (fichiers reçus via partage dirigé).
|
||||
if (item.type === "virtual-share") {
|
||||
const shareDir = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": "Partage", "data-type": "directory" }, [icon("chevron-right", 14), _sharedIcon(), el("span", { class: "tree-item-text" }, [document.createTextNode(t("sidebar.shared_folder"))]), smallBadge(item.shares.length)]);
|
||||
const shareSub = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-Partage` });
|
||||
item.shares.forEach((s) => {
|
||||
const name = (s.path || "").split("/").pop().replace(/\.md$/i, "");
|
||||
const fItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": `Partage/${s.path.split("/").pop()}`, "data-type": "file", "data-share-token": s.token }, [_sharedIcon(), el("span", { class: "tree-item-text" }, [document.createTextNode(name)])]);
|
||||
fItem.title = t("dashboard.shared_by", { user: s.created_by || "" });
|
||||
fItem.addEventListener("click", () => {
|
||||
scrollTreeItemIntoView(fItem, false);
|
||||
window.open(`/s/${s.token}`, "_blank");
|
||||
});
|
||||
shareSub.appendChild(fItem);
|
||||
});
|
||||
shareDir.addEventListener("click", () => {
|
||||
TabManager.openNav(vaultName, "Partage");
|
||||
const expanded = !shareSub.classList.contains("collapsed");
|
||||
shareSub.classList.toggle("collapsed", expanded);
|
||||
const chev = shareDir.querySelector("[data-lucide]");
|
||||
if (chev) chev.setAttribute("data-lucide", expanded ? "chevron-right" : "chevron-down");
|
||||
safeCreateIcons();
|
||||
});
|
||||
fragment.appendChild(shareDir);
|
||||
fragment.appendChild(shareSub);
|
||||
return;
|
||||
}
|
||||
// Apply client-side filtering for hidden files
|
||||
if (!shouldDisplayPath(item.path, vaultName)) {
|
||||
return; // Skip this item
|
||||
@@ -673,7 +790,8 @@ async function loadDirectory(vaultName, dirPath, container) {
|
||||
} 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, "data-type": "file" }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
|
||||
const isShared = sharedPaths.has(`${vaultName}::${item.path}`);
|
||||
const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path, "data-type": "file" }, [isShared ? _sharedIcon() : 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", () => {
|
||||
@@ -705,6 +823,7 @@ async function loadDirectory(vaultName, dirPath, container) {
|
||||
safeCreateIcons();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sidebar filter
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1133,4 +1252,4 @@ function searchByTag(tag) {
|
||||
}
|
||||
|
||||
|
||||
export { initVaultContext, setSelectedVaultContext, syncVaultSelectors, shouldDisplayPath, loadVaults, loadVaultSettings, initSidebarFilter, TagFilterService, loadTags, scrollTreeItemIntoView, refreshSidebarForContext, focusVaultInSidebar, focusPathInSidebar, refreshTagsForContext, syncActiveFileTreeItem, searchByTag, addTagFilter, buildSearchResultsHeader, removeTagFilter };
|
||||
export { initVaultContext, setSelectedVaultContext, syncVaultSelectors, shouldDisplayPath, loadVaults, loadVaultSettings, initSidebarFilter, TagFilterService, loadTags, scrollTreeItemIntoView, refreshSidebarForContext, focusVaultInSidebar, focusPathInSidebar, refreshTagsForContext, syncActiveFileTreeItem, searchByTag, addTagFilter, buildSearchResultsHeader, removeTagFilter, invalidateSharedPaths };
|
||||
|
||||
Reference in New Issue
Block a user