Full DMS: folders + ACLs, versioning, trash, bulk ops, preview, smart folders
Rebuild the dashboard as a Drive-style document management system on top of the existing CaveauAI hybrid RAG pipeline. Backend: - 5 migrations (versions, trash soft-delete, saved searches, categories, audit) - DMS helpers (folder ACL walker, disk storage, audit, version snapshot, XLSX/PPTX/HTML/CSV/MD extractors) - New APIs: folders, document-versions, trash, bulk, preview, saved-searches, categories, diagnostics - Extended APIs: documents (folder_id, soft-delete, ACL filter, sort), upload (9 file types, version-collision detection with replace/new/keep-both, disk persistence), chat-stream (folder scoping + graph related-documents) - 30-day trash purge cron with Qdrant + disk + graph cleanup Frontend: - Drive-style two-pane browser with folder tree, drag-drop, bulk-action bar, right-click context menu, multi-select - New pages: folders (tree + per-folder ACL editor), trash (restore/purge) - Extended pages: upload (folder picker, version-collision modal, 9 file type chips), document (Preview/Versions/Permissions tabs with PDF.js + mammoth.js + audio), index (DMS KPIs + activity feed), settings (live diagnostics ping MariaDB/Qdrant/LiteLLM/FalkorDB/disk), chat (folder scope chips + related-authorities chips) - New CSS (dms.css) + JS bundle (dms.js) exposing window.DBN_DMS - Sidebar nav adds Folders + Trash items All routes return HTTP 200 in local smoke test; all 32 files lint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||||
$dashboardPage = 'folders';
|
||||
$dashboardTitle = dbnToolsT('dash_title_folders', dbnToolsCurrentLanguage()) ?: 'Folders';
|
||||
$dashboardLead = dbnToolsT('dash_lead_folders', dbnToolsCurrentLanguage()) ?: 'Organise documents and set per-folder access.';
|
||||
require_once __DIR__ . '/../includes/layout_dashboard.php';
|
||||
?>
|
||||
<section class="dms-shell">
|
||||
<aside class="dms-tree" id="dmsTree" aria-label="Folder tree">
|
||||
<div class="dms-loading"></div>
|
||||
</aside>
|
||||
<div class="dms-main">
|
||||
<div class="dms-toolbar">
|
||||
<div class="dms-toolbar__crumbs" id="folderCrumbs"><span>Pick a folder on the left</span></div>
|
||||
<div class="dms-toolbar__actions">
|
||||
<button class="dash-btn dash-btn--primary" id="folderNewBtn" type="button">+ New folder</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="folderDetail">
|
||||
<div class="dms-list__empty">
|
||||
<strong>Select a folder</strong>
|
||||
<span>Use the tree on the left, or create a new folder.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
const DMS = window.DBN_DMS;
|
||||
const $tree = document.getElementById('dmsTree');
|
||||
const $detail = document.getElementById('folderDetail');
|
||||
const $crumbs = document.getElementById('folderCrumbs');
|
||||
let selectedFolderId = null;
|
||||
|
||||
async function loadTree() {
|
||||
await DMS.Tree.load();
|
||||
DMS.Tree.render($tree, { activeFolderId: selectedFolderId });
|
||||
}
|
||||
|
||||
async function showDetail(folderId) {
|
||||
if (folderId === 'all' || folderId === 'unassigned' || folderId === 'trash') {
|
||||
$detail.innerHTML = '<div class="dms-list__empty"><strong>System folder</strong><span>Cannot configure access for system folders.</span></div>';
|
||||
return;
|
||||
}
|
||||
selectedFolderId = folderId;
|
||||
$detail.innerHTML = '<div class="dms-loading"></div>';
|
||||
try {
|
||||
const [perms, crumbsData] = await Promise.all([
|
||||
DMS.api('/folders.php?action=list_permissions&folder_id=' + folderId),
|
||||
DMS.api('/folders.php?action=get_breadcrumb&folder_id=' + folderId),
|
||||
]);
|
||||
$crumbs.innerHTML = (crumbsData.breadcrumb || []).map((b, i, a) =>
|
||||
(i ? '<span class="dms-toolbar__crumb-sep">›</span>' : '')
|
||||
+ (i === a.length - 1
|
||||
? '<span class="dms-toolbar__crumb--current">' + DMS.safe(b.name) + '</span>'
|
||||
: '<span>' + DMS.safe(b.name) + '</span>')
|
||||
).join('');
|
||||
|
||||
const rows = (perms.permissions || []).map(p => {
|
||||
const who = p.user_id
|
||||
? '👤 ' + DMS.safe(p.user_email || ('user #' + p.user_id))
|
||||
: '🛡 role ≥ ' + DMS.safe(p.min_role);
|
||||
return '<div class="dms-perm-row">' +
|
||||
'<div>' + who + '</div>' +
|
||||
'<div>' + (p.can_read ? '✓' : '—') + ' read</div>' +
|
||||
'<div>' + (p.can_write ? '✓' : '—') + ' write</div>' +
|
||||
'<div>' + (p.can_manage ? '✓' : '—') + ' manage</div>' +
|
||||
'<div><button class="dms-list__more" data-pid="' + p.id + '" type="button" aria-label="Remove">✕</button></div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
$detail.innerHTML =
|
||||
'<div class="dms-toolbar"><div class="dms-toolbar__crumbs"><strong>Access control</strong></div>' +
|
||||
'<div class="dms-toolbar__actions">' +
|
||||
'<button class="dash-btn" type="button" id="folderRename">Rename</button>' +
|
||||
'<button class="dash-btn dash-btn--danger" type="button" id="folderDelete">Delete</button>' +
|
||||
'<button class="dash-btn dash-btn--primary" type="button" id="folderAddAcl">+ Add rule</button>' +
|
||||
'</div></div>' +
|
||||
'<div class="dms-list">' +
|
||||
(rows ||
|
||||
'<div class="dms-list__empty"><strong>Open to all in tenant</strong>' +
|
||||
'<span>No rules set — add one to restrict access.</span></div>') +
|
||||
(rows ? '' : '') +
|
||||
'</div>';
|
||||
|
||||
$detail.querySelector('#folderRename').addEventListener('click', () => {
|
||||
DMS.folderModal.open({ folderId, name: crumbsData.breadcrumb.slice(-1)[0]?.name || '' });
|
||||
});
|
||||
$detail.querySelector('#folderDelete').addEventListener('click', async () => {
|
||||
if (!confirm('Soft-delete this folder? Documents inside will move to Trash.')) return;
|
||||
try {
|
||||
await DMS.api('/folders.php?action=delete', { method: 'POST', body: { folder_id: folderId } });
|
||||
selectedFolderId = null;
|
||||
$detail.innerHTML = '<div class="dms-list__empty"><strong>Deleted</strong></div>';
|
||||
await loadTree();
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
$detail.querySelector('#folderAddAcl').addEventListener('click', () => openAclModal(folderId));
|
||||
$detail.querySelectorAll('[data-pid]').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!confirm('Remove this access rule?')) return;
|
||||
try {
|
||||
await DMS.api('/folders.php?action=remove_permission', { method: 'POST', body: { permission_id: Number(btn.dataset.pid) } });
|
||||
showDetail(folderId);
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
$detail.innerHTML = '<div class="dms-list__empty"><strong>Error</strong><span>' + DMS.safe(e.message) + '</span></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function openAclModal(folderId) {
|
||||
DMS.openModal({
|
||||
title: 'Add access rule',
|
||||
body:
|
||||
'<div class="dms-field"><label>Grantee type</label>' +
|
||||
'<select id="aclType"><option value="role">Role-based</option><option value="user">Specific user (by id)</option></select></div>' +
|
||||
'<div class="dms-field" id="aclRoleField"><label>Minimum role</label>' +
|
||||
'<select id="aclRole"><option>viewer</option><option>editor</option><option>admin</option><option>owner</option></select></div>' +
|
||||
'<div class="dms-field" id="aclUserField" style="display:none"><label>User ID</label><input type="number" id="aclUser"></div>' +
|
||||
'<div class="dms-field dms-field--inline"><label>Permissions</label>' +
|
||||
'<label><input type="checkbox" id="aclRead" checked> read</label>' +
|
||||
'<label><input type="checkbox" id="aclWrite"> write</label>' +
|
||||
'<label><input type="checkbox" id="aclManage"> manage</label></div>',
|
||||
actions: [
|
||||
{ label: 'Cancel', cls: 'dash-btn', onclick: DMS.closeModal },
|
||||
{ label: 'Add', cls: 'dash-btn dash-btn--primary', onclick: async () => {
|
||||
const type = document.getElementById('aclType').value;
|
||||
const body = { folder_id: folderId,
|
||||
can_read: document.getElementById('aclRead').checked,
|
||||
can_write: document.getElementById('aclWrite').checked,
|
||||
can_manage: document.getElementById('aclManage').checked,
|
||||
};
|
||||
if (type === 'role') body.min_role = document.getElementById('aclRole').value;
|
||||
else body.user_id = Number(document.getElementById('aclUser').value || 0);
|
||||
try {
|
||||
await DMS.api('/folders.php?action=set_permission', { method: 'POST', body });
|
||||
DMS.closeModal();
|
||||
showDetail(folderId);
|
||||
} catch (e) { alert(e.message); }
|
||||
}},
|
||||
],
|
||||
});
|
||||
document.getElementById('aclType').addEventListener('change', e => {
|
||||
document.getElementById('aclRoleField').style.display = e.target.value === 'role' ? '' : 'none';
|
||||
document.getElementById('aclUserField').style.display = e.target.value === 'user' ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('dms:folder-changed', e => showDetail(e.detail.folderId));
|
||||
document.addEventListener('dms:reload-required', loadTree);
|
||||
document.addEventListener('dms:reload-tree', loadTree);
|
||||
|
||||
document.getElementById('folderNewBtn').addEventListener('click', () => DMS.folderModal.open({}));
|
||||
|
||||
loadTree();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../includes/layout_dashboard_footer.php'; ?>
|
||||
Reference in New Issue
Block a user