fix: make manual CMS category creation visible and keyboard-accessible
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m46s
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m46s
This commit is contained in:
parent
baab83e50e
commit
5c5d68a7c1
4 changed files with 95 additions and 7 deletions
|
|
@ -20,10 +20,10 @@
|
|||
<div class="cms-sidebar-section">
|
||||
<h4><i class="fas fa-folder-tree"></i> Categories</h4>
|
||||
<div id="lh-cms-categories" class="cms-cat-list"></div>
|
||||
<div class="cms-add-cat">
|
||||
<input type="text" id="lh-cms-cat-name" placeholder="New category..." class="cms-input-sm">
|
||||
<button id="btn-lh-add-cat" class="cms-btn-add" title="Add category"><i class="fas fa-plus"></i></button>
|
||||
</div>
|
||||
<form id="lh-cms-add-category" class="cms-add-cat">
|
||||
<input type="text" id="lh-cms-cat-name" placeholder="New category..." aria-label="New category name" class="cms-input-sm">
|
||||
<button type="submit" id="btn-lh-add-cat" class="cms-btn-add" title="Add category">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="cms-sidebar-section">
|
||||
|
|
|
|||
|
|
@ -684,8 +684,9 @@ textarea.full-input{resize:vertical;}
|
|||
.cms-cat-item:hover{background:var(--g50);}
|
||||
.cms-cat-item .cms-cat-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.cms-cat-item .cms-cat-count{font-size:11px;color:var(--g400);margin:0 6px;}
|
||||
.cms-add-cat{display:flex;gap:4px;}
|
||||
.cms-btn-add{width:32px;height:32px;border:1px solid var(--g300);border-radius:6px;background:white;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--blue);font-size:12px;}
|
||||
.cms-add-cat{display:flex;gap:4px;margin:0;}
|
||||
.cms-add-cat .cms-input-sm{flex:1;min-width:0;width:0;}
|
||||
.cms-btn-add{flex-shrink:0;min-width:44px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;background:white;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--blue);font-size:12px;}
|
||||
.cms-btn-add:hover{background:var(--blue-light);}
|
||||
|
||||
/* Input small */
|
||||
|
|
|
|||
|
|
@ -107,7 +107,6 @@ import { createWebdavController } from './learningHub/webdavController.js';
|
|||
|
||||
// ── CMS events ─────────────────────────────
|
||||
if (e.target.closest('#btn-lh-refresh-content')) { cms.loadContent(); cms.loadStats(); 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-quiz')) { openEditor(null, 'quiz'); return; }
|
||||
if (e.target.closest('#btn-lh-new-pearl')) { openEditor(null, 'pearl'); return; }
|
||||
|
|
@ -177,6 +176,13 @@ import { createWebdavController } from './learningHub/webdavController.js';
|
|||
if (rmOpt) { var row = rmOpt.closest('.lh-option-row'); if (row) { destroyOptionEditor(row); row.remove(); } return; }
|
||||
});
|
||||
|
||||
// Native form submission handles both the visible Add button and Enter.
|
||||
document.addEventListener('submit', function(e) {
|
||||
if (e.target.id !== 'lh-cms-add-category') return;
|
||||
e.preventDefault();
|
||||
cms.addCategory();
|
||||
});
|
||||
|
||||
document.addEventListener('input', function(e) { cms.handleFilterInput(e); });
|
||||
document.addEventListener('change', function(e) { cms.handleFilterChange(e); });
|
||||
|
||||
|
|
|
|||
81
test/learning-hub-manual-category.test.js
Normal file
81
test/learning-hub-manual-category.test.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
const { JSDOM } = require('jsdom');
|
||||
|
||||
const root = process.env.PEDAI_CMS_TEST_ROOT || path.join(__dirname, '..');
|
||||
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
const tick = () => new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
test('actual CMS category form creates manually once, supports native submit and preserves failed drafts', async () => {
|
||||
const dom = new JSDOM('<!doctype html><style>' + read('public/css/styles.css') + '</style><body>' + read('public/components/cms.html') + '</body>');
|
||||
const keys = ['window', 'document', 'fetch', 'getAuthHeaders', 'showToast'];
|
||||
const previous = Object.fromEntries(keys.map(key => [key, global[key]]));
|
||||
const calls = [], categories = [], notices = [];
|
||||
let fail = false;
|
||||
global.window = dom.window;
|
||||
global.document = dom.window.document;
|
||||
global.getAuthHeaders = () => ({ 'Content-Type': 'application/json' });
|
||||
global.showToast = (message, type) => notices.push({ message, type });
|
||||
global.fetch = async (url, options = {}) => {
|
||||
calls.push({ url, options });
|
||||
assert.ok(!url.includes('ai-generate'), 'Manual categories must not call AI');
|
||||
if (options.method === 'POST') {
|
||||
assert.equal(url, '/api/admin/learning/categories');
|
||||
const payload = JSON.parse(options.body);
|
||||
if (fail) return { json: async () => ({ success: false, error: 'Synthetic rejection' }) };
|
||||
categories.push({ id: categories.length + 1, name: payload.name, slug: 'category-' + (categories.length + 1), content_count: 0 });
|
||||
return { json: async () => ({ success: true, id: categories.length }) };
|
||||
}
|
||||
assert.ok(['/api/admin/learning/categories', '/api/learning/categories'].includes(url), url);
|
||||
return { json: async () => ({ success: true, categories }) };
|
||||
};
|
||||
try {
|
||||
const input = document.getElementById('lh-cms-cat-name');
|
||||
const button = document.getElementById('btn-lh-add-cat');
|
||||
const form = input.closest('form');
|
||||
assert.ok(form, 'Category entry needs native form submission for Enter');
|
||||
assert.equal(form.id, 'lh-cms-add-category');
|
||||
assert.equal(button.type, 'submit');
|
||||
assert.equal(button.textContent.trim(), 'Add', 'Creation remains discoverable without icon fonts');
|
||||
assert.equal(input.getAttribute('aria-label'), 'New category name');
|
||||
assert.match(window.getComputedStyle(input).minWidth, /^0(px)?$/, 'Input must shrink inside the sidebar');
|
||||
assert.equal(window.getComputedStyle(button).flexShrink, '0', 'The Add control must remain visible');
|
||||
|
||||
await import(pathToFileURL(path.join(root, 'public/js/learningHub.js')).href);
|
||||
const prevented = [];
|
||||
document.addEventListener('submit', event => prevented.push(event.defaultPrevented));
|
||||
input.value = ' Manual category ';
|
||||
button.click();
|
||||
await tick(); await tick();
|
||||
assert.equal(calls.filter(c => c.options.method === 'POST').length, 1);
|
||||
assert.equal(categories[0].name, 'Manual category');
|
||||
assert.equal(input.value, '');
|
||||
assert.match(document.getElementById('lh-cms-categories').textContent, /Manual category/);
|
||||
|
||||
input.value = 'Keyboard category';
|
||||
form.requestSubmit(); // Same submit path as the browser's implicit Enter action.
|
||||
await tick(); await tick();
|
||||
assert.equal(categories.length, 2);
|
||||
assert.equal(calls.filter(c => c.options.method === 'POST').length, 2);
|
||||
|
||||
input.value = ' ';
|
||||
form.requestSubmit();
|
||||
await tick();
|
||||
assert.equal(calls.filter(c => c.options.method === 'POST').length, 2);
|
||||
assert.equal(notices.at(-1).message, 'Enter category name');
|
||||
|
||||
fail = true;
|
||||
input.value = 'Keep this draft';
|
||||
button.click();
|
||||
await tick(); await tick();
|
||||
assert.equal(input.value, 'Keep this draft');
|
||||
assert.equal(notices.at(-1).message, 'Synthetic rejection');
|
||||
assert.deepEqual(prevented, [true, true, true, true], 'Every path prevents native page navigation');
|
||||
} finally {
|
||||
for (const key of keys) global[key] = previous[key];
|
||||
dom.window.close();
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue