Skip to content

Commit

Permalink
[FEATURE] Ajouter un bouton pour renvoyer l'invitation à un centre de…
Browse files Browse the repository at this point in the history
… certification dans pix admin (PIX-10018)

 #11203
  • Loading branch information
pix-service-auto-merge authored Jan 31, 2025
2 parents 17b230e + 964facc commit 54332b1
Show file tree
Hide file tree
Showing 20 changed files with 178 additions and 21 deletions.
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,
};
},
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

0 comments on commit 54332b1

Please sign in to comment.