feat(cms): keyboard shortcuts — Ctrl+S save, Ctrl+P publish, ? help
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
- Ctrl/Cmd+S saves (native browser save dialog suppressed) - Ctrl/Cmd+P publishes; Ctrl/Cmd+Shift+V opens the Versions panel - '?' toggles a shortcuts overlay (Esc/click closes) - plain typing in inputs never triggers actions (modifiers required; '?' only outside editing targets) - jsdom regression tests run the real client script with dispatched KeyboardEvents; the client is evaluated once per suite because each eval would stack another keydown listener on the shared document Closes MITHOME-75
This commit is contained in:
@@ -70,6 +70,15 @@ A dokumentum a repó része, és **folyamatosan karbantartott**: minden funkció
|
|||||||
- A régi logó mentésre kerül — a csere biztonságos és visszavonható (a mentések a `.content-backups` mappában).
|
- A régi logó mentésre kerül — a csere biztonságos és visszavonható (a mentések a `.content-backups` mappában).
|
||||||
- A **CMS azonnal** az új logót mutatja; a **weboldalon a Publikálás (deploy) után** jelenik meg.
|
- A **CMS azonnal** az új logót mutatja; a **weboldalon a Publikálás (deploy) után** jelenik meg.
|
||||||
|
|
||||||
|
### ⌨️ Gyorsbillentyűk
|
||||||
|
|
||||||
|
- **Ctrl/Cmd + S** — Mentés
|
||||||
|
- **Ctrl/Cmd + P** — Publikálás
|
||||||
|
- **Ctrl/Cmd + Shift + V** — Verziók panel megnyitása új fülön
|
||||||
|
- **?** — gyorsbillentyű-súgó megjelenítése (Esc vagy kattintás zárja)
|
||||||
|
|
||||||
|
A gyorsbillentyűk csak a szerkesztő főoldalán működnek; beviteli mezőben gépelve a normál karakterként viselkednek.
|
||||||
|
|
||||||
### 🕘 Verziók — korábbi állapotok
|
### 🕘 Verziók — korábbi állapotok
|
||||||
|
|
||||||
- Az alsó sáv **🕘 Verziók** gombja megnyitja az éppen szerkesztett fájl mentéseit (minden Mentés automatikus másolatot készít).
|
- Az alsó sáv **🕘 Verziók** gombja megnyitja az éppen szerkesztett fájl mentéseit (minden Mentés automatikus másolatot készít).
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Regression tests for the Content Editor keyboard shortcuts (MITHOME-75).
|
||||||
|
* Runs the real scripts/cms-editor-client.js in jsdom, dispatches actual
|
||||||
|
* KeyboardEvents and asserts:
|
||||||
|
* 1. Ctrl+S calls save (fetch /save) and the browser save dialog is
|
||||||
|
* suppressed (preventDefault)
|
||||||
|
* 2. Ctrl+P calls publish
|
||||||
|
* 3. Ctrl+Shift+V opens the versions page in a new tab
|
||||||
|
* 4. '?' opens the shortcuts overlay, Esc closes it
|
||||||
|
* 5. plain 's' inside an input does NOT trigger save
|
||||||
|
*/
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const clientJs = fs.readFileSync(path.join(__dirname, '../../../scripts/cms-editor-client.js'), 'utf8')
|
||||||
|
|
||||||
|
const makeData = () => ({
|
||||||
|
hero: { title: 'T', subtitle: 'S', description: 'D' },
|
||||||
|
})
|
||||||
|
|
||||||
|
function press(target: Document | Element, init: KeyboardEventInit) {
|
||||||
|
const ev = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init })
|
||||||
|
target.dispatchEvent(ev)
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
const flush = () => new Promise(r => setTimeout(r, 0))
|
||||||
|
|
||||||
|
// Globals must exist BEFORE the eval — the client script boots immediately
|
||||||
|
// (render(DATA, …)). The eval runs ONCE: every eval would add another keydown
|
||||||
|
// listener to the shared jsdom document, and the toggle-style overlay handler
|
||||||
|
// would then open/close itself multiple times per keypress.
|
||||||
|
;(global as any).DATA = makeData()
|
||||||
|
;(global as any).FILE = 'home'
|
||||||
|
;(global as any).CSRF_TOKEN = 'boot'
|
||||||
|
;(global as any).CONTENT_HASH = 'x'.repeat(64)
|
||||||
|
;(global as any).fetch = jest.fn(async () => ({ status: 200, ok: true, json: async () => ({ ok: true }) }))
|
||||||
|
;(global as any).window = global
|
||||||
|
document.body.innerHTML = '<div id="editor"></div>'
|
||||||
|
;(0, eval)(clientJs)
|
||||||
|
|
||||||
|
describe('Content Editor keyboard shortcuts', () => {
|
||||||
|
let fetchCalls: Array<{ url: string; init?: RequestInit }>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchCalls = []
|
||||||
|
;(global as any).DATA = makeData()
|
||||||
|
;(global as any).FILE = 'home'
|
||||||
|
;(global as any).CSRF_TOKEN = 'test-csrf'
|
||||||
|
;(global as any).CONTENT_HASH = 'x'.repeat(64)
|
||||||
|
;(global as any).fetch = jest.fn(async (url: string, init?: RequestInit) => {
|
||||||
|
fetchCalls.push({ url, init })
|
||||||
|
return { status: 200, ok: true, json: async () => ({ ok: true, contentHash: 'y'.repeat(64) }) }
|
||||||
|
})
|
||||||
|
;(global as any).window = global
|
||||||
|
;(window as any).open = jest.fn()
|
||||||
|
document.body.innerHTML = '<div id="editor"></div><span id="saveStatus" style="display:none"></span><button id="publishBtn">pub</button>'
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete (global as any).DATA
|
||||||
|
delete (global as any).FILE
|
||||||
|
delete (global as any).CSRF_TOKEN
|
||||||
|
delete (global as any).CONTENT_HASH
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Ctrl+S saves via fetch and suppresses the browser save dialog', () => {
|
||||||
|
const ev = press(document, { key: 's', ctrlKey: true })
|
||||||
|
expect(ev.defaultPrevented).toBe(true)
|
||||||
|
expect(fetchCalls.length).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(fetchCalls[0].url).toContain('/save?file=home')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cmd+P publishes', async () => {
|
||||||
|
const ev = press(document, { key: 'p', metaKey: true })
|
||||||
|
expect(ev.defaultPrevented).toBe(true)
|
||||||
|
await flush() // publish awaits save() before its own fetch
|
||||||
|
expect(fetchCalls.some(c => c.url === '/publish')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Ctrl+Shift+V opens the versions page in a new tab', () => {
|
||||||
|
const ev = press(document, { key: 'V', ctrlKey: true, shiftKey: true })
|
||||||
|
expect(ev.defaultPrevented).toBe(true)
|
||||||
|
expect((window as any).open).toHaveBeenCalledWith('/versions?file=home', '_blank')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("'?' opens the shortcuts overlay and Esc closes it", () => {
|
||||||
|
press(document, { key: '?' })
|
||||||
|
expect(document.getElementById('shortcuts-overlay')).not.toBeNull()
|
||||||
|
press(document, { key: 'Escape' })
|
||||||
|
expect(document.getElementById('shortcuts-overlay')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('plain typing in an input never triggers save', () => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
document.body.appendChild(input)
|
||||||
|
const ev = press(input, { key: 's' })
|
||||||
|
expect(ev.defaultPrevented).toBe(false)
|
||||||
|
expect(fetchCalls.length).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -329,3 +329,59 @@ render(DATA, document.getElementById('editor'));
|
|||||||
// Auto-dismiss toast
|
// Auto-dismiss toast
|
||||||
const toast = document.querySelector('.toast');
|
const toast = document.querySelector('.toast');
|
||||||
if (toast) setTimeout(() => toast.remove(), 3500);
|
if (toast) setTimeout(() => toast.remove(), 3500);
|
||||||
|
|
||||||
|
// ── Keyboard shortcuts ───────────────────────────────────────────────────────
|
||||||
|
// Ctrl/Cmd+S save · Ctrl/Cmd+P publish · Ctrl/Cmd+Shift+V versions · ? help
|
||||||
|
// Plain typing in inputs never triggers actions — the handler requires the
|
||||||
|
// modifier key (or, for '?', a non-editing target).
|
||||||
|
|
||||||
|
function showShortcutsOverlay() {
|
||||||
|
const existing = document.getElementById('shortcuts-overlay');
|
||||||
|
if (existing) { existing.remove(); return; }
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.id = 'shortcuts-overlay';
|
||||||
|
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(15,17,23,.75);z-index:300;display:flex;align-items:center;justify-content:center;padding:24px;';
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div style="background:#1a2035;border:1px solid #2d3748;border-radius:14px;padding:28px 32px;max-width:420px;width:100%;font-size:14px;line-height:2;color:#e2e8f0;">
|
||||||
|
<h2 style="font-size:16px;color:#93c5fd;margin-bottom:12px;">⌨️ Gyorsbillentyűk</h2>
|
||||||
|
<div><kbd style="background:#0f1420;border:1px solid #2d3748;border-radius:5px;padding:2px 8px;font-family:monospace;">Ctrl/Cmd + S</kbd> — Mentés</div>
|
||||||
|
<div><kbd style="background:#0f1420;border:1px solid #2d3748;border-radius:5px;padding:2px 8px;font-family:monospace;">Ctrl/Cmd + P</kbd> — Publikálás</div>
|
||||||
|
<div><kbd style="background:#0f1420;border:1px solid #2d3748;border-radius:5px;padding:2px 8px;font-family:monospace;">Ctrl/Cmd + Shift + V</kbd> — Verziók</div>
|
||||||
|
<div><kbd style="background:#0f1420;border:1px solid #2d3748;border-radius:5px;padding:2px 8px;font-family:monospace;">?</kbd> — ez a súgó (Esc: bezárás)</div>
|
||||||
|
</div>`;
|
||||||
|
overlay.addEventListener('click', () => overlay.remove());
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
// Esc closes the shortcut overlay if open
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
const overlay = document.getElementById('shortcuts-overlay');
|
||||||
|
if (overlay) { overlay.remove(); e.preventDefault(); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mod = e.ctrlKey || e.metaKey;
|
||||||
|
if (mod && !e.shiftKey && !e.altKey && (e.key === 's' || e.key === 'S')) {
|
||||||
|
e.preventDefault();
|
||||||
|
save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mod && !e.shiftKey && !e.altKey && (e.key === 'p' || e.key === 'P')) {
|
||||||
|
e.preventDefault();
|
||||||
|
publish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mod && e.shiftKey && (e.key === 'v' || e.key === 'V')) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.open('/versions?file=' + encodeURIComponent(FILE), '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mod && !e.ctrlKey && !e.metaKey && !e.altKey && e.key === '?') {
|
||||||
|
const target = e.target;
|
||||||
|
const isEditing = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable);
|
||||||
|
if (!isEditing) {
|
||||||
|
e.preventDefault();
|
||||||
|
showShortcutsOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ const document = {
|
|||||||
getElementById: id => id === 'page-data' ? { textContent: JSON.stringify(fixture) } : {},
|
getElementById: id => id === 'page-data' ? { textContent: JSON.stringify(fixture) } : {},
|
||||||
querySelectorAll: selector => selector === '[data-path]' ? fields : [],
|
querySelectorAll: selector => selector === '[data-path]' ? fields : [],
|
||||||
querySelector: () => null,
|
querySelector: () => null,
|
||||||
|
// Keyboard-shortcut binding in the client script — not under test here.
|
||||||
|
addEventListener: () => {},
|
||||||
};
|
};
|
||||||
const browserContext = { document, console, setTimeout, fetch: async () => ({ json: async () => ({ ok: true }) }) };
|
const browserContext = { document, console, setTimeout, fetch: async () => ({ json: async () => ({ ok: true }) }) };
|
||||||
vm.createContext(browserContext);
|
vm.createContext(browserContext);
|
||||||
|
|||||||
Reference in New Issue
Block a user