group editor code display changed
Sonarqube Scanner / Prepare environment (push) Successful in 6s
Sonarqube Scanner / Build and test (push) Successful in 22s
Sonarqube Scanner / SonarQube analysis (push) Successful in 56s
Sonarqube Scanner / Deploy to Server (push) Successful in 6s

This commit is contained in:
Florian Zumpe
2026-10-18 23:03:12 +02:00
parent 04e05ecfd4
commit a66a795407
10 changed files with 109 additions and 111 deletions
+7 -5
View File
@@ -1,14 +1,16 @@
# Gemeinsam bearbeiten # Gemeinsam bearbeiten
In „Strecken bearbeiten“ öffnet der kleine Gruppenbutton zwischen Stift und In „Strecken bearbeiten“ öffnet der kleine Button mit zwei gezeichneten Wichteln
Löschen einen Dialog. Dort sind die beiden Wichtel groß zu sehen: links der Vogel, zwischen Stift und
rechts der Stein. Erst „Gemeinsamen Entwurf öffnen“ legt einen gemeinsamen Löschen den gemeinsamen Entwurf und zeigt anschließend den echten Code mit
Entwurf auf der Website an. Das Original bleibt unverändert. Bei lokalen Entwürfen fünf Wichteln in einem großen Popup an. Eine vorgeschaltete Zwei-Wichtel-Vorschau
gibt es nicht mehr. Das Original bleibt unverändert. Bei lokalen Entwürfen
fragt die App vorher, ob Weg, Stationen, Bilder und Ton übertragen werden dürfen. fragt die App vorher, ob Weg, Stationen, Bilder und Ton übertragen werden dürfen.
Ein bereits eingesandter eigener Entwurf wird dabei zuerst aktualisiert. Ein bereits eingesandter eigener Entwurf wird dabei zuerst aktualisiert.
Danach zeigt die App fünf Wichtel als Gruppencode. Andere öffnen in der Danach zeigt die App fünf Wichtel als Gruppencode. Andere öffnen in der
Streckenverwaltung „Gruppencode eingeben“ und schieben die Bilder in den fünf Streckenverwaltung „Gruppencode eingeben“. Ein eigenes breites Popup bietet
Platz für alle fünf Drehräder. Dort schieben sie die Bilder in den fünf
Drehrädern nach oben oder unten. Es zählen die Wichtel in der Mitte, von links Drehrädern nach oben oder unten. Es zählen die Wichtel in der Mitte, von links
nach rechts. Wiederholungen sind erlaubt. Pfeiltasten und die Pfeilschaltflächen nach rechts. Wiederholungen sind erlaubt. Pfeiltasten und die Pfeilschaltflächen
funktionieren ebenfalls. „Mitmachen“ öffnet den gemeinsamen Entwurf. funktionieren ebenfalls. „Mitmachen“ öffnet den gemeinsamen Entwurf.
+11
View File
@@ -92,3 +92,14 @@ Sitzungen, Rollen owner/member, Codewechsel und Widerruf. Bedienung und Einspiel
nicht vorhanden; bestehende Gruppenrechte gelten unverändert. Heartbeats sind nicht vorhanden; bestehende Gruppenrechte gelten unverändert. Heartbeats sind
POSTs mit eigenem Bearbeitungsschlüssel und dürfen weder fremde noch bereits POSTs mit eigenem Bearbeitungsschlüssel und dürfen weder fremde noch bereits
abgelaufene Sperren übernehmen. abgelaufene Sperren übernehmen.
## Code-Popups
- Gruppenbutton zeigt unmittelbar den echten fünfstelligen Einladungscode. Keine
vorgeschaltete Zwei-Wichtel-Vorschau wieder einführen; sie wurde mit dem Code
verwechselt. Eine unvollständige Codeantwort nicht anzeigen.
- Codeanzeige und Codeeingabe verwenden getrennte breite native Dialoge. Alle
fünf Drehräder nebeneinander anordnen; Wiederholungen weiterhin zulassen.
- Scrollschritt in groups.js und Optionshöhe/Innenabstand in editor.css müssen
übereinstimmen (96 Pixel). Schließen der Eingabe verwirft verspätete Antworten;
die automatische Einladungsaktualisierung darf die Eingabe nicht überdecken.
+8 -8
View File
@@ -595,24 +595,24 @@
.current-location-button img { width: 24px; height: 24px; vertical-align: middle; } .current-location-button img { width: 24px; height: 24px; vertical-align: middle; }
/* Shared drafts and vertically scrolling Wichtel-code wheels. */ /* Shared drafts and vertically scrolling Wichtel-code wheels. */
.group-share-pictures { display: flex; align-items: center; justify-content: center; gap: 1rem; margin: 1rem 0; } .group-share-button img { display: block; width: 100%; height: 100%; object-fit: contain; pointer-events: none; }
.group-share-pictures img { width: min(34vw, 9rem); height: 10rem; object-fit: contain; }
.group-dialog { width: min(35rem, calc(100% - 2rem)); max-height: 90dvh; box-sizing: border-box; overflow-y: auto; padding: 1.25rem; border: 1px solid #c8d3ca; border-radius: 1.25rem; background: #f8faf5; color: #1f3022; } .group-dialog { width: min(35rem, calc(100% - 2rem)); max-height: 90dvh; box-sizing: border-box; overflow-y: auto; padding: 1.25rem; border: 1px solid #c8d3ca; border-radius: 1.25rem; background: #f8faf5; color: #1f3022; }
.group-code-dialog { width: min(56rem, calc(100vw - 1rem)); max-height: calc(100dvh - 1rem); padding: clamp(.5rem, 2vw, 1.5rem); }
.group-dialog::backdrop { background: rgba(20, 40, 25, .5); } .group-dialog::backdrop { background: rgba(20, 40, 25, .5); }
.group-dialog-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } .group-dialog-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.group-dialog-header h2 { margin: 0; font-size: 1.2rem; } .group-dialog-header h2 { margin: 0; font-size: 1.2rem; }
.group-dialog button { min-width: 44px; min-height: 44px; border: 1px solid #c8d3ca; border-radius: .85rem; background: white; color: #24462b; touch-action: manipulation; } .group-dialog button { min-width: 44px; min-height: 44px; border: 1px solid #c8d3ca; border-radius: .85rem; background: white; color: #24462b; touch-action: manipulation; }
.group-dialog button:focus-visible, .group-wheel-viewport:focus-visible { outline: 3px solid #1e6fba; outline-offset: 2px; } .group-dialog button:focus-visible, .group-wheel-viewport:focus-visible { outline: 3px solid #1e6fba; outline-offset: 2px; }
.group-wheels { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .3rem; padding: 0; border: 0; margin: 1rem 0; } .group-wheels { display: grid; grid-template-columns: repeat(5, minmax(44px, 1fr)); gap: .3rem; padding: 0; border: 0; margin: 1rem 0; }
.group-code-fieldset { border: 0; padding: 0; min-width: 0; } .group-code-fieldset { border: 0; padding: 0; min-width: 0; }
.group-code-fieldset legend { margin-bottom: .5rem; font-weight: 600; } .group-code-fieldset legend { margin-bottom: .5rem; font-weight: 600; }
.group-wheel { display: grid; gap: .3rem; min-width: 0; } .group-wheel { display: grid; gap: .3rem; min-width: 0; }
.group-wheel-viewport { height: 156px; padding: 52px 0; box-sizing: border-box; overflow-y: auto; overscroll-behavior: contain; scroll-snap-type: y mandatory; scrollbar-width: none; border-radius: .85rem; background: linear-gradient(to bottom, #edf2eb 0 33.33%, #dcebdc 33.33% 66.66%, #edf2eb 66.66%); } .group-wheel-viewport { height: 288px; padding: 96px 0; box-sizing: border-box; overflow-y: auto; overscroll-behavior: contain; scroll-snap-type: y mandatory; scrollbar-width: none; border-radius: .85rem; background: linear-gradient(to bottom, #edf2eb 0 33.33%, #dcebdc 33.33% 66.66%, #edf2eb 66.66%); }
.group-wheel-viewport::-webkit-scrollbar { display: none; } .group-wheel-viewport::-webkit-scrollbar { display: none; }
.group-wheel-option { height: 52px; display: grid; place-items: center; scroll-snap-align: center; cursor: pointer; } .group-wheel-option { height: 96px; display: grid; place-items: center; scroll-snap-align: center; cursor: pointer; }
.group-wheel-option img { width: 100%; max-width: 48px; height: 48px; object-fit: contain; pointer-events: none; } .group-wheel-option img { width: 100%; max-width: 88px; height: 88px; object-fit: contain; pointer-events: none; }
.group-code { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .3rem; padding: .6rem; border: 1px solid #c8d3ca; border-radius: 1rem; background: #e7f1e9; } .group-code { display: grid; grid-template-columns: repeat(5, minmax(44px, 1fr)); gap: .3rem; padding: .6rem; border: 1px solid #c8d3ca; border-radius: 1rem; background: #e7f1e9; }
.group-code img { width: 100%; height: 64px; object-fit: contain; } .group-code img { width: 100%; height: clamp(64px, 14vw, 128px); object-fit: contain; }
.group-actions { max-width: min(18rem, 52vw); flex-wrap: wrap; justify-content: flex-end; } .group-actions { max-width: min(18rem, 52vw); flex-wrap: wrap; justify-content: flex-end; }
.group-actions .editor-secondary-button { font-size: .85rem; } .group-actions .editor-secondary-button { font-size: .85rem; }
.group-status { display: block; min-height: 1.5rem; color: #9b2c2c; } .group-status { display: block; min-height: 1.5rem; color: #9b2c2c; }
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" stroke="#315d3a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 23 12 6Q14 2 16 6L23 23Z" fill="#e7f1e9"/>
<path d="M7 24Q5 34 14 43 23 34 21 24" fill="#fff"/>
<circle cx="14" cy="25" r="3" fill="#f2ca6b"/>
<path d="M25 25 32 10Q34 6 36 10L43 25Z" fill="#f8f5eb"/>
<path d="M27 26Q25 35 34 43 43 35 41 26" fill="#fff"/>
<circle cx="34" cy="27" r="3" fill="#f2ca6b"/>
</svg>

After

Width:  |  Height:  |  Size: 507 B

+13 -17
View File
@@ -579,33 +579,29 @@
<script src="js/user-messages.js?v=0.12.14"></script> <script src="js/user-messages.js?v=0.12.14"></script>
<script src="js/local-routes.js?v=0.12.14"></script> <script src="js/local-routes.js?v=0.12.14"></script>
<script src="js/route-recorder.js?v=0.12.14"></script> <script src="js/route-recorder.js?v=0.12.14"></script>
<dialog id="group-share-dialog" class="group-dialog" aria-labelledby="group-share-title"> <dialog id="group-entry-dialog" class="group-dialog group-code-dialog" aria-labelledby="group-entry-title">
<header class="group-dialog-header"> <header class="group-dialog-header">
<h2 id="group-share-title" data-i18n="groups.share">Gemeinsam bearbeiten</h2> <h2 id="group-entry-title" data-i18n="groups.join-open">Gruppencode eingeben</h2>
<button id="group-share-close" type="button" data-i18n-aria-label="info.close">×</button> <button id="group-entry-close" type="button" data-i18n-aria-label="info.close">×</button>
</header>
<p id="group-share-route"></p>
<div id="group-share-pictures" class="group-share-pictures"></div>
<p data-i18n="groups.share-help">Mit diesen beiden Wichteln öffnest du einen gemeinsamen Entwurf. Andere können mit einem Wichtelcode mitmachen. Das Original bleibt erhalten.</p>
<div class="editor-form-actions">
<button id="group-share-start" type="button" data-i18n="groups.share-start">Gemeinsamen Entwurf öffnen</button>
</div>
</dialog>
<dialog id="group-dialog" class="group-dialog" aria-labelledby="group-title">
<header class="group-dialog-header">
<h2 id="group-title" data-i18n="groups.share">Gemeinsam bearbeiten</h2>
<button id="group-close" type="button" aria-label="Schließen" data-i18n-aria-label="info.close">×</button>
</header> </header>
<section id="group-entry"> <section id="group-entry">
<p data-i18n="groups.wheel-help">Schiebe die Wichtel nach oben oder unten. Die fünf Wichtel in der Mitte ergeben den Code. Lies von links nach rechts. Ein Wichtel darf mehrmals vorkommen.</p> <p data-i18n="groups.wheel-help">Schiebe die Wichtel nach oben oder unten. Die fünf Wichtel in der Mitte ergeben den Code. Lies von links nach rechts. Ein Wichtel darf mehrmals vorkommen.</p>
<fieldset class="group-code-fieldset"><legend data-i18n="groups.code-label">Gruppencode</legend><div id="group-wheels" class="group-wheels"></div></fieldset> <fieldset class="group-code-fieldset"><legend data-i18n="groups.code-label">Gruppencode</legend><div id="group-wheels" class="group-wheels"></div></fieldset>
<button id="group-join" type="button" data-i18n="groups.join">Mitmachen</button> <div class="editor-form-actions"><button id="group-join" type="button" data-i18n="groups.join">Mitmachen</button></div>
</section> </section>
<output id="group-entry-status" class="group-status"></output>
</dialog>
<dialog id="group-dialog" class="group-dialog group-code-dialog" aria-labelledby="group-title">
<header class="group-dialog-header">
<h2 id="group-title" data-i18n="groups.code-label">Gruppencode</h2>
<button id="group-close" type="button" aria-label="Schließen" data-i18n-aria-label="info.close">×</button>
</header>
<p id="group-route"></p>
<section id="group-invitation" hidden> <section id="group-invitation" hidden>
<p data-i18n="groups.invite-help">Zeige diesen Code den anderen. Damit können sie deinen gemeinsamen Entwurf öffnen und bearbeiten. Das Original bleibt erhalten.</p> <p data-i18n="groups.invite-help">Zeige diesen Code den anderen. Damit können sie deinen gemeinsamen Entwurf öffnen und bearbeiten. Das Original bleibt erhalten.</p>
<div id="group-code" class="group-code" aria-label="Gruppencode" data-i18n-aria-label="groups.code-label"></div> <div id="group-code" class="group-code" aria-label="Gruppencode" data-i18n-aria-label="groups.code-label"></div>
<p id="group-expiry"></p> <p id="group-expiry"></p>
<button id="group-edit" type="button" data-i18n="groups.edit">Entwurf bearbeiten</button> <div class="editor-form-actions"><button id="group-edit" type="button" data-i18n="groups.edit">Entwurf bearbeiten</button></div>
</section> </section>
<output id="group-status" class="group-status"></output> <output id="group-status" class="group-status"></output>
</dialog> </dialog>
-1
View File
@@ -1652,7 +1652,6 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (shareInProgress) return; if (shareInProgress) return;
shareInProgress = true; shareInProgress = true;
try { try {
if (ns.Groups.confirmShare && !await ns.Groups.confirmShare(routeName(route))) return;
let sourceId = routeId(route); let sourceId = routeId(route);
if (routeSource(route) === 'local') { if (routeSource(route) === 'local') {
if (!globalThis.confirm(ns.I18n.t('groups.upload-confirm'))) return; if (!globalThis.confirm(ns.I18n.t('groups.upload-confirm'))) return;
+28 -43
View File
@@ -6,10 +6,11 @@ globalThis.Wegwichtel ??= {};
joinOpen: 'group-join-open', editorJoin: 'editor-group-join', list: 'editor-group-list', joinOpen: 'group-join-open', editorJoin: 'editor-group-join', list: 'editor-group-list',
dialog: 'group-dialog', close: 'group-close', status: 'group-status', wheels: 'group-wheels', dialog: 'group-dialog', close: 'group-close', status: 'group-status', wheels: 'group-wheels',
entry: 'group-entry', invitation: 'group-invitation', code: 'group-code', join: 'group-join', entry: 'group-entry', invitation: 'group-invitation', code: 'group-code', join: 'group-join',
edit: 'group-edit', expiry: 'group-expiry' edit: 'group-edit', expiry: 'group-expiry',
entryDialog: 'group-entry-dialog', entryClose: 'group-entry-close', entryStatus: 'group-entry-status'
}; };
const codeLength = 5; const codeLength = 5;
const wheelStep = 52; const wheelStep = 96;
const revisions = new Map(); const revisions = new Map();
let selection = []; let selection = [];
let currentGroup = null; let currentGroup = null;
@@ -73,12 +74,12 @@ globalThis.Wegwichtel ??= {};
async function guarded(action) { async function guarded(action) {
if (pending) return; if (pending) return;
pending = true; pending = true;
element(ids.status).textContent = ''; const status = element(element(ids.entryDialog).open ? ids.entryStatus : ids.status);
status.textContent = '';
element(ids.join).disabled = true; element(ids.join).disabled = true;
try { await action(); } catch (error) { try { await action(); } catch (error) {
element(ids.status).textContent = errorMessage(error); element(element(ids.entryDialog).open ? ids.entryStatus : ids.status).textContent = errorMessage(error);
if (!element(ids.dialog).open) { if (!element(ids.dialog).open && !element(ids.entryDialog).open) {
element(ids.entry).hidden = true;
element(ids.invitation).hidden = true; element(ids.invitation).hidden = true;
element(ids.dialog).showModal(); element(ids.dialog).showModal();
} }
@@ -146,21 +147,22 @@ globalThis.Wegwichtel ??= {};
} }
function openEntry() { function openEntry() {
if (element(ids.dialog).open) element(ids.dialog).close();
dialogGeneration += 1; dialogGeneration += 1;
selection = new Array(codeLength).fill(0); selection = new Array(codeLength).fill(0);
element(ids.status).textContent = ''; element(ids.entryStatus).textContent = '';
element(ids.entry).hidden = false;
element(ids.invitation).hidden = true;
const wheels = element(ids.wheels); const wheels = element(ids.wheels);
wheels.replaceChildren(...selection.map((_, index) => makeWheel(index))); wheels.replaceChildren(...selection.map((_, index) => makeWheel(index)));
if (!element(ids.dialog).open) element(ids.dialog).showModal(); if (!element(ids.entryDialog).open) element(ids.entryDialog).showModal();
} }
function showInvitation(payload) { function showInvitation(payload) {
if (!Array.isArray(payload.code) || payload.code.length !== codeLength) throw new Error(t('error.connection'));
currentGroup = payload.group; currentGroup = payload.group;
element(ids.status).textContent = ''; element(ids.status).textContent = '';
element(ids.entry).hidden = true; if (element(ids.entryDialog).open) element(ids.entryDialog).close();
element(ids.invitation).hidden = false; element(ids.invitation).hidden = false;
element('group-route').textContent = payload.group.route.name || '';
const motifs = payload.code.map(value => ns.GroupSymbols.get(value)); const motifs = payload.code.map(value => ns.GroupSymbols.get(value));
element(ids.code).replaceChildren(...motifs.map(picture)); element(ids.code).replaceChildren(...motifs.map(picture));
const time = new Date(payload.expiresAt * 1000).toLocaleTimeString(ns.I18n.language, { hour: '2-digit', minute: '2-digit' }); const time = new Date(payload.expiresAt * 1000).toLocaleTimeString(ns.I18n.language, { hour: '2-digit', minute: '2-digit' });
@@ -182,7 +184,8 @@ globalThis.Wegwichtel ??= {};
finally { invitationPolling = false; } finally { invitationPolling = false; }
} }
async function edit(group) { async function edit(group) {
element(ids.dialog).close(); if (element(ids.dialog).open) element(ids.dialog).close();
if (element(ids.entryDialog).open) element(ids.entryDialog).close();
await ns.EditorUi.openGroup(group.route); await ns.EditorUi.openGroup(group.route);
} }
@@ -258,34 +261,20 @@ globalThis.Wegwichtel ??= {};
function shareButton(action) { function shareButton(action) {
const result = document.createElement('button'); const result = document.createElement('button');
result.type = 'button'; result.type = 'button';
result.className = 'editor-icon-button'; result.className = 'editor-icon-button group-share-button';
result.dataset.editorAction = action; result.dataset.editorAction = action;
result.title = t('share'); result.title = t('share');
result.setAttribute('aria-label', t('share')); result.setAttribute('aria-label', t('share'));
result.setAttribute('aria-haspopup', 'dialog'); result.setAttribute('aria-haspopup', 'dialog');
result.textContent = '👥'; const icon = document.createElement('img');
icon.src = '/images/icons/group-edit.svg';
icon.alt = '';
icon.width = 32;
icon.height = 32;
result.appendChild(icon);
return result; return result;
} }
let shareDecision = null;
function confirmShare(name) {
if (shareDecision) return Promise.resolve(false);
const dialog = element('group-share-dialog');
element('group-share-route').textContent = name;
element('group-share-pictures').replaceChildren(...['🐦', '🪨'].map(value => picture(ns.GroupSymbols.get(value))));
return new Promise(resolve => {
shareDecision = resolve;
dialog.showModal();
});
}
function finishShare(accepted) {
const resolve = shareDecision;
shareDecision = null;
element('group-share-dialog').close();
resolve?.(accepted);
}
function headersFor(path) { function headersFor(path) {
const id = /^\/routes\/(\d+)(?:\/|$)/.exec(path)?.[1]; const id = /^\/routes\/(\d+)(?:\/|$)/.exec(path)?.[1];
return id && revisions.has(id) ? { 'If-Match': String(revisions.get(id)) } : {}; return id && revisions.has(id) ? { 'If-Match': String(revisions.get(id)) } : {};
@@ -306,31 +295,27 @@ globalThis.Wegwichtel ??= {};
code: selection.map(index => motifs[index].unicode), creator_id: ns.LocalRoutes.creatorId() code: selection.map(index => motifs[index].unicode), creator_id: ns.LocalRoutes.creatorId()
}); });
} catch (error) { } catch (error) {
if (!element(ids.dialog).open || generation !== dialogGeneration) return; if (!element(ids.entryDialog).open || generation !== dialogGeneration) return;
throw error; throw error;
} }
if (!element(ids.dialog).open || generation !== dialogGeneration) return; if (!element(ids.entryDialog).open || generation !== dialogGeneration) return;
await edit(payload.group); await edit(payload.group);
await refresh(); await refresh();
} }
function init() { function init() {
globalThis.setInterval?.(() => { refreshInvitation(); }, 30_000); globalThis.setInterval?.(() => { refreshInvitation(); }, 30_000);
activate(element('group-share-start'), () => finishShare(true));
activate(element('group-share-close'), () => finishShare(false));
element('group-share-dialog').addEventListener('close', () => {
const resolve = shareDecision; shareDecision = null; resolve?.(false);
});
document.addEventListener('wegwichtel:point-drafts-changed', () => { renderPending(); ns.StorageUsage?.refresh(); }); document.addEventListener('wegwichtel:point-drafts-changed', () => { renderPending(); ns.StorageUsage?.refresh(); });
element(ids.dialog).addEventListener('cancel', () => { dialogGeneration += 1; }); element(ids.entryDialog).addEventListener('cancel', () => { dialogGeneration += 1; });
element(ids.dialog).addEventListener('close', () => { dialogGeneration += 1; }); element(ids.entryDialog).addEventListener('close', () => { dialogGeneration += 1; });
activate(element(ids.entryClose), () => element(ids.entryDialog).close());
for (const id of [ids.joinOpen, ids.editorJoin]) activate(element(id), openEntry); for (const id of [ids.joinOpen, ids.editorJoin]) activate(element(id), openEntry);
activate(element(ids.close), () => element(ids.dialog).close()); activate(element(ids.close), () => element(ids.dialog).close());
activate(element(ids.join), () => guarded(joinSelectedGroup)); activate(element(ids.join), () => guarded(joinSelectedGroup));
activate(element(ids.edit), () => guarded(() => edit(currentGroup))); activate(element(ids.edit), () => guarded(() => edit(currentGroup)));
} }
ns.Groups = Object.freeze({ share, shareButton, confirmShare, refresh, refreshInvitation, headersFor, observe }); ns.Groups = Object.freeze({ share, shareButton, refresh, refreshInvitation, headersFor, observe });
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true }); if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true });
else init(); else init();
}(globalThis.Wegwichtel)); }(globalThis.Wegwichtel));
-2
View File
@@ -135,8 +135,6 @@ globalThis.Wegwichtel ??= {};
}, },
"groups": { "groups": {
"code-wait": "In der Gruppe wird gerade eine Station bearbeitet. Ein neuer Code erscheint, sobald die Stationen wieder frei sind. Ein abgelaufener Code erlaubt keinen neuen Beitritt.", "code-wait": "In der Gruppe wird gerade eine Station bearbeitet. Ein neuer Code erscheint, sobald die Stationen wieder frei sind. Ein abgelaufener Code erlaubt keinen neuen Beitritt.",
"share-help": "Mit diesen beiden Wichteln öffnest du einen gemeinsamen Entwurf. Andere können mit einem Wichtelcode mitmachen. Das Original bleibt erhalten.",
"share-start": "Gemeinsamen Entwurf öffnen",
"title": "Gemeinsame Entwürfe", "title": "Gemeinsame Entwürfe",
"upload-confirm": "Dein Entwurf mit Bildern und Ton wird dafür an die Website gesendet und als gemeinsamer Entwurf kopiert. Fortfahren?", "upload-confirm": "Dein Entwurf mit Bildern und Ton wird dafür an die Website gesendet und als gemeinsamer Entwurf kopiert. Fortfahren?",
"share": "Gemeinsam bearbeiten", "share": "Gemeinsam bearbeiten",
-2
View File
@@ -135,8 +135,6 @@ globalThis.Wegwichtel ??= {};
}, },
"groups": { "groups": {
"code-wait": "Someone in the group is editing a stop. A new code will appear once the stops are free. An expired code cannot be used to join.", "code-wait": "Someone in the group is editing a stop. A new code will appear once the stops are free. An expired code cannot be used to join.",
"share-help": "These two gnomes open a shared draft. Others can join using a gnome code. The original stays unchanged.",
"share-start": "Open a shared draft",
"title": "Shared drafts", "title": "Shared drafts",
"upload-confirm": "Your draft, including pictures and recordings, will be sent to the website and copied into a shared draft. Continue?", "upload-confirm": "Your draft, including pictures and recordings, will be sent to the website and copied into a shared draft. Continue?",
"share": "Edit together", "share": "Edit together",
+34 -33
View File
@@ -27,7 +27,7 @@ function key(node, value) {
} }
async function harness(loading = false) { async function harness(loading = false) {
const ids = ['group-join-open', 'editor-group-join', 'editor-group-list', 'group-dialog', 'group-close', const ids = ['group-join-open', 'editor-group-join', 'editor-group-list', 'group-dialog', 'group-close',
'group-share-dialog', 'group-share-route', 'group-share-pictures', 'group-share-start', 'group-share-close', 'group-entry-dialog', 'group-entry-close', 'group-entry-status', 'group-route',
'group-status', 'group-wheels', 'group-entry', 'group-invitation', 'group-code', 'group-join', 'group-edit', 'group-expiry']; 'group-status', 'group-wheels', 'group-entry', 'group-invitation', 'group-code', 'group-join', 'group-edit', 'group-expiry'];
const nodes = new Map(ids.map(id => [id, new Element()])); const nodes = new Map(ids.map(id => [id, new Element()]));
const document = new EventTarget(); const document = new EventTarget();
@@ -66,9 +66,9 @@ test('group code wheels support arrows, keyboard wrapping, scrolling and joining
const h = await harness(true); const h = await harness(true);
h.context.document.dispatchEvent(new Event('DOMContentLoaded')); h.context.document.dispatchEvent(new Event('DOMContentLoaded'));
pointer(h.nodes.get('group-join-open'), { isPrimary: false }); pointer(h.nodes.get('group-join-open'), { isPrimary: false });
assert.equal(h.nodes.get('group-dialog').open, false); assert.equal(h.nodes.get('group-entry-dialog').open, false);
pointer(h.nodes.get('group-join-open')); pointer(h.nodes.get('group-join-open'));
assert.equal(h.nodes.get('group-dialog').open, true); assert.equal(h.nodes.get('group-entry-dialog').open, true);
const columns = h.nodes.get('group-wheels').children; const columns = h.nodes.get('group-wheels').children;
assert.equal(columns.length, 5); assert.equal(columns.length, 5);
const [up, viewport, down] = columns[0].children; const [up, viewport, down] = columns[0].children;
@@ -82,7 +82,7 @@ test('group code wheels support arrows, keyboard wrapping, scrolling and joining
key(viewport, 'Home'); key(viewport, 'ArrowDown'); key(viewport, 'Home'); key(viewport, 'ArrowDown');
pointer(viewport.children[5]); pointer(viewport.children[5]);
assert.equal(viewport.attributes.get('aria-valuetext'), 'Vogel'); assert.equal(viewport.attributes.get('aria-valuetext'), 'Vogel');
viewport.scrollTop = 3 * 52; viewport.dispatchEvent(new Event('scroll')); viewport.scrollTop = 3 * 96; viewport.dispatchEvent(new Event('scroll'));
key(h.nodes.get('group-join'), ' '); key(h.nodes.get('group-join'), ' ');
await tick(); await tick();
const joined = h.calls.find(call => call.url.endsWith('/join')); const joined = h.calls.find(call => call.url.endsWith('/join'));
@@ -91,10 +91,10 @@ test('group code wheels support arrows, keyboard wrapping, scrolling and joining
assert.equal(body.code[0], '🌼'); assert.equal(body.code[0], '🌼');
assert.equal(body.creator_id, 'a'.repeat(64)); assert.equal(body.creator_id, 'a'.repeat(64));
assert.deepEqual(h.edits, [22]); assert.deepEqual(h.edits, [22]);
assert.equal(h.nodes.get('group-dialog').open, false); assert.equal(h.nodes.get('group-entry-dialog').open, false);
pointer(h.nodes.get('editor-group-join')); pointer(h.nodes.get('editor-group-join'));
pointer(h.nodes.get('group-close')); pointer(h.nodes.get('group-entry-close'));
assert.equal(h.nodes.get('group-dialog').open, false); assert.equal(h.nodes.get('group-entry-dialog').open, false);
}); });
test('sharing displays five motifs and the owner can renew or close access with confirmation', async () => { test('sharing displays five motifs and the owner can renew or close access with confirmation', async () => {
@@ -102,11 +102,14 @@ test('sharing displays five motifs and the owner can renew or close access with
await h.groups.share(7); await h.groups.share(7);
assert.equal(JSON.parse(h.calls[0].body).routeId, 7); assert.equal(JSON.parse(h.calls[0].body).routeId, 7);
assert.equal(h.nodes.get('group-code').children.length, 5); assert.equal(h.nodes.get('group-code').children.length, 5);
assert.equal(h.nodes.get('group-entry').hidden, true); assert.equal(h.nodes.get('group-entry-dialog').open, false);
assert.equal(h.nodes.get('group-dialog').open, true);
assert.match(h.nodes.get('group-expiry').textContent, /Code gilt/); assert.match(h.nodes.get('group-expiry').textContent, /Code gilt/);
const shareButton = h.groups.shareButton('share-route'); const shareButton = h.groups.shareButton('share-route');
assert.equal(shareButton.dataset.editorAction, 'share-route'); assert.equal(shareButton.dataset.editorAction, 'share-route');
assert.equal(shareButton.textContent, '👥'); assert.equal(shareButton.children.length, 1);
assert.equal(shareButton.children[0].src, '/images/icons/group-edit.svg');
assert.equal(shareButton.children[0].alt, '');
assert.equal(shareButton.attributes.get('aria-haspopup'), 'dialog'); assert.equal(shareButton.attributes.get('aria-haspopup'), 'dialog');
pointer(h.nodes.get('group-edit')); await tick(); pointer(h.nodes.get('group-edit')); await tick();
assert.deepEqual(h.edits, [22]); assert.deepEqual(h.edits, [22]);
@@ -140,13 +143,13 @@ test('code failures are localized and group list refresh never advances an editi
for (const [status, text] of [[404, /Code stimmt/], [401, /nicht geöffnet/], [403, /nicht geöffnet/], [409, /inzwischen geändert/], [429, /eine Minute/], [500, /Internetverbindung/]]) { for (const [status, text] of [[404, /Code stimmt/], [401, /nicht geöffnet/], [403, /nicht geöffnet/], [409, /inzwischen geändert/], [429, /eine Minute/], [500, /Internetverbindung/]]) {
h.controls.error = status; h.controls.error = status;
pointer(h.nodes.get('group-join')); await tick(); pointer(h.nodes.get('group-join')); await tick();
assert.match(h.nodes.get('group-status').textContent, text); assert.match(h.nodes.get('group-entry-status').textContent, text);
assert.equal(h.nodes.get('group-join').disabled, false); assert.equal(h.nodes.get('group-join').disabled, false);
} }
h.controls.error = 0; h.controls.offline = true; h.controls.error = 0; h.controls.offline = true;
h.context.Wegwichtel.I18n.setLanguage('en', null); h.context.Wegwichtel.I18n.setLanguage('en', null);
pointer(h.nodes.get('group-join')); await tick(); pointer(h.nodes.get('group-join')); await tick();
assert.match(h.nodes.get('group-status').textContent, /internet connection/); assert.match(h.nodes.get('group-entry-status').textContent, /internet connection/);
await h.groups.refresh(); await h.groups.refresh();
}); });
@@ -161,10 +164,10 @@ test('a late join response does not reopen a cancelled dialog or editor', async
pointer(h.nodes.get('group-join-open')); pointer(h.nodes.get('group-join-open'));
pointer(h.nodes.get('group-join')); pointer(h.nodes.get('group-join'));
await tick(); await tick();
pointer(h.nodes.get('group-close')); pointer(h.nodes.get('group-entry-close'));
release(); release();
await tick(); await tick();
assert.equal(h.nodes.get('group-dialog').open, false); assert.equal(h.nodes.get('group-entry-dialog').open, false);
assert.deepEqual(h.edits, []); assert.deepEqual(h.edits, []);
}); });
@@ -189,29 +192,27 @@ test('unsent station changes are listed before network access and discarded only
assert.equal(list.children.length, 0); assert.equal(list.children.length, 0);
}); });
test('compact sharing button opens a large gnome preview and requires confirmation before starting', async () => { test('sharing opens the actual five-symbol invitation directly without a two-symbol preview', async () => {
const h = await harness(); const h = await harness();
const decision = h.groups.confirmShare('Blumenweg'); await h.groups.share(7);
const dialog = h.nodes.get('group-share-dialog'); assert.equal(h.nodes.get('group-dialog').open, true);
assert.equal(dialog.open, true); assert.equal(h.nodes.get('group-entry-dialog').open, false);
assert.equal(h.nodes.get('group-share-route').textContent, 'Blumenweg'); assert.equal(h.nodes.get('group-code').children.length, 5);
const pictures = h.nodes.get('group-share-pictures').children; assert.equal(h.nodes.get('group-route').textContent, 'Waldweg');
assert.equal(pictures.length, 2); pointer(h.nodes.get('group-close'));
assert.match(pictures[0].src, /🐦/); pointer(h.nodes.get('editor-group-join'));
assert.match(pictures[1].src, /🪨/); assert.equal(h.nodes.get('group-entry-dialog').open, true);
assert.equal(h.calls.length, 0); assert.equal(h.nodes.get('group-dialog').open, false);
assert.equal(await h.groups.confirmShare('Zweiter Weg'), false); assert.equal(h.nodes.get('group-wheels').children.length, 5);
pointer(h.nodes.get('group-share-close'));
assert.equal(await decision, false);
const accepted = h.groups.confirmShare('Blumenweg');
key(h.nodes.get('group-share-start'), 'Enter');
assert.equal(await accepted, true);
assert.equal(dialog.open, false);
const dismissed = h.groups.confirmShare('Blumenweg');
dialog.close();
assert.equal(await dismissed, false);
}); });
test('an incomplete invitation is never displayed as a two-symbol group code', async () => {
const h = await harness();
h.context.fetch = async () => ({ ok: true, json: async () => ({ group: { route: { id: 7 } }, code: ['🐦', '🪨'] }) });
await assert.rejects(h.groups.share(7));
assert.equal(h.nodes.get('group-dialog').open, false);
assert.equal(h.nodes.get('group-code').children.length, 0);
});
test('the open invitation refreshes its code automatically and shows a deferred renewal during station editing', async () => { test('the open invitation refreshes its code automatically and shows a deferred renewal during station editing', async () => {
const h = await harness(); const h = await harness();