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}}
+
+ <: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}.",