diff --git a/high-level-tests/e2e/cypress/support/step_definitions/member.js b/high-level-tests/e2e/cypress/support/step_definitions/member.js index becff64d1e4..795e91f0db3 100644 --- a/high-level-tests/e2e/cypress/support/step_definitions/member.js +++ b/high-level-tests/e2e/cypress/support/step_definitions/member.js @@ -31,4 +31,5 @@ When(`j'invite {string} à rejoindre l'organisation`, (emailAddresses) => { .parent() .within(() => cy.get("textarea").type(emailAddresses)); cy.get("button").contains("Inviter").click(); + cy.get("button").contains("Valider").click(); }); diff --git a/orga/app/components/team/invite-form.hbs b/orga/app/components/team/invite-form.hbs index 84c59a9edf4..2702c6d1325 100644 --- a/orga/app/components/team/invite-form.hbs +++ b/orga/app/components/team/invite-form.hbs @@ -6,21 +6,46 @@ @id="email" type="email" @value={{@email}} + aria-invalid={{if this.emailError "true" "false"}} + aria-describedby="email-error" class="invite-form__email-field" @requiredLabel={{t "common.form.mandatory-fields-title"}} {{on "change" @onUpdateEmail}} > <:label>{{t "pages.team-new-item.input-label"}} + {{#if this.emailError}} +

{{this.emailError}}

+ {{/if}}
{{t "common.actions.cancel"}} - + {{t "pages.team-new-item.invite-button"}}
+ + <:content> +

{{t "pages.team-new.invite-form-modal.warning"}}

+

{{t "pages.team-new.invite-form-modal.question"}}

+ + + <:footer> + + {{t "common.actions.cancel"}} + + {{t + "pages.team-new.invite-form-modal.confirm" + }} + +
\ No newline at end of file diff --git a/orga/app/components/team/invite-form.js b/orga/app/components/team/invite-form.js new file mode 100644 index 00000000000..82839ed6b6f --- /dev/null +++ b/orga/app/components/team/invite-form.js @@ -0,0 +1,35 @@ +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import isEmailValid from '../../utils/email-validator'; + +export default class InviteForm extends Component { + @service intl; + @tracked modalOpen = false; + @tracked emailError = null; + + @action + openModal() { + const emailInput = this.args?.email?.trim(); + if (!emailInput) { + this.emailError = this.intl.t('pages.team-new.errors.mandatory-email-field'); + return; + } + const emails = emailInput.split(',').map((email) => email.trim()); + const areEmailsValid = emails.every((email) => isEmailValid(email)); + + if (!areEmailsValid) { + this.emailError = this.intl.t('pages.team-new.errors.invalid-input'); + return; + } + this.emailError = null; + this.modalOpen = true; + } + + @action + closeModal() { + this.modalOpen = false; + } +} diff --git a/orga/app/styles/app.scss b/orga/app/styles/app.scss index a46e71e74dd..0574d9e9124 100644 --- a/orga/app/styles/app.scss +++ b/orga/app/styles/app.scss @@ -40,6 +40,7 @@ @use 'components/progress-bar' as *; @use 'components/register-form' as *; @use 'components/team' as *; +@use 'components/team/invite-form' as *; @use 'components/login-or-register' as *; @use 'components/manage-authentication-method-modal' as *; @use 'components/participation-filters' as *; diff --git a/orga/app/styles/components/team/invite-form.scss b/orga/app/styles/components/team/invite-form.scss new file mode 100644 index 00000000000..eeadebce346 --- /dev/null +++ b/orga/app/styles/components/team/invite-form.scss @@ -0,0 +1,15 @@ +@use 'pix-design-tokens/typography'; + +.invite-form { + + &__email-field { + min-height: 70px; + } + + &__error-message { + @extend %pix-body-s; + + margin-top: var(--pix-spacing-2x); + color: var(--pix-error-700); + } +} diff --git a/orga/app/styles/pages/authenticated/team/new.scss b/orga/app/styles/pages/authenticated/team/new.scss index 05e5c489a14..9f279e27cb4 100644 --- a/orga/app/styles/pages/authenticated/team/new.scss +++ b/orga/app/styles/pages/authenticated/team/new.scss @@ -10,7 +10,4 @@ } } -.invite-form__email-field { - min-height: 70px; -} diff --git a/orga/tests/acceptance/team-creation-test.js b/orga/tests/acceptance/team-creation-test.js index a1af6569f65..b5a289736c3 100644 --- a/orga/tests/acceptance/team-creation-test.js +++ b/orga/tests/acceptance/team-creation-test.js @@ -8,6 +8,7 @@ import { module, test } from 'qunit'; import authenticateSession from '../helpers/authenticate-session'; import setupIntl from '../helpers/setup-intl'; import { createPrescriberByUser, createUserMembershipWithRole } from '../helpers/test-init'; +import { waitForDialog } from '../helpers/wait-for'; module('Acceptance | Team Creation', function (hooks) { setupApplicationTest(hooks); @@ -55,6 +56,7 @@ module('Acceptance | Team Creation', function (hooks) { let inputLabel; let cancelButton; let inviteButton; + let confirmButton; hooks.beforeEach(async function () { user = createUserMembershipWithRole('ADMIN'); @@ -67,6 +69,7 @@ module('Acceptance | Team Creation', function (hooks) { inputLabel = `${t('pages.team-new-item.input-label')} *`; inviteButton = t('pages.team-new-item.invite-button'); cancelButton = t('common.actions.cancel'); + confirmButton = t('pages.team-new.invite-form-modal.confirm'); }); test('it should be accessible', async function (assert) { @@ -95,6 +98,12 @@ module('Acceptance | Team Creation', function (hooks) { await clickByName(inviteButton); // then + await waitForDialog(); + + //when + await clickByName(confirmButton); + + //then const organizationInvitation = server.db.organizationInvitations[server.db.organizationInvitations.length - 1]; assert.strictEqual(organizationInvitation.email, email); @@ -118,6 +127,9 @@ module('Acceptance | Team Creation', function (hooks) { // when await clickByName(inviteButton); + await waitForDialog(); + await clickByName(confirmButton); + // then assert.ok(screen.getByText(t('pages.team-new.success.multiple-invitations'))); }); @@ -182,6 +194,8 @@ module('Acceptance | Team Creation', function (hooks) { // when await clickByName(inviteButton); + await waitForDialog(); + await clickByName(confirmButton); // then @@ -211,6 +225,8 @@ module('Acceptance | Team Creation', function (hooks) { // when await clickByName(inviteButton); + await waitForDialog(); + await clickByName(confirmButton); // then @@ -240,6 +256,8 @@ module('Acceptance | Team Creation', function (hooks) { // when await clickByName(inviteButton); + await waitForDialog(); + await clickByName(confirmButton); // then @@ -269,6 +287,8 @@ module('Acceptance | Team Creation', function (hooks) { // when await clickByName(inviteButton); + await waitForDialog(); + await clickByName(confirmButton); // then @@ -302,6 +322,8 @@ module('Acceptance | Team Creation', function (hooks) { // When await clickByName(inviteButton); + await waitForDialog(); + await clickByName(confirmButton); // Then const expectedErrorMessage = t('pages.team-new.errors.sending-email-to-invalid-email-address', { diff --git a/orga/tests/integration/components/team/invite-form-test.js b/orga/tests/integration/components/team/invite-form-test.js index e445d0dd316..2c99dee6be7 100644 --- a/orga/tests/integration/components/team/invite-form-test.js +++ b/orga/tests/integration/components/team/invite-form-test.js @@ -1,10 +1,12 @@ import { fillByLabel, render } from '@1024pix/ember-testing-library'; +import { click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { t } from 'ember-intl/test-support'; import { module, test } from 'qunit'; import sinon from 'sinon'; import setupIntlRenderingTest from '../../../helpers/setup-intl-rendering'; +import { waitForDialog } from '../../../helpers/wait-for'; module('Integration | Component | Team::InviteForm', function (hooks) { setupIntlRenderingTest(hooks); @@ -17,16 +19,16 @@ module('Integration | Component | Team::InviteForm', function (hooks) { test('it should contain email input and validation button', async function (assert) { // when - await render( + const screen = await render( hbs``, ); // then assert.dom('#email').exists(); assert.dom('#email').isRequired(); - assert.dom('button[type="submit"]').exists(); + assert.ok(screen.getByText(t('pages.team-new-item.invite-button'))); + assert.dom(screen.queryByRole('dialog')).doesNotExist(); }); - test('it should bind organizationInvitation properties with email form input', async function (assert) { // given this.set('email', 'toto@org.fr'); @@ -46,4 +48,79 @@ module('Integration | Component | Team::InviteForm', function (hooks) { // then assert.ok(this.updateEmail.called); }); + test('it should open confirmation modal when invite button is clicked', async function (assert) { + // given + this.set('email', 'toto@org.fr'); + const screen = await render( + hbs``, + ); + + // when + const inputLabel = `${t('pages.team-new-item.input-label')} *`; + await fillByLabel(inputLabel, 'dev@example.net'); + const inviteButton = await screen.findByRole('button', { + name: t('pages.team-new-item.invite-button'), + }); + + await click(inviteButton); + await waitForDialog(); + + // then + assert.dom(screen.getByRole('dialog')).isVisible(); + }); + test('it should display error message when no email is in input and invite button is clicked', async function (assert) { + // given + this.set('email', ''); + const errorMessage = t('pages.team-new.errors.mandatory-email-field'); + const screen = await render( + hbs``, + ); + + // when + const inputLabel = `${t('pages.team-new-item.input-label')} *`; + await fillByLabel(inputLabel, ''); + const inviteButton = await screen.findByRole('button', { + name: t('pages.team-new-item.invite-button'), + }); + + await click(inviteButton); + + // then + assert.dom(await screen.findByText(errorMessage)).exists(); + assert.dom(screen.queryByRole('dialog')).doesNotExist(); + }); + test('it should display error message when email format is not correct', async function (assert) { + // given + this.set('email', 'toto@org.fr;alex-mail.incorrect'); + const errorMessage = t('pages.team-new.errors.invalid-input'); + const screen = await render( + hbs``, + ); + + // when + const inviteButton = await screen.findByRole('button', { + name: t('pages.team-new-item.invite-button'), + }); + + await click(inviteButton); + + // then + assert.dom(await screen.findByText(errorMessage)).exists(); + assert.dom(screen.queryByRole('dialog')).doesNotExist(); + }); }); diff --git a/orga/translations/en.json b/orga/translations/en.json index b09aa6ea197..4a11123fd4b 100644 --- a/orga/translations/en.json +++ b/orga/translations/en.json @@ -1669,6 +1669,8 @@ "email-requirement": "Enter here the email address of the member you want to invite.", "errors": { "default": "The service is temporarily unavailable. Please try again later.", + "invalid-input": "The data entered was not in the correct format", + "mandatory-email-field": "This field is mandatory", "sending-email-to-invalid-email-address": "Sending email to the address {email} failed. The sending server replied: \"{errorMessage}\".", "status": { "400": "The email address format is invalid.", @@ -1677,6 +1679,12 @@ "500": "Something went wrong. Please try again." } }, + "invite-form-modal": { + "confirm": "Confirm", + "question": "Do you want to continue? ", + "title": "Confirm the addition of new team members", + "warning": "Members you add will have access to participant results and campaign analytics." + }, "invited-members": "By clicking on the link provided in the invitation, the invited members will be able to create an account or log in with an existing Pix account.", "several-email-requirement": "Invite several members by separating the email addresses with commas.", "success": { diff --git a/orga/translations/fr.json b/orga/translations/fr.json index 17550491e50..a8c16a468af 100644 --- a/orga/translations/fr.json +++ b/orga/translations/fr.json @@ -1675,6 +1675,8 @@ "email-requirement": "Saisissez ici l'adresse e-mail du membre que vous souhaitez inviter.", "errors": { "default": "Le service est momentanément indisponible. Veuillez réessayer ultérieurement.", + "invalid-input": "Les données que vous avez soumises ne sont pas au bon format", + "mandatory-email-field": "Ce champ est obligatoire", "sending-email-to-invalid-email-address": "L'envoi d'e-mail pour l'adresse {email} a échoué. Le serveur d'envoi a répondu : \"{errorMessage}\"", "status": { "400": "Le format de l'adresse e-mail est incorrect.", @@ -1683,6 +1685,12 @@ "500": "Quelque chose s'est mal passé. Veuillez réessayer." } }, + "invite-form-modal": { + "confirm": "Valider", + "question": "Voulez-vous continuer ? ", + "title": "Confirmer l’ajout de nouveaux membres dans l'équipe", + "warning": "Les membres que vous ajoutez auront accès aux résultats des participants et à l’analyse des campagnes." + }, "invited-members": "À la réception de l'e-mail, les invités pourront choisir de se créer un compte Pix ou de se connecter avec un compte Pix existant.", "several-email-requirement": "Vous pouvez inviter plusieurs membres en séparant les adresses e-mails par des virgules.", "success": { diff --git a/orga/translations/nl.json b/orga/translations/nl.json index 7ca3a148e95..c83a6550c39 100644 --- a/orga/translations/nl.json +++ b/orga/translations/nl.json @@ -1673,6 +1673,8 @@ "email-requirement": "Voer hier het e-mailadres in van het lid dat je wilt uitnodigen.", "errors": { "default": "De service is tijdelijk niet beschikbaar. Probeer het later nog eens.", + "invalid-input": "The data entered was not in the correct format", + "mandatory-email-field": "This field is mandatory", "sending-email-to-invalid-email-address": "Het verzenden van e-mail naar adres {email} is mislukt. De verzendende server antwoordde: \"{errorMessage}\".", "status": { "400": "De indeling van het e-mailadres is onjuist.", @@ -1682,6 +1684,12 @@ } }, "invited-members": "Als gasten de e-mail ontvangen, kunnen ze kiezen of ze een Pix-account willen aanmaken of willen inloggen met een bestaand Pix-account.", + "invite-form-modal": { + "confirm": "Confirm", + "question": "Do you want to continue? ", + "title": "Confirm the addition of new team members", + "warning": "Members you add will have access to participant results and campaign analytics." + }, "several-email-requirement": "Je kunt meerdere leden uitnodigen door e-mailadressen te scheiden met komma's.", "success": { "invitation": "Er is een uitnodiging verstuurd naar het e-mailadres {email}.",