Files
ObsiGate/frontend/js/vaulthome.js
T
bruno 8c4ce5e3c0
CI / lint (push) Successful in 13s
CI / security (push) Successful in 9s
CI / test (push) Failing after 26s
CI / build (push) Has been skipped
fix: vaulthome.js import safeCreateIcons from utils.js instead of viewer.js
2026-06-04 23:35:16 -04:00

365 lines
12 KiB
JavaScript

/**
* Vault Home Page module for ObsiGate.
*
* When a specific vault is selected (not "All"), replaces the dashboard
* with a recursive file listing sorted by modification time (newest first).
* Files from the selected directory AND all its subdirectories are shown
* with their relative path displayed in the metadata line.
* The listing updates automatically when browsing into subdirectories.
*
* The visual style mirrors the search results (.search-result-item) for
* consistency.
*
* Usage:
* import { showVaultHome, updateVaultHomeDir, isVaultHomeActive } from './vaulthome.js';
*/
import { api } from './auth.js';
import { el } from './viewer.js';
import { getFileIcon, safeCreateIcons } from './utils.js';
import { TabManager } from './ui.js';
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const state = {
active: false,
currentVault: null,
currentDir: '', // empty = vault root
};
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/** Check if vault home is currently displayed. */
export function isVaultHomeActive() {
return state.active;
}
/**
* Display the vault home page for a vault directory.
* @param {string} vaultName - Vault name
* @param {string} dirPath - Directory path (empty for root)
*/
export async function showVaultHome(vaultName, dirPath = '') {
if (vaultName === 'all' || !vaultName) {
state.active = false;
return;
}
state.active = true;
state.currentVault = vaultName;
state.currentDir = dirPath || '';
const area = document.getElementById('content-area');
if (!area) return;
// Remove any existing dashboard
const home = document.getElementById('dashboard-home');
if (home) home.remove();
// Build vault home container
area.innerHTML = '';
const container = el('div', { class: 'vault-home', id: 'vault-home' });
area.appendChild(container);
// Breadcrumb
renderBreadcrumb(container, vaultName, state.currentDir);
// Results container
const resultsArea = el('div', { class: 'vault-home-results', id: 'vault-home-results' });
container.appendChild(resultsArea);
// Show loading
resultsArea.innerHTML = `
<div class="tree-loading" style="padding:2rem;text-align:center">
<div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:0 auto"></div>
<p style="margin-top:0.75rem;color:var(--text-muted);font-size:0.85rem">Chargement des fichiers...</p>
</div>`;
await loadAndRender(vaultName, state.currentDir, resultsArea);
// Also load recent files for this vault (only at root level)
if (!state.currentDir) {
loadRecentSection(vaultName, container);
}
}
/**
* Update the vault home directory without rebuilding the whole page.
* Called when navigating into a subdirectory.
* @param {string} vaultName
* @param {string} dirPath
*/
export async function updateVaultHomeDir(vaultName, dirPath) {
if (!state.active || state.currentVault !== vaultName) return;
state.currentDir = dirPath || '';
const container = document.getElementById('vault-home');
if (!container) return;
// Update breadcrumb
const bc = container.querySelector('.vault-home-breadcrumb');
if (bc) {
bc.innerHTML = '';
buildBreadcrumbHTML(bc, vaultName, state.currentDir);
safeCreateIcons();
}
// Reload results
const resultsArea = document.getElementById('vault-home-results');
if (resultsArea) {
resultsArea.innerHTML = `
<div class="tree-loading" style="padding:2rem;text-align:center">
<div class="loading-spinner" style="width:20px;height:20px;border-width:2px;margin:0 auto"></div>
</div>`;
await loadAndRender(vaultName, state.currentDir, resultsArea);
}
}
// ---------------------------------------------------------------------------
// Tree click delegation — update vault home on directory clicks
// ---------------------------------------------------------------------------
function _initTreeDelegation() {
const tree = document.getElementById('vault-tree');
if (!tree) return;
tree.addEventListener('click', (e) => {
if (!state.active) return;
// Find the closest tree-item that is a directory
const item = e.target.closest('.tree-item');
if (!item || !item.dataset.vault || item.dataset.path === undefined) return;
const vault = item.dataset.vault;
const path = item.dataset.path;
// Only react to directory clicks (items that have a child container)
const hasChildContainer = item.nextElementSibling
&& item.nextElementSibling.classList.contains('tree-children');
if (!hasChildContainer) return;
// Update vault home dir
updateVaultHomeDir(vault, path);
});
}
// Initialize delegation when module loads
_initTreeDelegation();
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
function renderBreadcrumb(container, vaultName, dirPath) {
const bc = el('div', { class: 'vault-home-breadcrumb' });
buildBreadcrumbHTML(bc, vaultName, dirPath);
container.appendChild(bc);
}
function buildBreadcrumbHTML(parentEl, vaultName, dirPath) {
// Vault icon + name (root)
const vaultLink = el('span', { class: 'vault-home-breadcrumb-item vault-home-breadcrumb-root' }, [
el('i', { 'data-lucide': 'database', style: 'width:14px;height:14px' }),
el('span', {}, [document.createTextNode(vaultName)]),
]);
vaultLink.addEventListener('click', () => updateVaultHomeDir(vaultName, ''));
vaultLink.style.cursor = 'pointer';
parentEl.appendChild(vaultLink);
if (!dirPath) return;
const segments = dirPath.split('/').filter(Boolean);
let cumulative = '';
for (const seg of segments) {
cumulative = cumulative ? cumulative + '/' + seg : seg;
const sep = el('span', { class: 'vault-home-breadcrumb-sep' }, [document.createTextNode(' / ')]);
parentEl.appendChild(sep);
const segLink = el('span', { class: 'vault-home-breadcrumb-item' }, [
el('i', { 'data-lucide': 'folder', style: 'width:14px;height:14px' }),
el('span', {}, [document.createTextNode(seg)]),
]);
segLink.addEventListener('click', () => updateVaultHomeDir(vaultName, cumulative));
segLink.style.cursor = 'pointer';
parentEl.appendChild(segLink);
}
}
async function loadAndRender(vaultName, dirPath, container) {
try {
const url = `/api/vault/${encodeURIComponent(vaultName)}/files?dir=${encodeURIComponent(dirPath)}&limit=200&recursive=true`;
const data = await api(url);
if (!data.files || data.files.length === 0) {
container.innerHTML = `
<div class="vault-home-empty">
<i data-lucide="folder-open" style="width:32px;height:32px;color:var(--text-muted)"></i>
<p>Aucun fichier dans ce répertoire</p>
</div>`;
safeCreateIcons();
return;
}
// Header
const header = el('div', { class: 'vault-home-header' });
const count = el('span', { class: 'vault-home-count' }, [
document.createTextNode(`${data.files.length} fichier${data.files.length > 1 ? 's' : ''}`),
]);
header.appendChild(count);
// Recursive indicator
if (data.recursive) {
const recursiveBadge = el('span', { class: 'vault-home-recursive-badge' }, [
document.createTextNode('récursif'),
]);
header.appendChild(recursiveBadge);
}
// Directory label
const dirLabel = dirPath ? `/${dirPath}` : ' (racine)';
const dirInfo = el('span', { class: 'vault-home-dir-info' }, [
document.createTextNode(dirLabel),
]);
header.appendChild(dirInfo);
container.innerHTML = '';
container.appendChild(header);
// File list — using search-result-item style
const list = el('div', { class: 'search-results' });
data.files.forEach((f) => {
const fileIconName = getFileIcon ? getFileIcon(f.name) : 'file';
const displayName = f.name.match(/\.md$/i) ? f.name.replace(/\.md$/i, '') : f.name;
const ext = f.extension || '';
const titleDiv = el('div', { class: 'search-result-title' }, [
document.createTextNode(displayName),
]);
// Format date
const modDate = new Date(f.modified * 1000);
const dateStr = modDate.toLocaleDateString('fr-CA', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit',
});
// Build metadata line: rel_dir (if any) + date + size
const metaParts = [];
if (f.rel_dir) {
metaParts.push(f.rel_dir + '/');
}
metaParts.push(dateStr);
metaParts.push(formatSize(f.size));
const metaDiv = el('div', { class: 'search-result-vault' }, [
document.createTextNode(metaParts.join(' · ')),
]);
const item = el('div', {
class: 'search-result-item vault-home-item',
'data-vault': f.vault,
'data-path': f.path,
}, [
el('span', { class: 'search-result-ext' }, [document.createTextNode(ext)]),
el('i', { 'data-lucide': fileIconName, style: 'width:14px;height:14px;margin-right:4px;opacity:0.6' }),
titleDiv,
metaDiv,
]);
item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path));
item.addEventListener('dblclick', (e) => {
e.preventDefault();
TabManager.openPersistent(f.vault, f.path);
});
list.appendChild(item);
});
container.appendChild(list);
safeCreateIcons();
} catch (err) {
container.innerHTML = `
<div class="vault-home-error">
<p>Erreur de chargement: ${escapeHtml(err?.detail || err?.message || 'Inconnue')}</p>
</div>`;
}
}
// ---------------------------------------------------------------------------
// Recent files section (loaded at vault root only)
// ---------------------------------------------------------------------------
async function loadRecentSection(vaultName, container) {
try {
const data = await api(`/api/recent?mode=opened&vault=${encodeURIComponent(vaultName)}&limit=5`);
const files = data.files || [];
if (files.length === 0) return;
// Insert recent section BEFORE the results area
const resultsArea = document.getElementById('vault-home-results');
const section = el('div', { class: 'vault-home-recent' });
const header = el('div', { class: 'vault-home-recent-header' });
header.innerHTML = `
<i data-lucide="clock" style="width:14px;height:14px"></i>
<span>Récents</span>`;
section.appendChild(header);
const list = el('div', { class: 'vault-home-recent-list' });
files.forEach((f) => {
const title = f.title || f.path.split('/').pop() || '';
const displayTitle = title.match(/\.md$/i) ? title.replace(/\.md$/i, '') : title;
const dir = f.path.includes('/') ? f.path.split('/').slice(0, -1).join('/') + '/' : '';
const item = el('div', { class: 'vault-home-recent-item' });
item.innerHTML = `
<i data-lucide="${getFileIcon ? getFileIcon(f.path) : 'file'}" style="width:14px;height:14px;opacity:0.6;flex-shrink:0"></i>
<span class="vault-home-recent-title">${escapeHtml(displayTitle)}</span>
<span class="vault-home-recent-dir">${escapeHtml(dir)}</span>
<span class="vault-home-recent-time">${escapeHtml(f.mtime_human || '')}</span>`;
item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path));
item.addEventListener('dblclick', (e) => {
e.preventDefault();
TabManager.openPersistent(f.vault, f.path);
});
list.appendChild(item);
});
section.appendChild(list);
// Insert before the results area
if (resultsArea) {
container.insertBefore(section, resultsArea);
} else {
container.appendChild(section);
}
safeCreateIcons();
} catch {
// Silently skip if recent files can't be loaded
}
}
// ---------------------------------------------------------------------------
// Utility
// ---------------------------------------------------------------------------
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' o';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' Ko';
return (bytes / (1024 * 1024)).toFixed(1) + ' Mo';
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}