fix tests and change invitation mechanics
Sonarqube Scanner / Prepare environment (push) Successful in 8s
Sonarqube Scanner / Build and test (push) Successful in 24s
Sonarqube Scanner / SonarQube analysis (push) Failing after 1m2s
Sonarqube Scanner / Deploy to Server (push) Successful in 6s

This commit is contained in:
Florian Zumpe
2026-10-19 17:21:50 +02:00
parent a66a795407
commit cacc63be66
13 changed files with 349 additions and 146 deletions
+3 -1
View File
@@ -1652,6 +1652,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (shareInProgress) return;
shareInProgress = true;
try {
const code = await ns.Groups.chooseCode();
if (!code) return;
let sourceId = routeId(route);
if (routeSource(route) === 'local') {
if (!globalThis.confirm(ns.I18n.t('groups.upload-confirm'))) return;
@@ -1659,7 +1661,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
await uploadLocalRoute();
sourceId = state.currentRoute.serverRouteId;
}
await ns.Groups.share(sourceId);
await ns.Groups.share(sourceId, code);
} finally { shareInProgress = false; }
return;
}
+78 -21
View File
@@ -13,7 +13,9 @@ globalThis.Wegwichtel ??= {};
const wheelStep = 96;
const revisions = new Map();
let selection = [];
let codeChoice = null;
let currentGroup = null;
let currentExpiry = 0;
let pending = false;
let dialogGeneration = 0;
const element = id => document.getElementById(id);
@@ -42,6 +44,8 @@ globalThis.Wegwichtel ??= {};
}
function errorMessage(error) {
if (error.reason === 'code-taken') return t('error.code-taken');
if (error.reason === 'invalid-code') return t('error.code');
if (error.reason === 'group-limit') return t('error.limit');
if (error.reason === 'group-full') return t('error.full');
if (error.reason === 'group-editing') return t('code-wait');
@@ -141,33 +145,68 @@ globalThis.Wegwichtel ??= {};
event.preventDefault(); choose(event.key === 'Home' ? 0 : motifs.length - 1);
}
});
paint(0);
paint(selection[position]);
column.append(up, viewport, down);
return column;
}
function openEntry() {
function openEntry(chooseCode = false) {
if (element(ids.dialog).open) element(ids.dialog).close();
dialogGeneration += 1;
selection = new Array(codeLength).fill(0);
selection = Array.from({ length: codeLength }, () => chooseCode ? randomIndex(ns.GroupSymbols.list().length) : 0);
element('group-entry-title').setAttribute('data-i18n', `groups.${chooseCode ? 'choose-code' : 'join-open'}`);
element('group-entry-title').textContent = t(chooseCode ? 'choose-code' : 'join-open');
element(ids.join).setAttribute('data-i18n', `groups.${chooseCode ? 'save-code' : 'join'}`);
element(ids.join).disabled = false;
element(ids.join).textContent = t(chooseCode ? 'save-code' : 'join');
element(ids.entryStatus).textContent = '';
if (!element(ids.entryDialog).open) element(ids.entryDialog).showModal();
const wheels = element(ids.wheels);
wheels.replaceChildren(...selection.map((_, index) => makeWheel(index)));
if (!element(ids.entryDialog).open) element(ids.entryDialog).showModal();
for (const [index, column] of Array.from(wheels.children).entries()) column.children[1].scrollTop = selection[index] * wheelStep;
}
function randomIndex(size) {
const values = new Uint32Array(1);
const limit = Math.floor(0x100000000 / size) * size;
do { globalThis.crypto.getRandomValues(values); } while (values[0] >= limit);
return values[0] % size;
}
function chooseCode() {
if (codeChoice) return Promise.resolve(null);
openEntry(true);
return new Promise(resolve => { codeChoice = resolve; });
}
function cancelChoice() {
dialogGeneration += 1;
const resolve = codeChoice;
codeChoice = null;
resolve?.(null);
}
function updateCountdown() {
if (!currentGroup || !element(ids.dialog).open || element(ids.invitation).hidden) return;
const seconds = Math.max(0, Math.ceil(currentExpiry - Date.now() / 1000));
const time = new Date(currentExpiry * 1000).toLocaleTimeString(ns.I18n.language, { hour: '2-digit', minute: '2-digit' });
element(ids.expiry).textContent = seconds ? `${t('expires', { time })} ${t('remaining', {
minutes: Math.floor(seconds / 60), seconds: String(seconds % 60).padStart(2, '0')
})}` : t('code-expired');
}
function showInvitation(payload) {
if (!Array.isArray(payload.code) || payload.code.length !== codeLength) throw new Error(t('error.connection'));
currentGroup = payload.group;
element(ids.status).textContent = '';
element(ids.status).textContent = payload.expired ? t('code-expired') : '';
if (element(ids.entryDialog).open) element(ids.entryDialog).close();
element(ids.invitation).hidden = false;
element('group-route').textContent = payload.group.route.name || '';
const motifs = payload.code.map(value => ns.GroupSymbols.get(value));
element(ids.code).replaceChildren(...motifs.map(picture));
const time = new Date(payload.expiresAt * 1000).toLocaleTimeString(ns.I18n.language, { hour: '2-digit', minute: '2-digit' });
element(ids.expiry).textContent = t('expires', { time });
currentExpiry = payload.expiresAt;
if (!element(ids.dialog).open) element(ids.dialog).showModal();
updateCountdown();
}
let invitationPolling = false;
@@ -179,14 +218,15 @@ globalThis.Wegwichtel ??= {};
const payload = await request(`/${group.id}/invitation`);
if (currentGroup?.id !== group.id || !element(ids.dialog).open || element(ids.invitation).hidden) return;
if (payload.code) showInvitation(payload);
element(ids.status).textContent = payload.busy && payload.expired ? t('code-wait') : '';
element(ids.status).textContent = payload.expired ? t('code-expired') : '';
} catch { /* Keep the invitation visible; expiry still applies on the server. */ }
finally { invitationPolling = false; }
}
async function edit(group) {
if (element(ids.dialog).open) element(ids.dialog).close();
if (element(ids.entryDialog).open) element(ids.entryDialog).close();
await ns.EditorUi.openGroup(group.route);
const payload = await request(`/${group.id}/edit`, 'POST', {});
await ns.EditorUi.openGroup(payload.group.route);
}
async function renderPending() {
@@ -228,8 +268,9 @@ globalThis.Wegwichtel ??= {};
})));
if (group.role === 'owner') {
actions.appendChild(button(t('new-code'), () => guarded(async () => {
if (!globalThis.confirm(t('new-code-confirm'))) return;
showInvitation(await request(`/${group.id}/code`, 'POST', {}));
const code = await chooseCode();
if (!code) return;
showInvitation(await request(`/${group.id}/code`, 'POST', { code }));
await refresh();
})));
if (group.active) actions.appendChild(button(t('close-sharing'), () => guarded(async () => {
@@ -238,6 +279,11 @@ globalThis.Wegwichtel ??= {};
await refresh();
})));
}
if (group.role === 'owner' && group.active) actions.appendChild(button(t('code-label'), () => guarded(async () => {
const payload = await request(`/${group.id}/invitation`);
if (payload.code) showInvitation(payload);
else { const code = await chooseCode(); if (code) showInvitation(await request(`/${group.id}/code`, 'POST', { code })); }
})));
item.append(name, actions);
list.appendChild(item);
}
@@ -249,11 +295,12 @@ globalThis.Wegwichtel ??= {};
} catch { /* Existing drafts and forms stay usable without a connection. */ }
}
async function share(routeId) {
async function share(routeId, code = null) {
code ??= await chooseCode();
if (!code) return;
try {
const payload = await request('', 'POST', { routeId, creator_id: ns.LocalRoutes.creatorId() });
const editor = document.getElementById('editor-dialog');
if (!editor || editor.open) showInvitation(payload);
const payload = await request('', 'POST', { routeId, code, creator_id: ns.LocalRoutes.creatorId() });
showInvitation(payload);
await refresh();
} catch (error) { throw new Error(errorMessage(error)); }
}
@@ -287,8 +334,17 @@ globalThis.Wegwichtel ??= {};
}
async function joinSelectedGroup() {
const generation = dialogGeneration;
if (!element(ids.entryDialog).open) return;
const motifs = ns.GroupSymbols.list();
if (codeChoice) {
const resolve = codeChoice;
codeChoice = null;
const code = selection.map(index => motifs[index].unicode);
element(ids.entryDialog).close();
resolve(code);
return;
}
const generation = dialogGeneration;
let payload;
try {
payload = await request('/join', 'POST', {
@@ -305,17 +361,18 @@ globalThis.Wegwichtel ??= {};
function init() {
globalThis.setInterval?.(() => { refreshInvitation(); }, 30_000);
globalThis.setInterval?.(updateCountdown, 1000);
document.addEventListener('wegwichtel:point-drafts-changed', () => { renderPending(); ns.StorageUsage?.refresh(); });
element(ids.entryDialog).addEventListener('cancel', () => { dialogGeneration += 1; });
element(ids.entryDialog).addEventListener('close', () => { dialogGeneration += 1; });
element(ids.entryDialog).addEventListener('cancel', cancelChoice);
element(ids.entryDialog).addEventListener('close', cancelChoice);
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.join), () => guarded(joinSelectedGroup));
activate(element(ids.join), () => codeChoice ? joinSelectedGroup() : guarded(joinSelectedGroup));
activate(element(ids.edit), () => guarded(() => edit(currentGroup)));
}
ns.Groups = Object.freeze({ share, shareButton, refresh, refreshInvitation, headersFor, observe });
ns.Groups = Object.freeze({ share, chooseCode, shareButton, refresh, refreshInvitation, headersFor, observe });
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true });
else init();
}(globalThis.Wegwichtel));
+7 -3
View File
@@ -134,7 +134,11 @@ globalThis.Wegwichtel ??= {};
"ready": "Alles ist bereit."
},
"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.",
"remaining": "Noch {minutes} Min. {seconds} Sek. gültig.",
"choose-code": "Code auswählen",
"save-code": "Diesen Code verwenden",
"code-expired": "Der Code ist abgelaufen. Wähle einen Code, um wieder andere einzuladen.",
"code-wait": "In der Gruppe wird gerade eine Station bearbeitet. Warte, bis die Stationen frei sind, bevor du den Code änderst.",
"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?",
"share": "Gemeinsam bearbeiten",
@@ -147,8 +151,7 @@ globalThis.Wegwichtel ??= {};
"edit": "Entwurf bearbeiten",
"reload": "Änderungen holen",
"reload-confirm": "Ungespeicherte Eingaben werden ersetzt. Möchtest du den aktuellen gemeinsamen Stand holen?",
"new-code": "Neuen Code anzeigen",
"new-code-confirm": "Der bisherige Code funktioniert danach nicht mehr. Bereits beigetretene Personen bleiben dabei. Neuen Code erstellen?",
"new-code": "Code auswählen",
"close-sharing": "Gruppe schließen",
"close-confirm": "Die anderen können diesen Entwurf danach nicht mehr öffnen oder bearbeiten. Du behältst den Zugriff. Gruppe schließen?",
"expires": "Der Code gilt bis {time}.",
@@ -157,6 +160,7 @@ globalThis.Wegwichtel ??= {};
"next": "Nächster Wichtel an Stelle {number}",
"none": "Du bist noch in keiner Gruppe.",
"error": {
"code-taken": "Diesen Code nutzt schon eine andere Gruppe. Wähle andere Wichtel.",
"limit": "Schließe erst eine deiner Gruppen, bevor du eine weitere öffnest.",
"full": "Diese Gruppe ist bereits voll. Bitte um einen Code für eine andere Gruppe.",
"rate": "Bitte warte eine Minute und versuche es dann erneut.",
+7 -3
View File
@@ -134,7 +134,11 @@ globalThis.Wegwichtel ??= {};
"ready": "Everything is ready."
},
"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.",
"remaining": "Valid for another {minutes} min {seconds} sec.",
"choose-code": "Choose a code",
"save-code": "Use this code",
"code-expired": "This code has expired. Choose a code to invite others again.",
"code-wait": "Someone is editing a stop. Wait until the stops are free before changing the code.",
"title": "Shared drafts",
"upload-confirm": "Your draft, including pictures and recordings, will be sent to the website and copied into a shared draft. Continue?",
"share": "Edit together",
@@ -147,8 +151,7 @@ globalThis.Wegwichtel ??= {};
"edit": "Edit draft",
"reload": "Get changes",
"reload-confirm": "Your unsaved changes will be replaced. Would you like to get the latest shared version?",
"new-code": "Show a new code",
"new-code-confirm": "The old code will stop working. People who have already joined will keep their access. Create a new code?",
"new-code": "Choose a code",
"close-sharing": "Close group",
"close-confirm": "The others will no longer be able to open or edit this draft. You will keep your access. Close the group?",
"expires": "This code works until {time}.",
@@ -157,6 +160,7 @@ globalThis.Wegwichtel ??= {};
"next": "Next gnome in position {number}",
"none": "You have not joined a group yet.",
"error": {
"code-taken": "Another group is using this code. Choose different gnomes.",
"limit": "Close one of your groups before opening another.",
"full": "This group is full. Ask for a code for another group.",
"rate": "Please wait a minute and try again.",