diff --git a/docs/Gruppenbearbeitung.md b/docs/Gruppenbearbeitung.md index 0294ca2..13dd156 100644 --- a/docs/Gruppenbearbeitung.md +++ b/docs/Gruppenbearbeitung.md @@ -1,14 +1,16 @@ # Gemeinsam bearbeiten -In „Strecken bearbeiten“ öffnet der kleine Gruppenbutton zwischen Stift und -Löschen einen Dialog. Dort sind die beiden Wichtel groß zu sehen: links der Vogel, -rechts der Stein. Erst „Gemeinsamen Entwurf öffnen“ legt einen gemeinsamen -Entwurf auf der Website an. Das Original bleibt unverändert. Bei lokalen Entwürfen +In „Strecken bearbeiten“ öffnet der kleine Button mit zwei gezeichneten Wichteln +zwischen Stift und +Löschen den gemeinsamen Entwurf und zeigt anschließend den echten Code mit +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. Ein bereits eingesandter eigener Entwurf wird dabei zuerst aktualisiert. 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 nach rechts. Wiederholungen sind erlaubt. Pfeiltasten und die Pfeilschaltflächen funktionieren ebenfalls. „Mitmachen“ öffnet den gemeinsamen Entwurf. diff --git a/docs/agents/GRUPPENBEARBEITUNG.md b/docs/agents/GRUPPENBEARBEITUNG.md index 05f8389..18333e0 100644 --- a/docs/agents/GRUPPENBEARBEITUNG.md +++ b/docs/agents/GRUPPENBEARBEITUNG.md @@ -92,3 +92,14 @@ Sitzungen, Rollen owner/member, Codewechsel und Widerruf. Bedienung und Einspiel nicht vorhanden; bestehende Gruppenrechte gelten unverändert. Heartbeats sind POSTs mit eigenem Bearbeitungsschlüssel und dürfen weder fremde noch bereits 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. diff --git a/public/css/editor.css b/public/css/editor.css index 3ef207f..0a68f93 100644 --- a/public/css/editor.css +++ b/public/css/editor.css @@ -595,24 +595,24 @@ .current-location-button img { width: 24px; height: 24px; vertical-align: middle; } /* 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-pictures img { width: min(34vw, 9rem); height: 10rem; object-fit: contain; } +.group-share-button img { display: block; width: 100%; height: 100%; object-fit: contain; pointer-events: none; } .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-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } .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: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 legend { margin-bottom: .5rem; font-weight: 600; } .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-option { height: 52px; 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-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 img { width: 100%; height: 64px; object-fit: contain; } +.group-wheel-option { height: 96px; display: grid; place-items: center; scroll-snap-align: center; cursor: pointer; } +.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(44px, 1fr)); gap: .3rem; padding: .6rem; border: 1px solid #c8d3ca; border-radius: 1rem; background: #e7f1e9; } +.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 .editor-secondary-button { font-size: .85rem; } .group-status { display: block; min-height: 1.5rem; color: #9b2c2c; } diff --git a/public/images/icons/group-edit.svg b/public/images/icons/group-edit.svg new file mode 100644 index 0000000..1d12484 --- /dev/null +++ b/public/images/icons/group-edit.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/index.html b/public/index.html index 2e4e8f2..95829ee 100644 --- a/public/index.html +++ b/public/index.html @@ -579,33 +579,29 @@ - + - Gemeinsam bearbeiten - × - - - - Mit diesen beiden Wichteln öffnest du einen gemeinsamen Entwurf. Andere können mit einem Wichtelcode mitmachen. Das Original bleibt erhalten. - - Gemeinsamen Entwurf öffnen - - - - - Gemeinsam bearbeiten - × + Gruppencode eingeben + × 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. Gruppencode - Mitmachen + Mitmachen + + + + + Gruppencode + × + + Zeige diesen Code den anderen. Damit können sie deinen gemeinsamen Entwurf öffnen und bearbeiten. Das Original bleibt erhalten. - Entwurf bearbeiten + Entwurf bearbeiten diff --git a/public/js/editor-ui.js b/public/js/editor-ui.js index be73344..d045539 100644 --- a/public/js/editor-ui.js +++ b/public/js/editor-ui.js @@ -1652,7 +1652,6 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; if (shareInProgress) return; shareInProgress = true; try { - if (ns.Groups.confirmShare && !await ns.Groups.confirmShare(routeName(route))) return; let sourceId = routeId(route); if (routeSource(route) === 'local') { if (!globalThis.confirm(ns.I18n.t('groups.upload-confirm'))) return; diff --git a/public/js/groups.js b/public/js/groups.js index 3aceb82..5120e65 100644 --- a/public/js/groups.js +++ b/public/js/groups.js @@ -6,10 +6,11 @@ globalThis.Wegwichtel ??= {}; joinOpen: 'group-join-open', editorJoin: 'editor-group-join', list: 'editor-group-list', dialog: 'group-dialog', close: 'group-close', status: 'group-status', wheels: 'group-wheels', 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 wheelStep = 52; + const wheelStep = 96; const revisions = new Map(); let selection = []; let currentGroup = null; @@ -73,12 +74,12 @@ globalThis.Wegwichtel ??= {}; async function guarded(action) { if (pending) return; pending = true; - element(ids.status).textContent = ''; + const status = element(element(ids.entryDialog).open ? ids.entryStatus : ids.status); + status.textContent = ''; element(ids.join).disabled = true; try { await action(); } catch (error) { - element(ids.status).textContent = errorMessage(error); - if (!element(ids.dialog).open) { - element(ids.entry).hidden = true; + element(element(ids.entryDialog).open ? ids.entryStatus : ids.status).textContent = errorMessage(error); + if (!element(ids.dialog).open && !element(ids.entryDialog).open) { element(ids.invitation).hidden = true; element(ids.dialog).showModal(); } @@ -146,21 +147,22 @@ globalThis.Wegwichtel ??= {}; } function openEntry() { + if (element(ids.dialog).open) element(ids.dialog).close(); dialogGeneration += 1; selection = new Array(codeLength).fill(0); - element(ids.status).textContent = ''; - element(ids.entry).hidden = false; - element(ids.invitation).hidden = true; + element(ids.entryStatus).textContent = ''; const wheels = element(ids.wheels); 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) { + if (!Array.isArray(payload.code) || payload.code.length !== codeLength) throw new Error(t('error.connection')); currentGroup = payload.group; element(ids.status).textContent = ''; - element(ids.entry).hidden = true; + 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' }); @@ -182,7 +184,8 @@ globalThis.Wegwichtel ??= {}; finally { invitationPolling = false; } } 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); } @@ -258,34 +261,20 @@ globalThis.Wegwichtel ??= {}; function shareButton(action) { const result = document.createElement('button'); result.type = 'button'; - result.className = 'editor-icon-button'; + result.className = 'editor-icon-button group-share-button'; result.dataset.editorAction = action; result.title = t('share'); result.setAttribute('aria-label', t('share')); 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; } - 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) { const id = /^\/routes\/(\d+)(?:\/|$)/.exec(path)?.[1]; 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() }); } catch (error) { - if (!element(ids.dialog).open || generation !== dialogGeneration) return; + if (!element(ids.entryDialog).open || generation !== dialogGeneration) return; throw error; } - if (!element(ids.dialog).open || generation !== dialogGeneration) return; + if (!element(ids.entryDialog).open || generation !== dialogGeneration) return; await edit(payload.group); await refresh(); } function init() { 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(); }); - element(ids.dialog).addEventListener('cancel', () => { dialogGeneration += 1; }); - element(ids.dialog).addEventListener('close', () => { dialogGeneration += 1; }); + element(ids.entryDialog).addEventListener('cancel', () => { 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); activate(element(ids.close), () => element(ids.dialog).close()); activate(element(ids.join), () => guarded(joinSelectedGroup)); 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 }); else init(); }(globalThis.Wegwichtel)); diff --git a/public/js/texts-de.js b/public/js/texts-de.js index 4bfd249..216adc4 100644 --- a/public/js/texts-de.js +++ b/public/js/texts-de.js @@ -135,8 +135,6 @@ globalThis.Wegwichtel ??= {}; }, "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.", - "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", "upload-confirm": "Dein Entwurf mit Bildern und Ton wird dafür an die Website gesendet und als gemeinsamer Entwurf kopiert. Fortfahren?", "share": "Gemeinsam bearbeiten", diff --git a/public/js/texts-en.js b/public/js/texts-en.js index 870031c..ecd7ff8 100644 --- a/public/js/texts-en.js +++ b/public/js/texts-en.js @@ -135,8 +135,6 @@ globalThis.Wegwichtel ??= {}; }, "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.", - "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", "upload-confirm": "Your draft, including pictures and recordings, will be sent to the website and copied into a shared draft. Continue?", "share": "Edit together", diff --git a/test/groups-ui.test.js b/test/groups-ui.test.js index f493325..189cea0 100644 --- a/test/groups-ui.test.js +++ b/test/groups-ui.test.js @@ -27,7 +27,7 @@ function key(node, value) { } async function harness(loading = false) { 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']; const nodes = new Map(ids.map(id => [id, new Element()])); const document = new EventTarget(); @@ -66,9 +66,9 @@ test('group code wheels support arrows, keyboard wrapping, scrolling and joining const h = await harness(true); h.context.document.dispatchEvent(new Event('DOMContentLoaded')); 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')); - 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; assert.equal(columns.length, 5); 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'); pointer(viewport.children[5]); 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'), ' '); await tick(); 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.creator_id, 'a'.repeat(64)); 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('group-close')); - assert.equal(h.nodes.get('group-dialog').open, false); + pointer(h.nodes.get('group-entry-close')); + 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 () => { @@ -102,11 +102,14 @@ test('sharing displays five motifs and the owner can renew or close access with await h.groups.share(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-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/); const shareButton = h.groups.shareButton('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'); pointer(h.nodes.get('group-edit')); await tick(); 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/]]) { h.controls.error = status; 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); } h.controls.error = 0; h.controls.offline = true; h.context.Wegwichtel.I18n.setLanguage('en', null); 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(); }); @@ -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')); await tick(); - pointer(h.nodes.get('group-close')); + pointer(h.nodes.get('group-entry-close')); release(); 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, []); }); @@ -189,29 +192,27 @@ test('unsent station changes are listed before network access and discarded only 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 decision = h.groups.confirmShare('Blumenweg'); - const dialog = h.nodes.get('group-share-dialog'); - assert.equal(dialog.open, true); - assert.equal(h.nodes.get('group-share-route').textContent, 'Blumenweg'); - const pictures = h.nodes.get('group-share-pictures').children; - assert.equal(pictures.length, 2); - assert.match(pictures[0].src, /🐦/); - assert.match(pictures[1].src, /🪨/); - assert.equal(h.calls.length, 0); - assert.equal(await h.groups.confirmShare('Zweiter Weg'), false); - 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); + await h.groups.share(7); + assert.equal(h.nodes.get('group-dialog').open, true); + assert.equal(h.nodes.get('group-entry-dialog').open, false); + assert.equal(h.nodes.get('group-code').children.length, 5); + assert.equal(h.nodes.get('group-route').textContent, 'Waldweg'); + pointer(h.nodes.get('group-close')); + pointer(h.nodes.get('editor-group-join')); + assert.equal(h.nodes.get('group-entry-dialog').open, true); + assert.equal(h.nodes.get('group-dialog').open, false); + assert.equal(h.nodes.get('group-wheels').children.length, 5); }); +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 () => { const h = await harness();
Mit diesen beiden Wichteln öffnest du einen gemeinsamen Entwurf. Andere können mit einem Wichtelcode mitmachen. Das Original bleibt erhalten.
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.
Zeige diesen Code den anderen. Damit können sie deinen gemeinsamen Entwurf öffnen und bearbeiten. Das Original bleibt erhalten.