report poll saving errors using a modal (#729)

This commit is contained in:
Jeldrik Hanschke 2023-11-05 12:19:47 +01:00 committed by GitHub
parent 0c4ef6fc5b
commit 88a51964f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 464 additions and 33 deletions

View file

@ -1,11 +1,22 @@
<BsButton
@type="primary"
{{!
Due to a bug in Ember, conditional modifiers cannot be used with the "on"
modifier. Need to always apply the modifier and fallback to a noop function
(returned by noop helper) instead of only applying the modifier when needed.
See https://github.com/emberjs/ember.js/issues/19869.
}}
{{on "click" (if @onClick @onClick (noop))}}
class="cr-steps-bottom-nav__button cr-steps-bottom-nav__next-button next"
type="submit"
...attributes
>
<span class="cr-steps-bottom-nav__label">
{{t "action.save"}}
{{#if (has-block)}}
{{yield}}
{{else}}
{{t "action.save"}}
{{/if}}
</span>
{{#if @isPending}}

View file

@ -4,8 +4,12 @@ interface SaveButtonSignature {
Args: {
Named: {
isPending: boolean;
onClick?: () => void;
};
};
Blocks: {
default: [];
};
Element: HTMLButtonElement;
}

View file

@ -9,6 +9,7 @@ import type RouterService from '@ember/routing/router-service';
import type { CreateSettingsRouteModel } from 'croodle/routes/create/settings';
import type IntlService from 'ember-intl/services/intl';
import type FlashMessagesService from 'ember-cli-flash/services/flash-messages';
import { tracked } from '@glimmer/tracking';
export default class CreateSettings extends Controller {
@service declare flashMessages: FlashMessagesService;
@ -17,6 +18,8 @@ export default class CreateSettings extends Controller {
declare model: CreateSettingsRouteModel;
@tracked savingPollFailed = false;
get anonymousUser() {
return this.model.anonymousUser;
}
@ -94,7 +97,7 @@ export default class CreateSettings extends Controller {
}
@action
async submit() {
async createPoll() {
const { model } = this;
const {
anonymousUser,
@ -158,15 +161,20 @@ export default class CreateSettings extends Controller {
);
// redirect to new poll
await this.router.transitionTo('poll', poll.id, {
await this.router.transitionTo('poll.participation', poll.id, {
queryParams: {
encryptionKey,
},
});
} catch (err) {
this.flashMessages.danger('error.poll.savingFailed');
this.savingPollFailed = true;
throw err;
reportError(err);
}
}
@action
resetSavingPollFailedState() {
this.savingPollFailed = false;
}
}

13
app/helpers/noop.ts Normal file
View file

@ -0,0 +1,13 @@
import { helper } from '@ember/component/helper';
const noop = helper(() => {
return () => {};
});
export default noop;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
noop: typeof noop;
}
}

View file

@ -3,7 +3,7 @@
@formLayout="horizontal"
@model={{this}}
@onInvalid={{(scroll-first-invalid-element-into-view-port)}}
@onSubmit={{this.submit}}
@onSubmit={{this.createPoll}}
novalidate
as |form|
>
@ -78,5 +78,36 @@
<BackButton @onClick={{this.previousPage}} />
</div>
</div>
<BsModal
@onHidden={{this.resetSavingPollFailedState}}
@onSubmit={{form.submit}}
@open={{this.savingPollFailed}}
data-test-modal="saving-failed"
as |modal|
>
<modal.header
@closeButton={{false}}
@title={{t "error.poll.savingFailed.title"}}
/>
<modal.body>
<p>
{{t "error.poll.savingFailed.description"}}
</p>
</modal.body>
<modal.footer>
<BsButton @onClick={{modal.close}} data-test-button="abort">
{{t "action.abort"}}
</BsButton>
<SaveButton
@isPending={{form.isSubmitting}}
@onClick={{modal.submit}}
data-test-button="retry"
type="button"
>
{{t "modal.save-retry.button-retry"}}
</SaveButton>
</modal.footer>
</BsModal>
</BsForm>
</div>

185
package-lock.json generated
View file

@ -62,6 +62,7 @@
"ember-power-calendar-luxon": "^0.5.0",
"ember-qunit": "^7.0.0",
"ember-resolver": "^11.0.1",
"ember-sinon-qunit": "^7.4.0",
"ember-source": "~5.4.0",
"ember-template-lint": "^5.11.2",
"ember-test-selectors": "^6.0.0",
@ -82,6 +83,7 @@
"qunit-dom": "^3.0.0",
"release-it": "^16.0.0",
"sass": "^1.19.0",
"sinon": "^17.0.1",
"sjcl": "^1.0.8",
"stylelint": "^15.10.3",
"stylelint-config-standard": "^34.0.0",
@ -9679,6 +9681,50 @@
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@sinonjs/commons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
"dev": true,
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/fake-timers": {
"version": "11.2.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@sinonjs/samsam": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
"integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^2.0.0",
"lodash.get": "^4.4.2",
"type-detect": "^4.0.8"
}
},
"node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
"dev": true,
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/text-encoding": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
"dev": true
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
@ -10214,6 +10260,21 @@
"@types/node": "*"
}
},
"node_modules/@types/sinon": {
"version": "10.0.20",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.20.tgz",
"integrity": "sha512-2APKKruFNCAZgx3daAyACGzWuJ028VVCUDk6o2rw/Z4PXT0ogwdV4KUegW0MwVs0Zu59auPXbbuBJHF12Sx1Eg==",
"dev": true,
"dependencies": {
"@types/sinonjs__fake-timers": "*"
}
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.4",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.4.tgz",
"integrity": "sha512-GDV68H0mBSN449sa5HEj51E0wfpVQb8xNSMzxf/PrypMFcLTMwJMOM/cgXiv71Mq5drkOQmUGvL1okOZcu6RrQ==",
"dev": true
},
"node_modules/@types/sizzle": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.5.tgz",
@ -38972,6 +39033,21 @@
"node": ">= 4"
}
},
"node_modules/ember-sinon-qunit": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ember-sinon-qunit/-/ember-sinon-qunit-7.4.0.tgz",
"integrity": "sha512-BcH2scgJ4Vpq5Fnjeq5Z2ESnHLsmcfFRaq/gOujy3my+8w7WTtrHyaUgWzmd5mLw+tfCYssAUEalQhk1ZNpV+g==",
"dev": true,
"dependencies": {
"@embroider/addon-shim": "^1.8.6",
"@types/sinon": "^10.0.19"
},
"peerDependencies": {
"ember-source": ">=3.28.0",
"qunit": "^2.0.0",
"sinon": "^15.0.3 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/ember-source": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/ember-source/-/ember-source-5.4.0.tgz",
@ -46825,6 +46901,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/just-extend": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
"integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
"dev": true
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -48842,6 +48924,61 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node_modules/nise": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz",
"integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^2.0.0",
"@sinonjs/fake-timers": "^10.0.2",
"@sinonjs/text-encoding": "^0.7.1",
"just-extend": "^4.0.2",
"path-to-regexp": "^1.7.0"
}
},
"node_modules/nise/node_modules/@sinonjs/commons": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
"dev": true,
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/nise/node_modules/@sinonjs/fake-timers": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
"dev": true,
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/nise/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"dev": true
},
"node_modules/nise/node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dev": true,
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
@ -52764,6 +52901,45 @@
"integrity": "sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==",
"dev": true
},
"node_modules/sinon": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.1.0",
"nise": "^5.1.5",
"supports-color": "^7.2.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/sinon"
}
},
"node_modules/sinon/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/sinon/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/sjcl": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz",
@ -55552,6 +55728,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/type-fest": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",

View file

@ -81,6 +81,7 @@
"ember-power-calendar-luxon": "^0.5.0",
"ember-qunit": "^7.0.0",
"ember-resolver": "^11.0.1",
"ember-sinon-qunit": "^7.4.0",
"ember-source": "~5.4.0",
"ember-template-lint": "^5.11.2",
"ember-test-selectors": "^6.0.0",
@ -101,6 +102,7 @@
"qunit-dom": "^3.0.0",
"release-it": "^16.0.0",
"sass": "^1.19.0",
"sinon": "^17.0.1",
"sjcl": "^1.0.8",
"stylelint": "^15.10.3",
"stylelint-config-standard": "^34.0.0",

View file

@ -26,6 +26,7 @@ import pageCreateSettings from 'croodle/tests/pages/create/settings';
import pagePollParticipation from 'croodle/tests/pages/poll/participation';
import asyncThrowsAssertion from '../assertions/async-throws';
import { calendarSelect } from 'ember-power-calendar/test-support';
import sinon from 'sinon';
module('Acceptance | create a poll', function (hooks) {
hooks.beforeEach(function () {
@ -141,25 +142,16 @@ module('Acceptance | create a poll', function (hooks) {
'available answers selection has autofocus',
);
// simulate temporary server error
this.server.logging = true;
this.server.post('/polls', undefined, 503);
await assert.asyncThrows(async () => {
await pageCreateSettings.save();
}, 'Unexpected server-side error. Server responded with 503 (Service Unavailable)');
assert.strictEqual(currentRouteName(), 'create.settings');
// simulate server is available again
// defer creation for testing loading spinner
let resolveSubmission;
let resolveSubmissionWith;
this.server.post('/polls', function (schema) {
return new Promise((resolve) => {
let attrs = this.normalizedRequestAttrs();
resolveSubmission = resolve;
resolveSubmissionWith = schema.polls.create(attrs);
resolveSubmission = () => {
const attrs = this.normalizedRequestAttrs();
const poll = schema.polls.create(attrs);
resolve(poll);
};
});
});
@ -171,7 +163,7 @@ module('Acceptance | create a poll', function (hooks) {
});
assert.ok(true, 'loading spinner is shown');
resolveSubmission(resolveSubmissionWith);
resolveSubmission();
await settled();
assert.strictEqual(currentRouteName(), 'poll.participation');
@ -958,6 +950,170 @@ module('Acceptance | create a poll', function (hooks) {
assert.strictEqual(currentRouteName(), 'create.index');
});
test('informs user if saving fails', async function (assert) {
const reportErrorFake = sinon.replace(window, 'reportError', sinon.fake());
await pageCreateIndex.visit();
assert.strictEqual(
currentRouteName(),
'create.index',
'assumption: can open start page of poll creation',
);
await pageCreateIndex.next();
assert.strictEqual(
currentRouteName(),
'create.meta',
'assumption: can go to title and description input step',
);
await pageCreateMeta.title('foo').next();
assert.strictEqual(
currentRouteName(),
'create.options',
'assumption: can go to options input step',
);
await pageCreateOptions.selectDates([new Date()]);
await pageCreateOptions.next();
assert.strictEqual(
currentRouteName(),
'create.options-datetime',
'assumption: can go to times input for dates after selecting one day',
);
await pageCreateOptionsDatetime.next();
assert.strictEqual(
currentRouteName(),
'create.settings',
'assumption: can go to settings page',
);
// simulate temporary server error
this.server.logging = true;
this.server.post('/polls', undefined, 503);
await click('form button[type="submit"]');
assert.strictEqual(
currentRouteName(),
'create.settings',
'user stays at settings route if saving fails',
);
assert
.dom('[data-test-modal="saving-failed"]')
.isVisible(
'modal is shown informing the user that saving the poll failed',
);
assert
.dom('[data-test-modal="saving-failed"] .modal-header')
.hasText(
t('error.poll.savingFailed.title'),
'modal has a meaningful title',
);
assert
.dom('[data-test-modal="saving-failed"] .modal-body')
.hasText(
t('error.poll.savingFailed.description'),
'modal has a meaningful body',
);
assert
.dom('[data-test-modal="saving-failed"] .modal-footer button')
.exists({ count: 2 }, 'modal has two buttons');
assert
.dom(
'[data-test-modal="saving-failed"] .modal-footer button[data-test-button="abort"]',
)
.hasText(t('action.abort'), 'abort button has meaningful text');
assert
.dom(
'[data-test-modal="saving-failed"] .modal-footer button[data-test-button="retry"]',
)
.hasText(
t('modal.save-retry.button-retry'),
'retry button has meaningful text',
);
assert.ok(reportErrorFake.calledOnce, 'error is reported to console');
assert.ok(
reportErrorFake.firstCall.args[0] instanceof Error,
'reported error is an instance of Error',
);
assert.strictEqual(
reportErrorFake.firstCall.args[0].message,
'Unexpected server-side error. Server responded with 503 (Service Unavailable)',
'reported error has meaningful error message',
);
await click(
'[data-test-modal="saving-failed"] button[data-test-button="retry"]',
);
assert.strictEqual(
currentRouteName(),
'create.settings',
'user stays at settings route if saving failed even on retry',
);
assert
.dom('[data-test-modal="saving-failed"]')
.isVisible('modal is still shown if retry fails');
assert.ok(
reportErrorFake.calledTwice,
'error is reported to console on failed retry',
);
await click(
'[data-test-modal="saving-failed"] button[data-test-button="abort"]',
);
assert
.dom('[data-test-modal="saving-failed"]')
.isNotVisible('user can close the modal that saving failed');
assert.strictEqual(
currentRouteName(),
'create.settings',
'user stays at settings route if closing the modal',
);
await click('form button[type="submit"]');
assert
.dom('[data-test-modal="saving-failed"]')
.isVisible('modal is visible again if saving fails again');
assert.ok(
reportErrorFake.calledThrice,
'error is reported to console on failed retry',
);
// simulate server is available again
// defer creation for testing loading spinner
let resolveSubmission;
this.server.post('/polls', function (schema) {
return new Promise((resolve) => {
resolveSubmission = () => {
const attrs = this.normalizedRequestAttrs();
const poll = schema.polls.create(attrs);
resolve(poll);
};
});
});
click('[data-test-modal="saving-failed"] button[data-test-button="retry"]');
// shows loading spinner while saving
await waitFor(
'[data-test-modal="saving-failed"] button[data-test-button="retry"] .spinner-border',
{
timeoutMessage: 'timeout while waiting for loading spinner to appear',
},
);
assert.ok(true, 'loading spinner is shown');
resolveSubmission();
await settled();
assert.strictEqual(
currentRouteName(),
'poll.participation',
'user is transitioned to poll participation page after successful retry',
);
});
module('validation', function () {
test('validates user input when creating a poll with dates and times', async function (assert) {
const day = DateTime.now();

View file

@ -4,6 +4,7 @@ import * as QUnit from 'qunit';
import { setApplication } from '@ember/test-helpers';
import { setup } from 'qunit-dom';
import { start } from 'ember-qunit';
import setupSinon from 'ember-sinon-qunit';
document.addEventListener(
'securitypolicyviolation',
@ -20,4 +21,6 @@ setApplication(Application.create(config.APP));
setup(QUnit.assert);
setupSinon();
start();

View file

@ -107,8 +107,10 @@ error:
expired: 'El sondeig ha caducat i ha estat eliminat.'
typo: 'Hi ha un error tipogràfic en la URL. Pots tornar a comprovar
el que has posat, especialment la part anterior al signe d''interrogació.'
savingFailed: 'No hem pogut guardar el sondeig, si us plau, intenta''l de
nou en uns segons.'
savingFailed:
title: 'Saving failed'
description: 'No hem pogut guardar el sondeig, si us plau, intenta''l de
nou en uns segons.'
generic:
unexpected:
title: 'Va ocórrer un error inesperat'

View file

@ -112,8 +112,11 @@ error:
typo: 'Die URL ist fehlerhaft. Bitte prüfe, dass die URL vollständig
und korrekt ist. Achte dabei insbesondere auf den Teil vor dem
Fragezeichen.'
savingFailed: 'Die Umfrage konnte nicht gespeichert werden. Bitte versuche
es in einigen Sekunden erneut.'
savingFailed:
title: 'Speichern fehlgeschlagen'
description: 'Die Umfrage konnte nicht gespeichert werden. Bitte prüfe deine
Internetverbindung und versuche es erneut. Sollte der Fehler weiterhin
auftreten, wende dich bitte an den Administrator der Seite.'
generic:
unexpected:
title: 'Ein unerwarteter Fehler ist aufgetreten'

View file

@ -107,7 +107,10 @@ error:
expired: 'The poll is expired and has been deleted.'
typo: 'There is a typo in the URL. You may want to double-check it
especially the part before the question mark.'
savingFailed: 'The poll could not be saved. Please try again in a few seconds.'
savingFailed:
title: 'Saving failed'
description: 'The poll could not be saved. Please check your network connection
and try again. Please contact the site administrator if the problem persists.'
generic:
unexpected:
title: 'An unexpected error occured'

View file

@ -109,8 +109,10 @@ error:
expired: 'La encuesta ha expirado y ha sido borrada.'
typo: 'Hay un error al teclear la URL. Por favor, comprueba que la
has escrito bien, sobre todo la parte anterior al signo de interrogación.'
savingFailed: 'La encuesta no pude ser guardada. Por favor prueba de nuevo
en unos segundos.'
savingFailed:
title: 'Saving failed'
description: 'La encuesta no pude ser guardada. Por favor prueba de nuevo
en unos segundos.'
generic:
unexpected:
title: 'Ocurrió un error inesperado'

View file

@ -108,8 +108,10 @@ error:
expired: 'Le sondage a expiré et a été supprimé.'
typo: "Il y a une faute de frappe dans l'URL. Vous voudrez peut-être\
\ revérifier - en particulier la partie avant le point d'interrogation."
savingFailed: "Le sondage n'a pas pu être enregistré. Veuillez réessayer dans\
\ quelques secondes."
savingFailed:
title: "Saving failed"
description: "Le sondage n'a pas pu être enregistré. Veuillez réessayer dans\
\ quelques secondes."
generic:
unexpected:
title: "Une erreur inattendue s'est produite"

View file

@ -108,8 +108,10 @@ error:
expired: 'Il sondaggio è scaduto ed è stato rimosso.'
typo: 'L''URL è errato. Prova a ricontrollarlo, specialmente la parte
prima del punto interrogativo.'
savingFailed: 'Non è stato possible salvare il sondaggio. Riprova tra qualche
secondo.'
savingFailed:
title: "Saving failed"
description: 'Non è stato possible salvare il sondaggio. Riprova tra qualche
secondo.'
generic:
unexpected:
title: 'Si è verificato un errore inaspettato'

View file

@ -87,7 +87,9 @@ error:
delen før spørsmålstegnet.
expired: Avstemmingen er utløpt, og har blitt slettet.
title: Kunne ikke finne avstemming
savingFailed: Kunne ikke lagre avstemming. Prøv igjen om noen sekunder.
savingFailed:
title: Saving failed
description: Kunne ikke lagre avstemming. Prøv igjen om noen sekunder.
decryptionFailed:
description: Dekryptering av avstemmingsdata mislyktes. Dette skjer antageligvis
fordi krypteringsnøkkelen ikke er riktig. Dobbeltsjekk nettadressen

View file

@ -15,6 +15,7 @@ type BsFormComponent = ComponentLike<{
{
element: BsFormElementComponent;
isSubmitting: boolean;
submit: () => void;
},
];
};

View file

@ -9,6 +9,7 @@ declare module '@glint/environment-ember-loose/registry' {
closeButton?: boolean;
footer?: boolean;
keyboard?: boolean;
onHide?: () => void;
onHidden?: () => void;
onSubmit?: () => void;
open: boolean;