extract learning hub cms controller
This commit is contained in:
parent
e298cc4746
commit
c0471603f7
4 changed files with 234 additions and 183 deletions
|
|
@ -11,12 +11,10 @@ import {
|
||||||
import { esc, sanitizeHtml } from './learningHub/sanitize.js';
|
import { esc, sanitizeHtml } from './learningHub/sanitize.js';
|
||||||
import { deleteJson, getJson, sendJson } from './learningHub/api.js';
|
import { deleteJson, getJson, sendJson } from './learningHub/api.js';
|
||||||
import {
|
import {
|
||||||
renderCmsCategoryList,
|
|
||||||
renderCmsContentEmpty,
|
|
||||||
renderCmsContentRow,
|
|
||||||
renderOptionRowShell,
|
renderOptionRowShell,
|
||||||
renderQuestionBlockShell
|
renderQuestionBlockShell
|
||||||
} from './learningHub/cmsRenderer.js';
|
} from './learningHub/cmsRenderer.js';
|
||||||
|
import { createCmsController } from './learningHub/cmsController.js';
|
||||||
import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearchHeader } from './learningHub/feedRenderer.js';
|
import { renderCategoryPills, renderEmptySearchMessage, renderFeed, renderSearchHeader } from './learningHub/feedRenderer.js';
|
||||||
import { renderQuizQuestions, renderQuizResultExplanations } from './learningHub/quizRenderer.js';
|
import { renderQuizQuestions, renderQuizResultExplanations } from './learningHub/quizRenderer.js';
|
||||||
import { createSlideController } from './learningHub/slideController.js';
|
import { createSlideController } from './learningHub/slideController.js';
|
||||||
|
|
@ -34,6 +32,10 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l
|
||||||
hideBusy: function() { hideBusy(); },
|
hideBusy: function() { hideBusy(); },
|
||||||
showToast: function(message, type) { showToast(message, type); }
|
showToast: function(message, type) { showToast(message, type); }
|
||||||
});
|
});
|
||||||
|
var cms = createCmsController({
|
||||||
|
refreshUserCategories: function() { loadCategories(); },
|
||||||
|
showToast: function(message, type) { showToast(message, type); }
|
||||||
|
});
|
||||||
|
|
||||||
// ── Load when tab activated ────────────────────────────────
|
// ── Load when tab activated ────────────────────────────────
|
||||||
document.addEventListener('tabChanged', function(e) {
|
document.addEventListener('tabChanged', function(e) {
|
||||||
|
|
@ -89,8 +91,8 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CMS events ─────────────────────────────
|
// ── CMS events ─────────────────────────────
|
||||||
if (e.target.closest('#btn-lh-refresh-content')) { loadCmsContent(); loadCmsStats(); return; }
|
if (e.target.closest('#btn-lh-refresh-content')) { cms.loadContent(); cms.loadStats(); return; }
|
||||||
if (e.target.closest('#btn-lh-add-cat')) { addCategory(); return; }
|
if (e.target.closest('#btn-lh-add-cat')) { cms.addCategory(); return; }
|
||||||
if (e.target.closest('#btn-lh-new-content')) { openEditor(null, 'article'); return; }
|
if (e.target.closest('#btn-lh-new-content')) { openEditor(null, 'article'); return; }
|
||||||
if (e.target.closest('#btn-lh-new-quiz')) { openEditor(null, 'quiz'); return; }
|
if (e.target.closest('#btn-lh-new-quiz')) { openEditor(null, 'quiz'); return; }
|
||||||
if (e.target.closest('#btn-lh-new-pearl')) { openEditor(null, 'pearl'); return; }
|
if (e.target.closest('#btn-lh-new-pearl')) { openEditor(null, 'pearl'); return; }
|
||||||
|
|
@ -141,11 +143,11 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l
|
||||||
|
|
||||||
// CMS delete category
|
// CMS delete category
|
||||||
var delCat = e.target.closest('.lh-cms-del-cat');
|
var delCat = e.target.closest('.lh-cms-del-cat');
|
||||||
if (delCat) { deleteCategory(delCat.dataset.id); return; }
|
if (delCat) { cms.deleteCategory(delCat.dataset.id); return; }
|
||||||
|
|
||||||
// CMS sidebar category filter
|
// CMS sidebar category filter
|
||||||
var cmsCat = e.target.closest('.lh-cms-cat-filter');
|
var cmsCat = e.target.closest('.lh-cms-cat-filter');
|
||||||
if (cmsCat && cmsCat.dataset.id) { filterCmsContentByCategory(cmsCat.dataset.id); return; }
|
if (cmsCat && cmsCat.dataset.id) { cms.filterByCategory(cmsCat.dataset.id); return; }
|
||||||
|
|
||||||
// Remove question block
|
// Remove question block
|
||||||
var rmQ = e.target.closest('.lh-rm-question');
|
var rmQ = e.target.closest('.lh-rm-question');
|
||||||
|
|
@ -164,6 +166,9 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l
|
||||||
if (rmOpt) { var row = rmOpt.closest('.lh-option-row'); if (row) { destroyOptionEditor(row); row.remove(); } return; }
|
if (rmOpt) { var row = rmOpt.closest('.lh-option-row'); if (row) { destroyOptionEditor(row); row.remove(); } return; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('input', function(e) { cms.handleFilterInput(e); });
|
||||||
|
document.addEventListener('change', function(e) { cms.handleFilterChange(e); });
|
||||||
|
|
||||||
// wireSearch() called after component is in DOM
|
// wireSearch() called after component is in DOM
|
||||||
var _searchTimeout = null;
|
var _searchTimeout = null;
|
||||||
function wireSearch() {
|
function wireSearch() {
|
||||||
|
|
@ -506,9 +511,7 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCms() {
|
function loadCms() {
|
||||||
loadCmsCategories();
|
cms.refreshAll();
|
||||||
loadCmsContent();
|
|
||||||
loadCmsStats();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -837,7 +840,7 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l
|
||||||
opt.textContent = categoryName;
|
opt.textContent = categoryName;
|
||||||
sel.appendChild(opt);
|
sel.appendChild(opt);
|
||||||
sel.value = String(data.id);
|
sel.value = String(data.id);
|
||||||
loadCmsCategories(String(data.id));
|
cms.loadCategories(String(data.id));
|
||||||
loadCategories();
|
loadCategories();
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
|
|
@ -884,161 +887,6 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l
|
||||||
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCmsStats() {
|
|
||||||
getJson('/api/admin/learning/stats')
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.success) return;
|
|
||||||
var s = data.stats;
|
|
||||||
var set = function(id, val) { var el = document.getElementById(id); if (el) el.textContent = val; };
|
|
||||||
set('cms-stat-published', s.publishedContent);
|
|
||||||
set('cms-stat-drafts', s.totalContent - s.publishedContent);
|
|
||||||
set('cms-stat-categories', s.totalCategories);
|
|
||||||
set('cms-stat-quizzes', s.totalQuizzes);
|
|
||||||
set('cms-stat-attempts', s.totalAttempts);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadCmsCategories(editorCategoryId, filterCategoryId) {
|
|
||||||
getJson('/api/admin/learning/categories')
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.success) return;
|
|
||||||
var container = document.getElementById('lh-cms-categories');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
container.innerHTML = renderCmsCategoryList(data.categories);
|
|
||||||
|
|
||||||
// Update category dropdowns (editor + filter)
|
|
||||||
var sel = document.getElementById('lh-cms-edit-category');
|
|
||||||
if (sel) {
|
|
||||||
var selectedEditorId = editorCategoryId !== undefined ? editorCategoryId : sel.value;
|
|
||||||
sel.innerHTML = '<option value="">Uncategorized</option>';
|
|
||||||
data.categories.forEach(function(c) {
|
|
||||||
sel.innerHTML += '<option value="' + c.id + '">' + esc(c.name) + '</option>';
|
|
||||||
});
|
|
||||||
if (selectedEditorId && hasSelectValue(sel, selectedEditorId)) sel.value = selectedEditorId;
|
|
||||||
}
|
|
||||||
var filterSel = document.getElementById('cms-filter-category');
|
|
||||||
if (filterSel) {
|
|
||||||
var selectedFilterId = filterCategoryId !== undefined ? filterCategoryId : filterSel.value;
|
|
||||||
filterSel.innerHTML = '<option value="all">All Categories</option>';
|
|
||||||
data.categories.forEach(function(c) {
|
|
||||||
filterSel.innerHTML += '<option value="' + c.id + '">' + esc(c.name) + '</option>';
|
|
||||||
});
|
|
||||||
if (selectedFilterId && hasSelectValue(filterSel, selectedFilterId)) filterSel.value = selectedFilterId;
|
|
||||||
}
|
|
||||||
setActiveCmsCat((document.getElementById('cms-filter-category') || {}).value || 'all');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasSelectValue(sel, value) {
|
|
||||||
value = String(value);
|
|
||||||
for (var i = 0; i < sel.options.length; i++) {
|
|
||||||
if (sel.options[i].value === value) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCategory() {
|
|
||||||
var input = document.getElementById('lh-cms-cat-name');
|
|
||||||
if (!input || !input.value.trim()) { showToast('Enter category name', 'error'); return; }
|
|
||||||
|
|
||||||
sendJson('/api/admin/learning/categories', 'POST', { name: input.value.trim() })
|
|
||||||
.then(function(data) {
|
|
||||||
if (data.success) {
|
|
||||||
input.value = '';
|
|
||||||
loadCmsCategories();
|
|
||||||
loadCategories(); // refresh user-facing pills too
|
|
||||||
showToast('Category created', 'success');
|
|
||||||
} else showToast(data.error || 'Failed', 'error');
|
|
||||||
})
|
|
||||||
.catch(function() { showToast('Request failed', 'error'); });
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteCategory(id) {
|
|
||||||
if (!window.__confirmDeleteCat) {
|
|
||||||
window.__confirmDeleteCat = id;
|
|
||||||
showToast('Click delete again to confirm removing this category', 'info');
|
|
||||||
setTimeout(function() { window.__confirmDeleteCat = null; }, 4000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.__confirmDeleteCat = null;
|
|
||||||
deleteJson('/api/admin/learning/categories/' + id)
|
|
||||||
.then(function(data) {
|
|
||||||
if (data.success) { loadCmsCategories(); loadCategories(); showToast('Category deleted', 'info'); }
|
|
||||||
else showToast(data.error || 'Failed', 'error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadCmsContent() {
|
|
||||||
var container = document.getElementById('lh-cms-content-list');
|
|
||||||
if (!container) return;
|
|
||||||
container.innerHTML = '<p style="text-align:center;color:var(--g400);padding:12px;">Loading...</p>';
|
|
||||||
|
|
||||||
getJson('/api/admin/learning/content')
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.success) return;
|
|
||||||
if (data.content.length === 0) {
|
|
||||||
container.innerHTML = '<p style="text-align:center;color:var(--g400);padding:12px;">No content yet. Click "New Content" to create.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window._cmsContentData = data.content;
|
|
||||||
renderCmsContentList(data.content);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCmsContentList(items) {
|
|
||||||
var container = document.getElementById('lh-cms-content-list');
|
|
||||||
if (!container) return;
|
|
||||||
if (!items || items.length === 0) {
|
|
||||||
container.innerHTML = renderCmsContentEmpty();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = items.map(renderCmsContentRow).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// CMS search & filter
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target.id === 'cms-search') filterCmsContent();
|
|
||||||
});
|
|
||||||
document.addEventListener('change', function(e) {
|
|
||||||
if (e.target.id === 'cms-filter-status' || e.target.id === 'cms-filter-category') {
|
|
||||||
if (e.target.id === 'cms-filter-category') setActiveCmsCat(e.target.value || 'all');
|
|
||||||
filterCmsContent();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function filterCmsContentByCategory(categoryId) {
|
|
||||||
var filterSel = document.getElementById('cms-filter-category');
|
|
||||||
if (filterSel) filterSel.value = String(categoryId);
|
|
||||||
setActiveCmsCat(categoryId);
|
|
||||||
filterCmsContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setActiveCmsCat(categoryId) {
|
|
||||||
categoryId = String(categoryId || 'all');
|
|
||||||
document.querySelectorAll('.lh-cms-cat-filter').forEach(function(item) {
|
|
||||||
item.classList.toggle('active', categoryId !== 'all' && item.dataset.id === categoryId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterCmsContent() {
|
|
||||||
var items = window._cmsContentData || [];
|
|
||||||
var search = (document.getElementById('cms-search') || {}).value || '';
|
|
||||||
var statusFilter = (document.getElementById('cms-filter-status') || {}).value || 'all';
|
|
||||||
var catFilter = (document.getElementById('cms-filter-category') || {}).value || 'all';
|
|
||||||
|
|
||||||
search = search.toLowerCase().trim();
|
|
||||||
var filtered = items.filter(function(item) {
|
|
||||||
if (search && item.title.toLowerCase().indexOf(search) === -1 && (item.subject || '').toLowerCase().indexOf(search) === -1) return false;
|
|
||||||
if (statusFilter === 'published' && !item.published) return false;
|
|
||||||
if (statusFilter === 'draft' && item.published) return false;
|
|
||||||
if (catFilter !== 'all' && String(item.category_id) !== catFilter) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
renderCmsContentList(filtered);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Body editor ────────────────────────────────────────────
|
// ── Body editor ────────────────────────────────────────────
|
||||||
function initBodyEditor() {
|
function initBodyEditor() {
|
||||||
var wrap = document.getElementById('lh-body-editor');
|
var wrap = document.getElementById('lh-body-editor');
|
||||||
|
|
@ -1272,9 +1120,7 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l
|
||||||
hideLoading();
|
hideLoading();
|
||||||
showToast('Content saved', 'success');
|
showToast('Content saved', 'success');
|
||||||
closeEditor();
|
closeEditor();
|
||||||
loadCmsContent();
|
cms.refreshAll();
|
||||||
loadCmsCategories();
|
|
||||||
loadCmsStats();
|
|
||||||
loaded = false; // force refresh user feed
|
loaded = false; // force refresh user feed
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
@ -1345,9 +1191,7 @@ import { buildViewerMeta, renderPresentationCard, renderProgressList } from './l
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showToast('Content deleted', 'info');
|
showToast('Content deleted', 'info');
|
||||||
closeEditor();
|
closeEditor();
|
||||||
loadCmsContent();
|
cms.refreshAll();
|
||||||
loadCmsCategories();
|
|
||||||
loadCmsStats();
|
|
||||||
loaded = false;
|
loaded = false;
|
||||||
} else showToast(data.error || 'Failed', 'error');
|
} else showToast(data.error || 'Failed', 'error');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
190
public/js/learningHub/cmsController.js
Normal file
190
public/js/learningHub/cmsController.js
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
import { deleteJson, getJson, sendJson } from './api.js';
|
||||||
|
import { renderCmsCategoryList, renderCmsContentEmpty, renderCmsContentRow } from './cmsRenderer.js';
|
||||||
|
import { esc } from './sanitize.js';
|
||||||
|
|
||||||
|
export function createCmsController(deps) {
|
||||||
|
var contentData = [];
|
||||||
|
|
||||||
|
function loadStats() {
|
||||||
|
getJson('/api/admin/learning/stats')
|
||||||
|
.then(function(data) {
|
||||||
|
if (!data.success) return;
|
||||||
|
var s = data.stats;
|
||||||
|
setText('cms-stat-published', s.publishedContent);
|
||||||
|
setText('cms-stat-drafts', s.totalContent - s.publishedContent);
|
||||||
|
setText('cms-stat-categories', s.totalCategories);
|
||||||
|
setText('cms-stat-quizzes', s.totalQuizzes);
|
||||||
|
setText('cms-stat-attempts', s.totalAttempts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCategories(editorCategoryId, filterCategoryId) {
|
||||||
|
getJson('/api/admin/learning/categories')
|
||||||
|
.then(function(data) {
|
||||||
|
if (!data.success) return;
|
||||||
|
var container = document.getElementById('lh-cms-categories');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.innerHTML = renderCmsCategoryList(data.categories);
|
||||||
|
updateCategorySelects(data.categories, editorCategoryId, filterCategoryId);
|
||||||
|
setActiveCategory((document.getElementById('cms-filter-category') || {}).value || 'all');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCategory() {
|
||||||
|
var input = document.getElementById('lh-cms-cat-name');
|
||||||
|
if (!input || !input.value.trim()) { deps.showToast('Enter category name', 'error'); return; }
|
||||||
|
|
||||||
|
sendJson('/api/admin/learning/categories', 'POST', { name: input.value.trim() })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
input.value = '';
|
||||||
|
loadCategories();
|
||||||
|
deps.refreshUserCategories();
|
||||||
|
deps.showToast('Category created', 'success');
|
||||||
|
} else deps.showToast(data.error || 'Failed', 'error');
|
||||||
|
})
|
||||||
|
.catch(function() { deps.showToast('Request failed', 'error'); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCategory(id) {
|
||||||
|
if (!window.__confirmDeleteCat) {
|
||||||
|
window.__confirmDeleteCat = id;
|
||||||
|
deps.showToast('Click delete again to confirm removing this category', 'info');
|
||||||
|
setTimeout(function() { window.__confirmDeleteCat = null; }, 4000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.__confirmDeleteCat = null;
|
||||||
|
deleteJson('/api/admin/learning/categories/' + id)
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
loadCategories();
|
||||||
|
deps.refreshUserCategories();
|
||||||
|
deps.showToast('Category deleted', 'info');
|
||||||
|
} else deps.showToast(data.error || 'Failed', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadContent() {
|
||||||
|
var container = document.getElementById('lh-cms-content-list');
|
||||||
|
if (!container) return;
|
||||||
|
container.innerHTML = '<p style="text-align:center;color:var(--g400);padding:12px;">Loading...</p>';
|
||||||
|
|
||||||
|
getJson('/api/admin/learning/content')
|
||||||
|
.then(function(data) {
|
||||||
|
if (!data.success) return;
|
||||||
|
contentData = data.content || [];
|
||||||
|
if (contentData.length === 0) {
|
||||||
|
container.innerHTML = '<p style="text-align:center;color:var(--g400);padding:12px;">No content yet. Click "New Content" to create.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderContentList(contentData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFilterInput(e) {
|
||||||
|
if (e.target.id === 'cms-search') filterContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFilterChange(e) {
|
||||||
|
if (e.target.id !== 'cms-filter-status' && e.target.id !== 'cms-filter-category') return;
|
||||||
|
if (e.target.id === 'cms-filter-category') setActiveCategory(e.target.value || 'all');
|
||||||
|
filterContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByCategory(categoryId) {
|
||||||
|
var filterSel = document.getElementById('cms-filter-category');
|
||||||
|
if (filterSel) filterSel.value = String(categoryId);
|
||||||
|
setActiveCategory(categoryId);
|
||||||
|
filterContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAll() {
|
||||||
|
loadContent();
|
||||||
|
loadCategories();
|
||||||
|
loadStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContentList(items) {
|
||||||
|
var container = document.getElementById('lh-cms-content-list');
|
||||||
|
if (!container) return;
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
container.innerHTML = renderCmsContentEmpty();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = items.map(renderCmsContentRow).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterContent() {
|
||||||
|
var search = (document.getElementById('cms-search') || {}).value || '';
|
||||||
|
var statusFilter = (document.getElementById('cms-filter-status') || {}).value || 'all';
|
||||||
|
var catFilter = (document.getElementById('cms-filter-category') || {}).value || 'all';
|
||||||
|
|
||||||
|
search = search.toLowerCase().trim();
|
||||||
|
var filtered = contentData.filter(function(item) {
|
||||||
|
var title = (item.title || '').toLowerCase();
|
||||||
|
var subject = (item.subject || '').toLowerCase();
|
||||||
|
if (search && title.indexOf(search) === -1 && subject.indexOf(search) === -1) return false;
|
||||||
|
if (statusFilter === 'published' && !item.published) return false;
|
||||||
|
if (statusFilter === 'draft' && item.published) return false;
|
||||||
|
if (catFilter !== 'all' && String(item.category_id) !== catFilter) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
renderContentList(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addCategory: addCategory,
|
||||||
|
deleteCategory: deleteCategory,
|
||||||
|
filterByCategory: filterByCategory,
|
||||||
|
handleFilterChange: handleFilterChange,
|
||||||
|
handleFilterInput: handleFilterInput,
|
||||||
|
loadCategories: loadCategories,
|
||||||
|
loadContent: loadContent,
|
||||||
|
loadStats: loadStats,
|
||||||
|
refreshAll: refreshAll
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCategorySelects(categories, editorCategoryId, filterCategoryId) {
|
||||||
|
var sel = document.getElementById('lh-cms-edit-category');
|
||||||
|
if (sel) {
|
||||||
|
var selectedEditorId = editorCategoryId !== undefined ? editorCategoryId : sel.value;
|
||||||
|
sel.innerHTML = '<option value="">Uncategorized</option>';
|
||||||
|
categories.forEach(function(c) {
|
||||||
|
sel.innerHTML += '<option value="' + c.id + '">' + esc(c.name) + '</option>';
|
||||||
|
});
|
||||||
|
if (selectedEditorId && hasSelectValue(sel, selectedEditorId)) sel.value = selectedEditorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filterSel = document.getElementById('cms-filter-category');
|
||||||
|
if (filterSel) {
|
||||||
|
var selectedFilterId = filterCategoryId !== undefined ? filterCategoryId : filterSel.value;
|
||||||
|
filterSel.innerHTML = '<option value="all">All Categories</option>';
|
||||||
|
categories.forEach(function(c) {
|
||||||
|
filterSel.innerHTML += '<option value="' + c.id + '">' + esc(c.name) + '</option>';
|
||||||
|
});
|
||||||
|
if (selectedFilterId && hasSelectValue(filterSel, selectedFilterId)) filterSel.value = selectedFilterId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setText(id, val) {
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (el) el.textContent = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveCategory(categoryId) {
|
||||||
|
categoryId = String(categoryId || 'all');
|
||||||
|
document.querySelectorAll('.lh-cms-cat-filter').forEach(function(item) {
|
||||||
|
item.classList.toggle('active', categoryId !== 'all' && item.dataset.id === categoryId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSelectValue(sel, value) {
|
||||||
|
value = String(value);
|
||||||
|
for (var i = 0; i < sel.options.length; i++) {
|
||||||
|
if (sel.options[i].value === value) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ test('Learning Hub applies AI-generated category names to the editor', () => {
|
||||||
assert.match(frontend, /function applyAiCategory\(categoryName\)/);
|
assert.match(frontend, /function applyAiCategory\(categoryName\)/);
|
||||||
assert.match(frontend, /sendJson\('\/api\/admin\/learning\/categories', 'POST', \{ name: categoryName \}\)/);
|
assert.match(frontend, /sendJson\('\/api\/admin\/learning\/categories', 'POST', \{ name: categoryName \}\)/);
|
||||||
assert.match(frontend, /findCategoryOption\(sel, categoryName\)/);
|
assert.match(frontend, /findCategoryOption\(sel, categoryName\)/);
|
||||||
assert.match(frontend, /loadCmsCategories\(\);/);
|
assert.match(frontend, /cms\.loadCategories\(String\(data\.id\)\)/);
|
||||||
assert.match(frontend, /loadCategories\(\);/);
|
assert.match(frontend, /loadCategories\(\);/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ function read(relativePath) {
|
||||||
|
|
||||||
const source = read('public/js/learningHub.js');
|
const source = read('public/js/learningHub.js');
|
||||||
const apiModule = read('public/js/learningHub/api.js');
|
const apiModule = read('public/js/learningHub/api.js');
|
||||||
|
const cmsControllerModule = read('public/js/learningHub/cmsController.js');
|
||||||
const cmsRendererModule = read('public/js/learningHub/cmsRenderer.js');
|
const cmsRendererModule = read('public/js/learningHub/cmsRenderer.js');
|
||||||
const editorModule = read('public/js/learningHub/tiptapEditor.js');
|
const editorModule = read('public/js/learningHub/tiptapEditor.js');
|
||||||
const feedRendererModule = read('public/js/learningHub/feedRenderer.js');
|
const feedRendererModule = read('public/js/learningHub/feedRenderer.js');
|
||||||
|
|
@ -75,22 +76,38 @@ test('Learning Hub CMS renderers are isolated from API and editor flows', () =>
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Learning Hub preserves AI-created category selection before save', () => {
|
test('Learning Hub preserves AI-created category selection before save', () => {
|
||||||
assert.match(source, /function loadCmsCategories\(editorCategoryId, filterCategoryId\)/);
|
assert.match(cmsControllerModule, /function loadCategories\(editorCategoryId, filterCategoryId\)/);
|
||||||
assert.match(source, /var selectedEditorId = editorCategoryId !== undefined \? editorCategoryId : sel\.value/);
|
assert.match(cmsControllerModule, /var selectedEditorId = editorCategoryId !== undefined \? editorCategoryId : sel\.value/);
|
||||||
assert.match(source, /if \(selectedEditorId && hasSelectValue\(sel, selectedEditorId\)\) sel\.value = selectedEditorId/);
|
assert.match(cmsControllerModule, /if \(selectedEditorId && hasSelectValue\(sel, selectedEditorId\)\) sel\.value = selectedEditorId/);
|
||||||
assert.match(source, /loadCmsCategories\(String\(data\.id\)\)/);
|
assert.match(source, /cms\.loadCategories\(String\(data\.id\)\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Learning Hub CMS sidebar categories filter content list', () => {
|
test('Learning Hub CMS sidebar categories filter content list', () => {
|
||||||
assert.match(source, /filterCmsContentByCategory\(cmsCat\.dataset\.id\)/);
|
assert.match(source, /cms\.filterByCategory\(cmsCat\.dataset\.id\)/);
|
||||||
assert.match(source, /function filterCmsContentByCategory\(categoryId\)/);
|
assert.match(cmsControllerModule, /function filterByCategory\(categoryId\)/);
|
||||||
assert.match(source, /function setActiveCmsCat\(categoryId\)/);
|
assert.match(cmsControllerModule, /function setActiveCategory\(categoryId\)/);
|
||||||
assert.match(source, /item\.classList\.toggle\('active'/);
|
assert.match(cmsControllerModule, /item\.classList\.toggle\('active'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Learning Hub refreshes CMS category counts after content changes', () => {
|
test('Learning Hub refreshes CMS category counts after content changes', () => {
|
||||||
assert.match(source, /saveQuestions\(contentId, questions, 0, function\(\) \{[\s\S]*?loadCmsContent\(\);[\s\S]*?loadCmsCategories\(\);[\s\S]*?loadCmsStats\(\);/);
|
assert.match(source, /saveQuestions\(contentId, questions, 0, function\(\) \{[\s\S]*?cms\.refreshAll\(\);/);
|
||||||
assert.match(source, /showToast\('Content deleted', 'info'\);[\s\S]*?loadCmsContent\(\);[\s\S]*?loadCmsCategories\(\);[\s\S]*?loadCmsStats\(\);/);
|
assert.match(source, /showToast\('Content deleted', 'info'\);[\s\S]*?cms\.refreshAll\(\);/);
|
||||||
|
assert.match(cmsControllerModule, /function refreshAll\(\) \{[\s\S]*?loadContent\(\);[\s\S]*?loadCategories\(\);[\s\S]*?loadStats\(\);/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Learning Hub CMS controller owns admin list operations', () => {
|
||||||
|
assert.match(source, /from '\.\/learningHub\/cmsController\.js'/);
|
||||||
|
assert.match(source, /var cms = createCmsController\(/);
|
||||||
|
assert.match(source, /cms\.loadContent\(\); cms\.loadStats\(\)/);
|
||||||
|
assert.match(source, /cms\.addCategory\(\)/);
|
||||||
|
assert.match(source, /cms\.deleteCategory\(delCat\.dataset\.id\)/);
|
||||||
|
assert.match(cmsControllerModule, /export function createCmsController\(deps\)/);
|
||||||
|
assert.match(cmsControllerModule, /function loadStats\(\)/);
|
||||||
|
assert.match(cmsControllerModule, /function loadCategories\(editorCategoryId, filterCategoryId\)/);
|
||||||
|
assert.match(cmsControllerModule, /function loadContent\(\)/);
|
||||||
|
assert.match(cmsControllerModule, /function handleFilterInput\(e\)/);
|
||||||
|
assert.doesNotMatch(cmsControllerModule, /window\.Tiptap|ai-generate|FormData/);
|
||||||
|
assert.doesNotMatch(source, /function loadCmsStats\(\)|function loadCmsCategories\(|function loadCmsContent\(\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Learning Hub slide controller owns preview and PPTX flow', () => {
|
test('Learning Hub slide controller owns preview and PPTX flow', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue