Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Ajouter un bouton pour renvoyer l'invitation à un centre de certification dans pix admin (PIX-10018) #11203

35 changes: 25 additions & 10 deletions admin/app/components/certification-centers/invitations.gjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import PixButton from '@1024pix/pix-ui/components/pix-button';
import { fn } from '@ember/helper';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import dayjsFormat from 'ember-dayjs/helpers/dayjs-format';
import { t } from 'ember-intl';

export default class CertificationCenterInvitations extends Component {
@service intl;

get sortedCertificationCenterInvitations() {
return this.args.certificationCenterInvitations.sortBy('updatedAt').reverse();
}
Expand Down Expand Up @@ -32,16 +36,27 @@ export default class CertificationCenterInvitations extends Component {
<td>{{invitation.roleLabel}}</td>
<td>{{dayjsFormat invitation.updatedAt "DD/MM/YYYY [-] HH:mm"}}</td>
<td>
<PixButton
@size="small"
@variant="error"
class="certification-center-invitations-actions__button"
aria-label="Annuler l’invitation de {{invitation.email}}"
@triggerAction={{fn @onCancelCertificationCenterInvitation invitation}}
@iconBefore="delete"
>
Annuler l’invitation
</PixButton>
<div class="certification-center-invitations__actions-buttons">
<PixButton
@size="small"
class="certification-center-invitations-actions__button"
aria-label={{t "common.invitations.send-new-label" invitationEmail=invitation.email}}
@triggerAction={{fn @onSendNewCertificationCenterInvitation invitation}}
@iconBefore="refresh"
>
{{t "common.invitations.send-new"}}
</PixButton>
<PixButton
@size="small"
@variant="error"
class="certification-center-invitations-actions__button"
aria-label="Annuler l’invitation de {{invitation.email}}"
@triggerAction={{fn @onCancelCertificationCenterInvitation invitation}}
@iconBefore="delete"
>
Annuler l’invitation
</PixButton>
</div>
</td>
</tr>
{{/each}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import isEmailValid from '../../../../utils/email-validator';

export default class AuthenticatedCertificationCentersGetInvitationsController extends Controller {
@service accessControl;
@service intl;
@service pixToast;
@service errorResponseHandler;
@service store;
Expand All @@ -26,10 +27,8 @@ export default class AuthenticatedCertificationCentersGetInvitationsController e

@action
async createInvitation(language, role) {
this.isLoading = true;
const email = this.userEmailToInvite?.trim();
if (!this._isEmailToInviteValid(email)) {
this.isLoading = false;
return;
}

Expand All @@ -46,7 +45,25 @@ export default class AuthenticatedCertificationCentersGetInvitationsController e
} catch (err) {
this.errorResponseHandler.notify(err, this.CUSTOM_ERROR_MESSAGES);
}
this.isLoading = false;
}

@action
async sendNewCertificationCenterInvitation(certificationCenterInvitation) {
const { email, language, role } = certificationCenterInvitation;
try {
await this.store.queryRecord('certification-center-invitation', {
email,
language,
role,
certificationCenterId: this.model.certificationCenterId,
});

this.pixToast.sendSuccessNotification({
message: this.intl.t('common.invitations.send-new-confirm', { invitationEmail: email }),
});
} catch (err) {
this.errorResponseHandler.notify(err, this.CUSTOM_ERROR_MESSAGES);
}
}

_isEmailToInviteValid(email) {
Expand Down
1 change: 1 addition & 0 deletions admin/app/models/certification-center-invitation.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default class CertificationCenterInvitationModel extends Model {
@attr email;
@attr updatedAt;
@attr role;
@attr language;

@belongsTo('certification-center', { async: true, inverse: null }) certificationCenter;

Expand Down
14 changes: 14 additions & 0 deletions admin/app/styles/components/certification-center-invitations.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@
text-align: center;
}

&__actions-buttons {
display: flex;
flex-direction: column;

button {
width: 13rem;
margin: 0.2rem;
}

svg {
margin-right: 6px;
}
}

&-actions__button {

svg {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@

<CertificationCenters::Invitations
@certificationCenterInvitations={{this.model.certificationCenterInvitations}}
@onSendNewCertificationCenterInvitation={{this.sendNewCertificationCenterInvitation}}
@onCancelCertificationCenterInvitation={{this.cancelCertificationCenterInvitation}}
/>
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { render } from '@1024pix/ember-testing-library';
import { setupIntl } from 'ember-intl/test-support';
import { setupRenderingTest } from 'ember-qunit';
import Invitations from 'pix-admin/components/certification-centers/invitations';
import { module, test } from 'qunit';
import sinon from 'sinon';

module('Integration | Component | Certification Centers | Invitations', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks, 'fr');

module('when there is no certification center invitations', function () {
test('should show "Aucune invitation en attente"', async function (assert) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,58 @@ module('Unit | Controller | authenticated/certification-centers/get/invitations'
sinon.assert.calledWith(notificationErrorStub, { message: 'Une erreur s’est produite, veuillez réessayer.' });
assert.ok(true);
});

module('#sendNewInvitation', function () {
test('It sends a new invitation', async function (assert) {
// given
const controller = this.owner.lookup('controller:authenticated/certification-centers/get/invitations');

const store = this.owner.lookup('service:store');
const queryRecordStub = sinon.stub();
store.queryRecord = queryRecordStub;
const certificationCenterInvitation = {
email: '[email protected]',
language: 'en',
role: 'member',
certificationCenterId: 1,
};
// when
await controller.sendNewCertificationCenterInvitation(certificationCenterInvitation);

// then
assert.ok(
queryRecordStub.calledWith('certification-center-invitation', {
...certificationCenterInvitation,
}),
);
});

test('When an error occurs, it should send a notification error', async function (assert) {
// given
const controller = this.owner.lookup('controller:authenticated/certification-centers/get/invitations');
const store = this.owner.lookup('service:store');
const anError = Symbol('an error');
store.queryRecord = sinon.stub().rejects(anError);
const notifyStub = sinon.stub();
class ErrorResponseHandler extends Service {
notify = notifyStub;
}
this.owner.register('service:error-response-handler', ErrorResponseHandler);
const customErrors = Symbol('custom errors');
controller.CUSTOM_ERROR_MESSAGES = customErrors;
const certificationCenterInvitation = {
email: '[email protected]',
language: 'en',
role: 'member',
certificationCenterId: 1,
};

// when
await controller.sendNewCertificationCenterInvitation(certificationCenterInvitation);

// then
assert.ok(notifyStub.calledWithExactly(anError, customErrors));
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const buildCertificationCenterInvitation = function ({
email = '[email protected]',
status = 'pending',
role = 'MEMBER',
locale = 'fr',
code = 'ABCDEF123',
createdAt = new Date(),
updatedAt = new Date(),
Expand All @@ -18,6 +19,7 @@ const buildCertificationCenterInvitation = function ({
email,
status,
role,
locale,
code,
createdAt,
updatedAt,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const TABLE_NAME = 'certification-center-invitations';
const COLUMN_NAME = 'locale';

const up = async function (knex) {
await knex.schema.table(TABLE_NAME, function (table) {
table.string(COLUMN_NAME).defaultTo('fr');
});
};

const down = async function (knex) {
await knex.schema.table(TABLE_NAME, function (table) {
table.dropColumn(COLUMN_NAME);
});
};

export { down, up };
17 changes: 15 additions & 2 deletions api/src/team/domain/models/CertificationCenterInvitation.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const validationScheme = Joi.object({
role: Joi.string()
.valid(...Object.values(roles))
.optional(),
locale: Joi.string().optional(),
updatedAt: Joi.date().optional(),
status: Joi.string()
.valid(...Object.values(statuses))
Expand All @@ -34,11 +35,22 @@ const validationScheme = Joi.object({
});

export class CertificationCenterInvitation {
constructor({ id, email, updatedAt, role, status, certificationCenterId, certificationCenterName, code } = {}) {
constructor({
id,
email,
updatedAt,
role,
locale,
status,
certificationCenterId,
certificationCenterName,
code,
} = {}) {
this.id = id;
this.email = email;
this.updatedAt = updatedAt;
this.role = role;
this.locale = locale;
this.status = status;
this.certificationCenterId = certificationCenterId;
this.certificationCenterName = certificationCenterName;
Expand All @@ -47,14 +59,15 @@ export class CertificationCenterInvitation {
validateEntity(validationScheme, this);
}

static create({ email, certificationCenterId, updatedAt = new Date(), code = this.generateCode(), role }) {
static create({ email, certificationCenterId, updatedAt = new Date(), code = this.generateCode(), role, locale }) {
const certificationCenterToCreate = new CertificationCenterInvitation({
email,
certificationCenterId,
status: CertificationCenterInvitation.StatusType.PENDING,
updatedAt,
code,
role,
locale,
});
delete certificationCenterToCreate.id;
delete certificationCenterToCreate.certificationCenterName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const createOrUpdateCertificationCenterInvitationForAdmin = async function ({
const shouldCreateInvitation = !alreadyExistingPendingInvitationForThisEmail;

if (shouldCreateInvitation) {
const newInvitation = CertificationCenterInvitation.create({ email, role, certificationCenterId });
const newInvitation = CertificationCenterInvitation.create({ email, role, locale, certificationCenterId });
certificationCenterInvitation = await certificationCenterInvitationRepository.create(newInvitation);
isInvitationCreated = true;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function _toDomain(invitationDTO) {
email: invitationDTO.email,
code: invitationDTO.code,
role: invitationDTO.role,
locale: invitationDTO.locale,
updatedAt: invitationDTO.updatedAt,
certificationCenterId: invitationDTO.certificationCenterId,
certificationCenterName: invitationDTO.certificationCenterName,
Expand All @@ -24,7 +25,7 @@ function _toDomain(invitationDTO) {
*/
const findPendingByCertificationCenterId = async function ({ certificationCenterId }) {
const pendingCertificationCenterInvitations = await knex(CERTIFICATION_CENTER_INVITATIONS)
.select('id', 'email', 'certificationCenterId', 'updatedAt', 'role')
.select('id', 'email', 'certificationCenterId', 'updatedAt', 'role', 'locale')
.where({ certificationCenterId, status: CertificationCenterInvitation.StatusType.PENDING })
.orderBy('updatedAt', 'desc');
return pendingCertificationCenterInvitations.map(_toDomain);
Expand Down Expand Up @@ -96,7 +97,7 @@ const findOnePendingByEmailAndCertificationCenterId = async function ({ email, c
const create = async function (invitation) {
const [newInvitation] = await knex(CERTIFICATION_CENTER_INVITATIONS)
.insert(invitation)
.returning(['id', 'email', 'code', 'certificationCenterId', 'updatedAt', 'role']);
.returning(['id', 'email', 'code', 'certificationCenterId', 'updatedAt', 'role', 'locale']);

const { name: certificationCenterName } = await knex('certification-centers')
.select('name')
Expand All @@ -115,7 +116,7 @@ const update = async function (certificationCenterInvitation) {
const [updatedCertificationCenterInvitation] = await knex('certification-center-invitations')
.update({ updatedAt: new Date() })
.where({ id: certificationCenterInvitation.id })
.returning(['id', 'email', 'code', 'certificationCenterId', 'updatedAt', 'role']);
.returning(['id', 'email', 'code', 'certificationCenterId', 'updatedAt', 'role', 'locale']);

const { name: certificationCenterName } = await knex('certification-centers')
.select('name')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CertificationCenterInvitedUser } from '../../domain/models/Certificatio

const get = async function ({ certificationCenterInvitationId, email }) {
const invitation = await knex('certification-center-invitations')
.select('id', 'certificationCenterId', 'code', 'status', 'role')
.select('id', 'certificationCenterId', 'code', 'status', 'role', 'locale')
.where({ id: certificationCenterInvitationId })
.first();
if (!invitation) {
Expand All @@ -21,6 +21,7 @@ const get = async function ({ certificationCenterInvitationId, email }) {
invitation,
status: invitation.status,
role: invitation.role,
locale: invitation.locale,
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ const serialize = function (invitations) {

const serializeForAdmin = function (invitations) {
return new Serializer('certification-center-invitations', {
attributes: ['email', 'updatedAt', 'role'],
transform: (invitation) => {
return {
...invitation,
language: invitation.locale,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oui, mais pour la partie des centres de certification, c'est language qui est utilisé, donc on s'adapte:)

P-Jeremy marked this conversation as resolved.
Show resolved Hide resolved
};
},
attributes: ['email', 'updatedAt', 'role', 'language'],
}).serialize(invitations);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ describe('Acceptance | API | Certification center invitations', function () {
attributes: {
email: updatedCertificationCenterInvitation.email,
role: updatedCertificationCenterInvitation.role,
language: updatedCertificationCenterInvitation.locale,
'updated-at': updatedCertificationCenterInvitation.updatedAt,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('Acceptance | Team | Application | Route | Admin | Certification Center
attributes: {
email: '[email protected]',
role: 'MEMBER',
language: 'fr',
'updated-at': now,
},
},
Expand All @@ -67,6 +68,7 @@ describe('Acceptance | Team | Application | Route | Admin | Certification Center
attributes: {
email: '[email protected]',
role: 'ADMIN',
language: 'fr',
'updated-at': now,
},
},
Expand Down Expand Up @@ -119,6 +121,7 @@ describe('Acceptance | Team | Application | Route | Admin | Certification Center
'updated-at': now,
email: '[email protected]',
role: 'ADMIN',
language: 'fr',
});
});
});
Expand Down
Loading