Files
doctate/server/templates/partials/oneliner_edit.js
T
Brummel 2c6062a53e refactor: drop /web/ URL prefix from browser routes
The /web/ prefix predated the /api/ split; today it just clutters every URL
without disambiguating anything. All 16 browser routes move to the apex
(/cases, /login, /magic, /events, /audio/...). The 6 /api/* routes are
unchanged. A new /->>/cases redirect closes the apex 404.

The open-redirect guard in magic.rs and case_actions.rs flips from a
positive whitelist (starts_with("/web/")) to a deny-list: same-origin path,
not protocol-relative, not under /api/, no \. The /api/ exclusion is now
load-bearing and covered by tests.

Pre-production: no transition redirects.
2026-05-04 18:36:10 +02:00

77 lines
2.6 KiB
JavaScript

// Inline tap-to-edit for .oneliner-edit blocks. Click swaps the inner
// .oneliner-text for an <input>; Enter POSTs to /cases/{id}/oneliner;
// Escape (or blur) cancels. The SSE-driven reload picks up the persisted
// state, so no DOM patching is needed on success.
(() => {
const handleClick = (host) => {
if (host.classList.contains('editing')) return;
const span = host.querySelector('.oneliner-text');
if (!span) return;
const original = span.textContent.trim();
host.classList.add('editing');
const input = document.createElement('input');
input.type = 'text';
input.value = original;
input.maxLength = 500;
input.className = 'oneliner-input';
span.replaceWith(input);
input.focus();
input.select();
const restore = () => {
const restoreSpan = document.createElement('span');
restoreSpan.className = 'oneliner-text';
restoreSpan.textContent = original;
input.replaceWith(restoreSpan);
host.classList.remove('editing', 'saving', 'error');
};
const submit = async () => {
const text = input.value.trim();
if (text === '' || text === original) { restore(); return; }
host.classList.remove('error');
host.classList.add('saving');
const body = new URLSearchParams({
text,
csrf_token: host.dataset.csrf,
});
try {
const r = await fetch(
`/cases/${host.dataset.caseId}/oneliner`,
{ method: 'POST', body, credentials: 'same-origin' }
);
if (!r.ok) {
host.classList.remove('saving');
host.classList.add('error');
input.focus();
return;
}
// SSE will trigger reload; keep input visible until then.
} catch (_) {
host.classList.remove('saving');
host.classList.add('error');
input.focus();
}
};
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); submit(); }
else if (e.key === 'Escape') { e.preventDefault(); restore(); }
});
input.addEventListener('blur', () => {
// Blur acts as cancel unless we're already saving (where blur
// happens implicitly when the SSE reload swaps the DOM) or in
// an error state (so the user can re-focus and retry).
if (host.classList.contains('saving') || host.classList.contains('error')) return;
restore();
});
};
document.addEventListener('click', (e) => {
const host = e.target.closest('.oneliner-edit');
if (!host) return;
if (e.target.closest('a, button, input')) return;
handleClick(host);
});
})();