Tasks, eigene Typen und Metadaten pro Editor-Aufruf laden und an den Aufrufer zurückgeben. MongoDB-Persistenz entfernen und Header auf Rückgängig, Abbrechen und Fertig umstellen. Python-Testserver und Regressionstests ergänzen; Verbindungslinien nach Import und Layoutänderungen neu berechnen.
204 lines
9.9 KiB
HTML
204 lines
9.9 KiB
HTML
<!doctype html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>TaskList Editor · Testaufrufer</title>
|
|
<style>
|
|
:root { color-scheme: light; font-family: system-ui, sans-serif; color: #172033; background: #f3f5f9; }
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; }
|
|
main { max-width: 1100px; padding: 32px 24px; margin: auto; }
|
|
h1 { font-size: 27px; margin: 0 0 10px; }
|
|
p { line-height: 1.5; }
|
|
.muted { color: #566278; }
|
|
section { padding: 24px; margin-top: 24px; border: 1px solid #dfe5ef; border-radius: 12px; background: white; }
|
|
label { display: block; font-weight: 600; margin-bottom: 10px; }
|
|
textarea { display: block; width: 100%; min-height: 300px; padding: 16px; border: 1px solid #bac5d6; border-radius: 8px; font: 14px/1.5 ui-monospace, monospace; resize: vertical; }
|
|
button, .file-label { font: inherit; cursor: pointer; border: 1px solid #bac5d6; border-radius: 6px; padding: 10px 16px; color: #172033; background: #fff; margin: 0; }
|
|
button.primary { background: #2563eb; color: white; border-color: #2563eb; }
|
|
button:disabled { cursor: wait; opacity: .55; }
|
|
input[type=file] { max-width: 100%; font: inherit; }
|
|
.actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-top: 16px; }
|
|
#status { min-height: 24px; margin-top: 20px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
#status.error { color: #b42318; }
|
|
#result { background: #f4f6fa; padding: 16px; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 420px; overflow-y: auto; }
|
|
#editor-panel { position: fixed; inset: 0; background: #fff; z-index: 5; display: flex; flex-direction: column; }
|
|
#editor-panel[hidden], [hidden] { display: none !important; }
|
|
#editor-header { padding: 10px 20px; display: flex; flex-wrap: wrap; gap: 12px; align-items: center; border-bottom: 1px solid #dfe5ef; background: #f3f5f9; }
|
|
#editor-status { flex: 1; margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
iframe { width: 100%; border: 0; flex: 1; min-height: 0; }
|
|
code { font: .93em ui-monospace, monospace; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>TaskList Editor testen</h1>
|
|
<p class="muted">Tasks, eigene Typen und Metadaten übergeben, im Editor bearbeiten und das Ergebnis in der Python-Konsole lesen.</p>
|
|
<p class="muted" id="editor-url">Verbindung wird vorbereitet …</p>
|
|
<section>
|
|
<label for="payload">JSON für den Editor</label>
|
|
<textarea id="payload" spellcheck="false" aria-describedby="payload-help"></textarea>
|
|
<p class="muted" id="payload-help">Ein <code>tasks</code>-Array oder ein Objekt mit <code>name</code>, <code>tasks</code>, dem vollständigen Katalog eigener Typen in <code>customTypes</code> und beliebigen <code>metadata</code>. Für eine neue Liste: <code>"tasks": []</code>. Nach „Fertig“ wird das Eingabefeld mit den zurückgegebenen Tasks und Typen aktualisiert.</p>
|
|
<div class="actions">
|
|
<button class="primary" id="open" disabled>Im Editor öffnen</button>
|
|
<button id="new" disabled>Neue Liste</button>
|
|
<input id="file" type="file" accept=".json,application/json" aria-label="JSON-Datei laden" disabled>
|
|
</div>
|
|
<p id="status" role="status" aria-live="polite"></p>
|
|
</section>
|
|
<section id="result-section" hidden>
|
|
<h2>Empfangene Rückgabe</h2>
|
|
<pre id="result"></pre>
|
|
</section>
|
|
</main>
|
|
<div id="editor-panel" hidden>
|
|
<div id="editor-header">
|
|
<p id="editor-status" role="status" aria-live="polite">Mit „Fertig“ übernehmen oder mit „Abbrechen“ ohne Änderungen zurückkehren.</p>
|
|
<button id="retry" hidden>Rückgabe erneut senden</button>
|
|
</div>
|
|
<iframe id="editor" title="TaskList Editor"></iframe>
|
|
</div>
|
|
<script>
|
|
'use strict';
|
|
const payloadInput = document.querySelector('#payload');
|
|
const status = document.querySelector('#status');
|
|
const openButton = document.querySelector('#open');
|
|
const newButton = document.querySelector('#new');
|
|
const fileInput = document.querySelector('#file');
|
|
const panel = document.querySelector('#editor-panel');
|
|
const frame = document.querySelector('#editor');
|
|
const editorStatus = document.querySelector('#editor-status');
|
|
const retryButton = document.querySelector('#retry');
|
|
let config;
|
|
let activeToken = null;
|
|
let pendingResult = null;
|
|
let submitting = false;
|
|
|
|
function setStatus(message, error = false) {
|
|
status.textContent = message;
|
|
status.classList.toggle('error', error);
|
|
}
|
|
|
|
async function request(path, value) {
|
|
let response;
|
|
try {
|
|
response = await fetch(path, value === undefined ? {} : {
|
|
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(value)
|
|
});
|
|
} catch (error) {
|
|
throw new Error(`Verbindung zum Python-Testserver unter ${window.location.origin} fehlgeschlagen. ` +
|
|
'Starte ihn mit „python3 tools/editor_test_server.py“ und lasse das Terminal geöffnet. ' +
|
|
'Versuche es anschließend erneut.');
|
|
}
|
|
const result = await response.json();
|
|
if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
|
|
return result;
|
|
}
|
|
|
|
openButton.addEventListener('click', async () => {
|
|
openButton.disabled = true;
|
|
setStatus('JSON wird an den Editor übergeben …');
|
|
try {
|
|
const payload = JSON.parse(payloadInput.value);
|
|
const result = await request('/api/open', payload);
|
|
if (new URL(result.deepLink).origin !== config.editorOrigin) {
|
|
throw new Error('Der Editor hat einen unerwarteten Origin zurückgegeben.');
|
|
}
|
|
activeToken = result.token;
|
|
pendingResult = null;
|
|
retryButton.hidden = true;
|
|
editorStatus.textContent = 'Mit „Fertig“ übernehmen oder mit „Abbrechen“ ohne Änderungen zurückkehren.';
|
|
frame.src = result.deepLink;
|
|
panel.hidden = false;
|
|
setStatus('Editor geöffnet.');
|
|
} catch (error) {
|
|
setStatus(error.message, true);
|
|
} finally {
|
|
openButton.disabled = false;
|
|
}
|
|
});
|
|
|
|
newButton.addEventListener('click', () => {
|
|
try {
|
|
const previous = JSON.parse(payloadInput.value);
|
|
const customTypes = previous?.customTypes ?? [];
|
|
payloadInput.value = JSON.stringify({name: 'Neue Testliste', tasks: [], customTypes,
|
|
metadata: {source: 'python-testserver'}}, null, 2);
|
|
setStatus('Neue leere Liste mit den vorhandenen eigenen Typen vorbereitet. Mit „Im Editor öffnen“ beginnen.');
|
|
} catch (error) {
|
|
setStatus('Die Eingabe enthält ungültiges JSON. Bitte korrigieren, damit eigene Typen übernommen werden können.', true);
|
|
}
|
|
});
|
|
|
|
fileInput.addEventListener('change', async () => {
|
|
const file = fileInput.files[0];
|
|
if (!file) return;
|
|
try {
|
|
if (file.size > 2 * 1024 * 1024) throw new Error('Die JSON-Datei darf höchstens 2 MiB groß sein.');
|
|
const value = JSON.parse((await file.text()).replace(/^\uFEFF/, ''));
|
|
payloadInput.value = JSON.stringify(value, null, 2);
|
|
setStatus(`Datei „${file.name}“ geladen.`);
|
|
} catch (error) {
|
|
setStatus(error.message, true);
|
|
} finally {
|
|
fileInput.value = '';
|
|
}
|
|
});
|
|
|
|
async function submitResult() {
|
|
if (submitting || !pendingResult) return;
|
|
submitting = true;
|
|
retryButton.disabled = true;
|
|
editorStatus.textContent = 'Rückgabe wird an die Python-Konsole gesendet …';
|
|
try {
|
|
await request('/api/result', pendingResult);
|
|
document.querySelector('#result').textContent = JSON.stringify(pendingResult, null, 2);
|
|
document.querySelector('#result-section').hidden = false;
|
|
if (pendingResult.status === 'completed') {
|
|
const payload = {tasks: pendingResult.tasks, customTypes: pendingResult.customTypes};
|
|
for (const key of ['name', 'metadata']) {
|
|
if (Object.hasOwn(pendingResult, key)) payload[key] = pendingResult[key];
|
|
}
|
|
payloadInput.value = JSON.stringify(payload, null, 2);
|
|
}
|
|
setStatus(pendingResult.status === 'completed'
|
|
? 'Bearbeitung abgeschlossen. Tasks und eigene Typen wurden für den nächsten Aufruf übernommen und in der Python-Konsole ausgegeben.'
|
|
: 'Bearbeitung abgebrochen. Die Abbruchmeldung wurde in der Python-Konsole ausgegeben.');
|
|
activeToken = null;
|
|
pendingResult = null;
|
|
panel.hidden = true;
|
|
frame.src = 'about:blank';
|
|
openButton.focus();
|
|
} catch (error) {
|
|
editorStatus.textContent = `Rückgabe noch nicht angekommen: ${error.message}`;
|
|
retryButton.hidden = false;
|
|
} finally {
|
|
submitting = false;
|
|
retryButton.disabled = false;
|
|
}
|
|
}
|
|
|
|
window.addEventListener('message', event => {
|
|
if (!config || !activeToken || event.origin !== config.editorOrigin || event.source !== frame.contentWindow) return;
|
|
const result = event.data;
|
|
if (!result || typeof result !== 'object' || result.type !== 'tasklist-editor:result' || result.token !== activeToken) return;
|
|
if (!['completed', 'cancelled'].includes(result.status)) return;
|
|
if (result.status === 'completed' && (!Array.isArray(result.tasks) || !Array.isArray(result.customTypes))) return;
|
|
if (result.status === 'cancelled' && (Object.hasOwn(result, 'tasks') || Object.hasOwn(result, 'customTypes'))) return;
|
|
if (submitting) return;
|
|
pendingResult = result;
|
|
submitResult();
|
|
});
|
|
retryButton.addEventListener('click', submitResult);
|
|
|
|
request('/api/config').then(value => {
|
|
config = value;
|
|
payloadInput.value = JSON.stringify(config.initialPayload, null, 2);
|
|
document.querySelector('#editor-url').textContent = 'Editor: ' + config.editorUrl;
|
|
openButton.disabled = newButton.disabled = fileInput.disabled = false;
|
|
}).catch(error => setStatus(error.message, true));
|
|
</script>
|
|
</body>
|
|
</html>
|