From 92351e1ab53cd2142945f468c4eb704a66042e1f Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 May 2026 16:44:02 +0200 Subject: [PATCH] extract learning hub webdav controller --- public/js/learningHub.js | 108 ++---------------- public/js/learningHub/webdavController.js | 116 ++++++++++++++++++++ test/learning-hub-editor.test.js | 15 +++ test/learning-hub-webdav-controller.test.js | 72 ++++++++++++ 4 files changed, 213 insertions(+), 98 deletions(-) create mode 100644 public/js/learningHub/webdavController.js create mode 100644 test/learning-hub-webdav-controller.test.js diff --git a/public/js/learningHub.js b/public/js/learningHub.js index 0466070..b6b27b7 100644 --- a/public/js/learningHub.js +++ b/public/js/learningHub.js @@ -19,6 +19,7 @@ import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearch import { createQuizController } from './learningHub/quizController.js'; import { createSlideController } from './learningHub/slideController.js'; import { buildViewerMeta, renderPresentationCard, renderProgressList } from './learningHub/viewerRenderer.js'; +import { createWebdavController } from './learningHub/webdavController.js'; var loaded = false; var cmsLoaded = false; @@ -42,6 +43,7 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l hideLoading: function() { hideLoading(); }, showToast: function(message, type) { showToast(message, type); } }); + var webdav = createWebdavController({}); // ── Load when tab activated ──────────────────────────────── document.addEventListener('tabChanged', function(e) { @@ -121,8 +123,8 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l if (e.target.closest('#btn-lh-ai-refine-body')) { toggleRefineBar(); return; } if (e.target.closest('#btn-lh-refine-submit')) { submitRefineBody(); return; } if (e.target.closest('#btn-lh-refine-cancel')) { toggleRefineBar(false); return; } - if (e.target.closest('#btn-lh-webdav-refresh')) { browseWebdav(_aiWebdavCurrentPath); return; } - if (e.target.closest('#btn-lh-webdav-deselect')) { deselectWebdavFile(); return; } + if (e.target.closest('#btn-lh-webdav-refresh')) { webdav.browse(webdav.getCurrentPath()); return; } + if (e.target.closest('#btn-lh-webdav-deselect')) { webdav.deselectFile(); return; } // AI tabs var aiTab = e.target.closest('.lh-ai-tab'); @@ -131,11 +133,7 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l // WebDAV list items var wdItem = e.target.closest('.lh-webdav-item'); if (wdItem) { - if (wdItem.dataset.isdir === '1') { - browseWebdav(wdItem.dataset.path); - } else { - selectWebdavFile(wdItem.dataset.path, wdItem.dataset.name); - } + webdav.openItem(wdItem); return; } @@ -440,10 +438,6 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l // AI GENERATE PANEL // ============================================================ - var _aiWebdavCurrentPath = '/'; - var _aiWebdavSelectedPath = ''; - var _aiWebdavSelectedName = ''; - function openAiPanel() { var panel = document.getElementById('lh-ai-panel'); if (!panel) return; @@ -461,7 +455,7 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l .then(function(d) { var tab = document.getElementById('lh-ai-tab-webdav'); if (tab) tab.style.display = (d.user && d.user.nextcloud_url) ? '' : 'none'; - if (d.user && d.user.webdav_learning_path) _aiWebdavCurrentPath = d.user.webdav_learning_path || '/'; + if (d.user && d.user.webdav_learning_path) webdav.setCurrentPath(d.user.webdav_learning_path || '/'); }); // Wire ctype change once (use a flag so it doesn't stack on re-opens) @@ -538,93 +532,10 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l var tp = document.getElementById('lh-ai-tp-' + tabName); if (tp) tp.classList.remove('hidden'); if (tabName === 'webdav' && !document.querySelector('#lh-ai-webdav-list .lh-webdav-item')) { - browseWebdav(_aiWebdavCurrentPath); + webdav.browse(webdav.getCurrentPath()); } } - // ── WebDAV browser ────────────────────────────────────────── - function browseWebdav(path) { - _aiWebdavCurrentPath = path || '/'; - var list = document.getElementById('lh-ai-webdav-list'); - var pathLabel = document.getElementById('lh-ai-webdav-path-label'); - if (list) list.innerHTML = '
Loading...
'; - if (pathLabel) pathLabel.textContent = _aiWebdavCurrentPath; - - getJson('/api/admin/learning/webdav-browse?path=' + encodeURIComponent(_aiWebdavCurrentPath)) - .then(function(data) { - if (!data.success) { if (list) list.innerHTML = '
' + esc(data.error) + '
'; return; } - if (pathLabel) pathLabel.textContent = data.path; - renderWebdavList(data); - }) - .catch(function(err) { if (list) list.innerHTML = '
' + esc(err.message) + '
'; }); - } - - function renderWebdavList(data) { - var list = document.getElementById('lh-ai-webdav-list'); - if (!list) return; - - var html = ''; - - // Parent nav - if (data.path !== '/' && data.path !== data.parentPath) { - html += '
' + - ' ..
'; - } - - if (data.items.length === 0 && html === '') { - html = '
Empty folder
'; - } - - data.items.forEach(function(item) { - var icon = item.isDir ? 'fa-folder' : getFileIcon(item.contentType, item.name); - var iconColor = item.isDir ? 'var(--amber)' : 'var(--blue)'; - var sizeStr = (!item.isDir && item.size) ? ' (' + formatBytes(item.size) + ')' : ''; - html += '
' + - '' + - '' + esc(item.name) + '' + sizeStr + - '
'; - }); - list.innerHTML = html; - } - - function getFileIcon(mime, name) { - if (!name) name = ''; - var ext = name.split('.').pop().toLowerCase(); - if (mime && mime.includes('pdf') || ext === 'pdf') return 'fa-file-pdf'; - if (['doc','docx'].includes(ext)) return 'fa-file-word'; - if (['txt','md'].includes(ext)) return 'fa-file-lines'; - if (['html','htm'].includes(ext)) return 'fa-file-code'; - return 'fa-file'; - } - - function formatBytes(bytes) { - if (bytes < 1024) return bytes + ' B'; - if (bytes < 1024*1024) return (bytes/1024).toFixed(0) + ' KB'; - return (bytes/1024/1024).toFixed(1) + ' MB'; - } - - function selectWebdavFile(path, name) { - _aiWebdavSelectedPath = path; - _aiWebdavSelectedName = name; - var browser = document.getElementById('lh-ai-webdav-browser'); - var selDiv = document.getElementById('lh-ai-webdav-selected'); - var selName = document.getElementById('lh-ai-webdav-selected-name'); - // Hide browser, show selected indicator (use style.display — class="hidden" is overridden by inline styles) - if (browser) browser.style.display = 'none'; - if (selDiv) selDiv.style.display = 'flex'; - if (selName) selName.textContent = name; - } - - function deselectWebdavFile() { - _aiWebdavSelectedPath = ''; - _aiWebdavSelectedName = ''; - var browser = document.getElementById('lh-ai-webdav-browser'); - var selDiv = document.getElementById('lh-ai-webdav-selected'); - if (selDiv) selDiv.style.display = 'none'; - if (browser) browser.style.display = ''; - } - // ── Generate ──────────────────────────────────────────────── function runAiGenerate() { var activeTab = document.querySelector('.lh-ai-tab.active'); @@ -659,8 +570,9 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l var uploadCtx = document.getElementById('lh-ai-upload-context'); if (uploadCtx && uploadCtx.value.trim()) formData.append('topic', uploadCtx.value.trim()); } else if (tabName === 'webdav') { - if (!_aiWebdavSelectedPath) { showToast('Select a file from Nextcloud', 'error'); return; } - formData.append('webdavPath', _aiWebdavSelectedPath); + var webdavPath = webdav.getSelectedPath(); + if (!webdavPath) { showToast('Select a file from Nextcloud', 'error'); return; } + formData.append('webdavPath', webdavPath); var webdavCtx = document.getElementById('lh-ai-webdav-context'); if (webdavCtx && webdavCtx.value.trim()) formData.append('topic', webdavCtx.value.trim()); } diff --git a/public/js/learningHub/webdavController.js b/public/js/learningHub/webdavController.js new file mode 100644 index 0000000..61dc9ff --- /dev/null +++ b/public/js/learningHub/webdavController.js @@ -0,0 +1,116 @@ +import { getJson } from './api.js'; +import { esc } from './sanitize.js'; + +export function createWebdavController(deps) { + var currentPath = '/'; + var selectedPath = ''; + var selectedName = ''; + + function browse(path) { + currentPath = path || '/'; + var list = document.getElementById('lh-ai-webdav-list'); + var pathLabel = document.getElementById('lh-ai-webdav-path-label'); + if (list) list.innerHTML = '
Loading...
'; + if (pathLabel) pathLabel.textContent = currentPath; + + getJson('/api/admin/learning/webdav-browse?path=' + encodeURIComponent(currentPath)) + .then(function(data) { + if (!data.success) { + if (list) list.innerHTML = '
' + esc(data.error) + '
'; + return; + } + if (pathLabel) pathLabel.textContent = data.path; + renderList(data); + }) + .catch(function(err) { + if (list) list.innerHTML = '
' + esc(err.message) + '
'; + }); + } + + function renderList(data) { + var list = document.getElementById('lh-ai-webdav-list'); + if (!list) return; + list.innerHTML = renderWebdavListHtml(data); + } + + function openItem(el) { + if (!el) return; + if (el.dataset.isdir === '1') browse(el.dataset.path); + else selectFile(el.dataset.path, el.dataset.name); + } + + function selectFile(path, name) { + selectedPath = path; + selectedName = name; + var browser = document.getElementById('lh-ai-webdav-browser'); + var selDiv = document.getElementById('lh-ai-webdav-selected'); + var selName = document.getElementById('lh-ai-webdav-selected-name'); + if (browser) browser.style.display = 'none'; + if (selDiv) selDiv.style.display = 'flex'; + if (selName) selName.textContent = name; + } + + function deselectFile() { + selectedPath = ''; + selectedName = ''; + var browser = document.getElementById('lh-ai-webdav-browser'); + var selDiv = document.getElementById('lh-ai-webdav-selected'); + if (selDiv) selDiv.style.display = 'none'; + if (browser) browser.style.display = ''; + } + + return { + browse: browse, + deselectFile: deselectFile, + getCurrentPath: function() { return currentPath; }, + getSelectedName: function() { return selectedName; }, + getSelectedPath: function() { return selectedPath; }, + openItem: openItem, + selectFile: selectFile, + setCurrentPath: function(path) { currentPath = path || '/'; } + }; +} + +export function renderWebdavListHtml(data) { + var html = ''; + + if (data.path !== '/' && data.path !== data.parentPath) { + html += '
' + + ' ..
'; + } + + if (data.items.length === 0 && html === '') { + return '
Empty folder
'; + } + + data.items.forEach(function(item) { + var icon = item.isDir ? 'fa-folder' : getFileIcon(item.contentType, item.name); + var iconColor = item.isDir ? 'var(--amber)' : 'var(--blue)'; + var sizeStr = (!item.isDir && item.size) + ? ' (' + formatBytes(item.size) + ')' + : ''; + html += '
' + + '' + + '' + esc(item.name) + '' + sizeStr + + '
'; + }); + + return html; +} + +export function getFileIcon(mime, name) { + if (!name) name = ''; + var ext = name.split('.').pop().toLowerCase(); + if (mime && mime.includes('pdf') || ext === 'pdf') return 'fa-file-pdf'; + if (['doc', 'docx'].includes(ext)) return 'fa-file-word'; + if (['txt', 'md'].includes(ext)) return 'fa-file-lines'; + if (['html', 'htm'].includes(ext)) return 'fa-file-code'; + return 'fa-file'; +} + +export function formatBytes(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + ' KB'; + return (bytes / 1024 / 1024).toFixed(1) + ' MB'; +} diff --git a/test/learning-hub-editor.test.js b/test/learning-hub-editor.test.js index bf344aa..5ac1152 100644 --- a/test/learning-hub-editor.test.js +++ b/test/learning-hub-editor.test.js @@ -18,6 +18,7 @@ const quizRendererModule = read('public/js/learningHub/quizRenderer.js'); const sanitizeModule = read('public/js/learningHub/sanitize.js'); const slideControllerModule = read('public/js/learningHub/slideController.js'); const viewerRendererModule = read('public/js/learningHub/viewerRenderer.js'); +const webdavControllerModule = read('public/js/learningHub/webdavController.js'); test('Learning Hub destroys embedded Tiptap editors before removing CMS rows', () => { assert.match(source, /from '\.\/learningHub\/tiptapEditor\.js'/); @@ -149,6 +150,20 @@ test('Learning Hub quiz controller owns submission and answer tracking', () => { assert.doesNotMatch(source, /function submitQuiz\(\)|function showQuizResults\(|function renderQuiz\(questions\)/); }); +test('Learning Hub WebDAV controller owns browser state and rendering', () => { + assert.match(source, /from '\.\/learningHub\/webdavController\.js'/); + assert.match(source, /var webdav = createWebdavController\(/); + assert.match(source, /webdav\.browse\(webdav\.getCurrentPath\(\)\)/); + assert.match(source, /webdav\.openItem\(wdItem\)/); + assert.match(source, /var webdavPath = webdav\.getSelectedPath\(\)/); + assert.match(webdavControllerModule, /export function createWebdavController\(deps\)/); + assert.match(webdavControllerModule, /export function renderWebdavListHtml\(data\)/); + assert.match(webdavControllerModule, /export function getFileIcon\(mime, name\)/); + assert.match(webdavControllerModule, /export function formatBytes\(bytes\)/); + assert.doesNotMatch(source, /var _aiWebdav|function browseWebdav\(|function renderWebdavList\(|function selectWebdavFile\(/); + assert.doesNotMatch(webdavControllerModule, /window\.Tiptap|ai-generate|FormData/); +}); + test('Learning Hub viewer renderers are isolated from API and slide loading', () => { assert.match(source, /from '\.\/learningHub\/viewerRenderer\.js'/); assert.match(viewerRendererModule, /export function buildViewerMeta\(item\)/); diff --git a/test/learning-hub-webdav-controller.test.js b/test/learning-hub-webdav-controller.test.js new file mode 100644 index 0000000..d09b9b2 --- /dev/null +++ b/test/learning-hub-webdav-controller.test.js @@ -0,0 +1,72 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const { pathToFileURL } = require('node:url'); + +async function loadWebdavController() { + return import(pathToFileURL(path.join(__dirname, '..', 'public/js/learningHub/webdavController.js')).href); +} + +test('Learning Hub WebDAV helpers preserve icons and byte labels', async () => { + const { formatBytes, getFileIcon } = await loadWebdavController(); + + assert.equal(getFileIcon('application/pdf', 'scan.bin'), 'fa-file-pdf'); + assert.equal(getFileIcon('', 'note.md'), 'fa-file-lines'); + assert.equal(getFileIcon('', 'page.html'), 'fa-file-code'); + assert.equal(getFileIcon('', 'letter.docx'), 'fa-file-word'); + assert.equal(getFileIcon('', 'archive.zip'), 'fa-file'); + assert.equal(formatBytes(512), '512 B'); + assert.equal(formatBytes(2048), '2 KB'); + assert.equal(formatBytes(1572864), '1.5 MB'); +}); + +test('Learning Hub WebDAV list rendering escapes paths and names', async () => { + const { renderWebdavListHtml } = await loadWebdavController(); + const html = renderWebdavListHtml({ + path: '/Learning/Sub', + parentPath: '/Learning?', + items: [ + { isDir: true, path: '/Learning/