66 lines
4.2 KiB
JavaScript
66 lines
4.2 KiB
JavaScript
/** Shared optimistic concurrency for dashboard and inline content forms. */
|
|
(() => {
|
|
'use strict';
|
|
if (window.RaijuRevisions) return;
|
|
const revisions = new Map();
|
|
const originalFetch = window.fetch.bind(window);
|
|
const tr = key => (window.RJ_LANG || {})['conflict.' + key] || key;
|
|
let dialog;
|
|
async function showConflict(url, action, page, pending) {
|
|
if (dialog?.open) return;
|
|
const latestUrl = new URL(url);
|
|
latestUrl.search = new URLSearchParams({ action: action === 'save' ? 'load' : 'load-settings', ...(page ? { page } : {}) });
|
|
let latest;
|
|
try { latest = (await (await originalFetch(latestUrl)).json()).data; }
|
|
catch (_) { latest = tr('unavailable'); }
|
|
const previousFocus = document.activeElement;
|
|
dialog = document.createElement('dialog');
|
|
dialog.className = 'raiju-conflict';
|
|
const heading = document.createElement('h2');
|
|
heading.id = 'raijuConflictTitle'; heading.textContent = tr('changed');
|
|
dialog.setAttribute('aria-labelledby', heading.id);
|
|
const hint = document.createElement('p'); hint.textContent = tr('hint');
|
|
const columns = document.createElement('div'); columns.className = 'raiju-conflict__columns';
|
|
for (const [label, value] of [[tr('yours'), pending], [tr('current'), latest]]) {
|
|
const wrapper = document.createElement('label'); wrapper.textContent = label;
|
|
const content = document.createElement('textarea'); content.readOnly = true;
|
|
content.value = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
|
wrapper.append(content); columns.append(wrapper);
|
|
}
|
|
const actions = document.createElement('div'); actions.className = 'raiju-conflict__actions';
|
|
function button(label, callback) { const btn = document.createElement('button'); btn.type = 'button'; btn.textContent = label; btn.onclick = callback; actions.append(btn); }
|
|
button(tr('keep'), () => dialog.close());
|
|
button(tr('download'), () => {
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(new Blob([typeof pending === 'string' ? pending : JSON.stringify(pending, null, 2)], { type: 'application/json' }));
|
|
link.download = (page || 'settings') + '-unsaved.json'; link.click(); URL.revokeObjectURL(link.href);
|
|
});
|
|
button(tr('reload'), () => window.location.reload());
|
|
dialog.append(heading, hint, columns, actions); document.body.append(dialog);
|
|
dialog.addEventListener('close', () => { dialog.remove(); previousFocus?.focus(); });
|
|
dialog.showModal();
|
|
}
|
|
window.fetch = async function(input, init) {
|
|
const url = new URL(input instanceof Request ? input.url : input, location.href);
|
|
const apiUrl = new URL(window.RJ_ADMIN_API_URL || 'api.php', location.href);
|
|
if (url.origin !== apiUrl.origin || url.pathname !== apiUrl.pathname) return originalFetch(input, init);
|
|
const body = init?.body;
|
|
const fields = body instanceof FormData || body instanceof URLSearchParams ? body : url.searchParams;
|
|
const action = fields.get('action') || url.searchParams.get('action');
|
|
const page = fields.get('page') || url.searchParams.get('page') || '';
|
|
const key = ['load-settings', 'save-settings'].includes(action) ? 'settings' : 'page:' + page;
|
|
const write = ['save', 'save-settings'].includes(action);
|
|
if (write && fields !== url.searchParams && !fields.has('revision') && revisions.has(key)) fields.set('revision', revisions.get(key));
|
|
const response = await originalFetch(input, init);
|
|
if (['load', 'load-settings', 'save', 'save-settings'].includes(action)) {
|
|
try {
|
|
const payload = await response.clone().json();
|
|
if (payload.success && payload.revision) revisions.set(key, payload.revision);
|
|
if ([409, 428].includes(response.status)) void showConflict(url, action, page, fields.get(action === 'save' ? 'content' : 'settings'));
|
|
} catch (_) { /* The caller owns transport/error handling. */ }
|
|
}
|
|
return response;
|
|
};
|
|
window.RaijuRevisions = { get: key => revisions.get(key) };
|
|
})();
|