mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 14:55:08 +00:00
feat(web): Rotation, Operation History, Config Editor and Backup & Restore become ES-module pages (stage 2) (#727)
Rotation, Operation History, Config Editor and Backup & Restore become ES-module pages (stage 2): no inline scripts or onclick in the four partials, delegated data-action listeners, reads cancelled on swap-out, old globals kept as deprecated aliases through window.LEDMatrix, and four new DOM suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* pages/raw-json.js -- the Config Editor tab (templates/v3/partials/raw_json.html):
|
||||
* plain-text editors for config.json and config_secrets.json.
|
||||
*
|
||||
* The buttons carry data-action ("format", "validate", "save") and
|
||||
* data-editor ("main", "secrets"); one delegated listener on the page root
|
||||
* handles all six. Validation messages are built with textContent, so a
|
||||
* parser message that quotes the user's text stays text.
|
||||
*
|
||||
* Saves are writes: they are not cancelled when the page is swapped away,
|
||||
* so the server always finishes and the result is still reported.
|
||||
*
|
||||
* The old globals (formatJson, manualValidateJson, validateJSON,
|
||||
* saveMainConfig, saveSecretsConfig) are deprecated aliases made in
|
||||
* core/boot.js; they forward to the exports at the bottom of this file.
|
||||
*/
|
||||
|
||||
const EDITORS = {
|
||||
main: {
|
||||
editor: 'main-config-editor', validation: 'main-config-validation',
|
||||
url: '/api/v3/config/raw/main', file: 'config.json',
|
||||
},
|
||||
secrets: {
|
||||
editor: 'secrets-config-editor', validation: 'secrets-config-validation',
|
||||
url: '/api/v3/config/raw/secrets', file: 'config_secrets.json',
|
||||
},
|
||||
};
|
||||
|
||||
// The mounted page, for the deprecated aliases (boot.js).
|
||||
let active = null;
|
||||
|
||||
function el(doc, tag, className, text) {
|
||||
const node = doc.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function parseError(text) {
|
||||
try {
|
||||
JSON.parse(text);
|
||||
return null;
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
function parts(ctx, kind) {
|
||||
const spec = EDITORS[kind];
|
||||
if (!spec) return null;
|
||||
const textarea = ctx.root.querySelector('#' + spec.editor);
|
||||
const validation = ctx.root.querySelector('#' + spec.validation);
|
||||
return textarea && validation ? { spec: spec, textarea: textarea, validation: validation } : null;
|
||||
}
|
||||
|
||||
/** The short "Valid JSON" / "Invalid JSON: ..." line under an editor. True when valid. */
|
||||
export function validate(ctx, kind) {
|
||||
const p = parts(ctx, kind);
|
||||
if (!p) return true;
|
||||
const doc = ctx.root.ownerDocument;
|
||||
const error = parseError(p.textarea.value);
|
||||
const line = error
|
||||
? el(doc, 'span', 'text-red-600')
|
||||
: el(doc, 'span', 'text-green-600');
|
||||
line.append(el(doc, 'i', (error ? 'fas fa-times-circle' : 'fas fa-check-circle') + ' mr-1'),
|
||||
doc.createTextNode(error ? 'Invalid JSON: ' + error.message : 'Valid JSON'));
|
||||
p.validation.replaceChildren(line);
|
||||
return !error;
|
||||
}
|
||||
|
||||
/** The Validate button: a detailed box, and a notification. */
|
||||
export function validateDetailed(ctx, kind) {
|
||||
const p = parts(ctx, kind);
|
||||
if (!p) return false;
|
||||
const doc = ctx.root.ownerDocument;
|
||||
const error = parseError(p.textarea.value);
|
||||
const box = el(doc, 'div', error
|
||||
? 'p-3 bg-red-50 border border-red-200 rounded-md'
|
||||
: 'p-3 bg-green-50 border border-green-200 rounded-md');
|
||||
const row = el(doc, 'div', 'flex items-start');
|
||||
const text = el(doc, 'div');
|
||||
const detail = el(doc, 'div', error ? 'text-sm text-red-700 mt-1' : 'text-sm text-green-700 mt-1');
|
||||
if (error) {
|
||||
row.append(el(doc, 'i', 'fas fa-times-circle text-red-600 text-xl mr-3 mt-1'));
|
||||
text.append(el(doc, 'div', 'font-semibold text-red-800', '✗ Invalid JSON syntax'));
|
||||
detail.append(el(doc, 'strong', null, 'Error:'), doc.createTextNode(' ' + error.message));
|
||||
} else {
|
||||
row.append(el(doc, 'i', 'fas fa-check-circle text-green-600 text-xl mr-3 mt-1'));
|
||||
text.append(el(doc, 'div', 'font-semibold text-green-800', '✓ JSON is valid!'));
|
||||
['✓ Valid JSON syntax', '✓ Proper structure', '✓ No syntax errors detected'].forEach(function(line, i) {
|
||||
if (i) detail.append(el(doc, 'br'));
|
||||
detail.append(doc.createTextNode(line));
|
||||
});
|
||||
}
|
||||
text.append(detail);
|
||||
row.append(text);
|
||||
box.append(row);
|
||||
p.validation.replaceChildren(box);
|
||||
ctx.notify(error ? 'JSON validation failed: ' + error.message : 'JSON validation successful!',
|
||||
error ? 'error' : 'success');
|
||||
return !error;
|
||||
}
|
||||
|
||||
/** The Format button: re-indent valid JSON by four spaces. */
|
||||
export function format(ctx, kind) {
|
||||
const p = parts(ctx, kind);
|
||||
if (!p) return false;
|
||||
try {
|
||||
p.textarea.value = JSON.stringify(JSON.parse(p.textarea.value), null, 4);
|
||||
validate(ctx, kind);
|
||||
ctx.notify('JSON formatted successfully!', 'success');
|
||||
return true;
|
||||
} catch (error) {
|
||||
validate(ctx, kind);
|
||||
ctx.notify('Cannot format invalid JSON: ' + error.message, 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The Save button: validate, then post the parsed file. */
|
||||
export function save(ctx, kind) {
|
||||
const p = parts(ctx, kind);
|
||||
if (!p) return Promise.resolve(false);
|
||||
if (!validate(ctx, kind)) {
|
||||
ctx.notify('Invalid JSON! Please fix errors before saving.', 'error');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const config = JSON.parse(p.textarea.value);
|
||||
return ctx.api.post(p.spec.url, config).then(function() {
|
||||
ctx.notify(p.spec.file + ' saved successfully!', 'success');
|
||||
return true;
|
||||
}).catch(function(err) {
|
||||
if (err && err.loginRequired) return false;
|
||||
ctx.notify('Error saving ' + p.spec.file + ': ' + ((err && err.message) || 'An error occurred'), 'error');
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
const ACTIONS = { format: format, validate: validateDetailed, save: save };
|
||||
|
||||
export function init(root, ctx) {
|
||||
const on = { signal: ctx.signal };
|
||||
root.addEventListener('click', function(event) {
|
||||
const button = event.target.closest('button[data-action][data-editor]');
|
||||
if (!button || !root.contains(button)) return;
|
||||
const action = ACTIONS[button.dataset.action];
|
||||
if (action) action(ctx, button.dataset.editor);
|
||||
}, on);
|
||||
Object.keys(EDITORS).forEach(function(kind) {
|
||||
const p = parts(ctx, kind);
|
||||
if (!p) return;
|
||||
p.textarea.addEventListener('input', function() { validate(ctx, kind); }, on);
|
||||
validate(ctx, kind);
|
||||
});
|
||||
active = ctx;
|
||||
}
|
||||
|
||||
export function destroy(root, ctx) {
|
||||
if (active === ctx) active = null;
|
||||
}
|
||||
|
||||
// ── the old globals, kept as deprecated aliases (boot.js) ────────────────────
|
||||
// They took element ids; map those back to an editor.
|
||||
function kindOf(editorId) {
|
||||
return Object.keys(EDITORS).find(function(kind) { return EDITORS[kind].editor === editorId; });
|
||||
}
|
||||
|
||||
/** window.formatJson(editorId, validationDivId) */
|
||||
export function formatJson(editorId) {
|
||||
return active ? format(active, kindOf(editorId)) : false;
|
||||
}
|
||||
|
||||
/** window.manualValidateJson(editorId, validationDivId) */
|
||||
export function manualValidateJson(editorId) {
|
||||
return active ? validateDetailed(active, kindOf(editorId)) : false;
|
||||
}
|
||||
|
||||
/** window.validateJSON(editorId, validationDivId): true when valid. */
|
||||
export function validateJSON(editorId) {
|
||||
return active ? validate(active, kindOf(editorId)) : true;
|
||||
}
|
||||
|
||||
/** window.saveMainConfig() */
|
||||
export function saveMainConfig() {
|
||||
return active ? save(active, 'main') : Promise.resolve(false);
|
||||
}
|
||||
|
||||
/** window.saveSecretsConfig() */
|
||||
export function saveSecretsConfig() {
|
||||
return active ? save(active, 'secrets') : Promise.resolve(false);
|
||||
}
|
||||
Reference in New Issue
Block a user