From f50ee3bb0571944c5ede25732bca652c13582178 Mon Sep 17 00:00:00 2001 From: dianeCdrPix Date: Mon, 24 Feb 2025 11:59:53 +0100 Subject: [PATCH 1/6] feat(admin): add duplicate training button --- .../components/trainings/training-details-card.scss | 6 ++++++ .../templates/authenticated/trainings/training.hbs | 4 ++++ .../authenticated/trainings/training-test.js | 12 ++++++++++++ 3 files changed, 22 insertions(+) diff --git a/admin/app/styles/components/trainings/training-details-card.scss b/admin/app/styles/components/trainings/training-details-card.scss index ef97b45ece9..9bf1b290a5d 100644 --- a/admin/app/styles/components/trainings/training-details-card.scss +++ b/admin/app/styles/components/trainings/training-details-card.scss @@ -32,4 +32,10 @@ width: 100%; height: 100%; } + + &__actions { + display: flex; + flex-wrap: wrap; + gap: 16px; + } } diff --git a/admin/app/templates/authenticated/trainings/training.hbs b/admin/app/templates/authenticated/trainings/training.hbs index 5d8f17154fe..0ba511e15a3 100644 --- a/admin/app/templates/authenticated/trainings/training.hbs +++ b/admin/app/templates/authenticated/trainings/training.hbs @@ -35,8 +35,12 @@ {{else}} {{#if this.canEdit}} +
Modifier + Dupliquer ce contenu formatif + +
{{/if}} {{/if}} diff --git a/admin/tests/acceptance/authenticated/trainings/training-test.js b/admin/tests/acceptance/authenticated/trainings/training-test.js index 30e110d3570..ce2956f46bc 100644 --- a/admin/tests/acceptance/authenticated/trainings/training-test.js +++ b/admin/tests/acceptance/authenticated/trainings/training-test.js @@ -173,6 +173,18 @@ module('Acceptance | Trainings | Training', function (hooks) { // then assert.dom(screen.getByRole('heading', { name: 'Mon titre interne' })).exists(); }); + + test('should be possible to duplicate displayed training', async function (assert) { + // given + await authenticateAdminMemberWithRole({ isSuperAdmin: true })(server); + await visit(`/trainings/${trainingId}`); + + // when + const duplicateButton = await screen.getByRole('button', { name: 'Dupliquer ce contenu formatif' }); + + // then + assert.dom(duplicateButton).exists(); + }); }); module('when admin role is "SUPPORT', function () { From c6ece1839eb4ce5b02b75c602a930b1757339388 Mon Sep 17 00:00:00 2001 From: dianeCdrPix Date: Wed, 26 Feb 2025 16:40:02 +0100 Subject: [PATCH 2/6] feat(admin): add traduction for training duplicate modal --- admin/translations/fr.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/admin/translations/fr.json b/admin/translations/fr.json index 6a3cc04e458..e7b23c15a37 100644 --- a/admin/translations/fr.json +++ b/admin/translations/fr.json @@ -848,6 +848,18 @@ "status": "Statut :", "title": "Titre public :" }, + "duplicate": { + "button": { + "label": "Dupliquer ce contenu formatif" + }, + "modal": { + "instruction": "Cette action dupliquera le contenu formatif avec ses déclencheurs.", + "title": "Dupliquer le contenu formatif ?" + }, + "notifications": { + "success": "Le contenu formatif a bien été dupliqué !" + } + }, "list": { "caption": "Liste des contenus formatifs", "goalThreshold": "Objectif à ne pas dépasser", From 865ff52096ffe467d280ca4701ef057a9b0fd51f Mon Sep 17 00:00:00 2001 From: dianeCdrPix Date: Mon, 24 Feb 2025 16:24:12 +0100 Subject: [PATCH 3/6] feat(admin): create duplicate-training component with button and modal --- .../trainings/duplicate-training.gjs | 50 +++++++++++++++++++ .../authenticated/trainings/training.js | 10 ++++ .../authenticated/trainings/training.hbs | 7 ++- .../authenticated/trainings/training-test.js | 12 ++++- .../trainings/duplicate-training-test.gjs | 38 ++++++++++++++ 5 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 admin/app/components/trainings/duplicate-training.gjs create mode 100644 admin/tests/integration/components/trainings/duplicate-training-test.gjs diff --git a/admin/app/components/trainings/duplicate-training.gjs b/admin/app/components/trainings/duplicate-training.gjs new file mode 100644 index 00000000000..70049deca6e --- /dev/null +++ b/admin/app/components/trainings/duplicate-training.gjs @@ -0,0 +1,50 @@ +import PixButton from '@1024pix/pix-ui/components/pix-button'; +import PixModal from '@1024pix/pix-ui/components/pix-modal'; +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { t } from 'ember-intl'; + +export default class DuplicateTraining extends Component { + @tracked showModal = false; + + @action + closeModal() { + this.showModal = false; + } + + @action + openModal() { + this.showModal = true; + } + + @action + validateDuplication() { + this.args.onSubmit(); + this.showModal = false; + } + + +} diff --git a/admin/app/controllers/authenticated/trainings/training.js b/admin/app/controllers/authenticated/trainings/training.js index f004243e0ac..a668afc10e4 100644 --- a/admin/app/controllers/authenticated/trainings/training.js +++ b/admin/app/controllers/authenticated/trainings/training.js @@ -31,4 +31,14 @@ export default class Training extends Controller { this.pixToast.sendErrorNotification({ message: 'Une erreur est survenue.' }); } } + + @action + triggerDuplication() { + this.showModal = true; + } + + @action + closeModal() { + this.showModal = false; + } } diff --git a/admin/app/templates/authenticated/trainings/training.hbs b/admin/app/templates/authenticated/trainings/training.hbs index 0ba511e15a3..d6e0e6cae93 100644 --- a/admin/app/templates/authenticated/trainings/training.hbs +++ b/admin/app/templates/authenticated/trainings/training.hbs @@ -38,8 +38,11 @@
Modifier - Dupliquer ce contenu formatif - + +
{{/if}} {{/if}} diff --git a/admin/tests/acceptance/authenticated/trainings/training-test.js b/admin/tests/acceptance/authenticated/trainings/training-test.js index ce2956f46bc..3408a2f151b 100644 --- a/admin/tests/acceptance/authenticated/trainings/training-test.js +++ b/admin/tests/acceptance/authenticated/trainings/training-test.js @@ -180,10 +180,18 @@ module('Acceptance | Trainings | Training', function (hooks) { await visit(`/trainings/${trainingId}`); // when - const duplicateButton = await screen.getByRole('button', { name: 'Dupliquer ce contenu formatif' }); + await clickByName('Dupliquer ce contenu formatif'); + await screen.findByRole('button', { name: 'Valider' }); + await clickByName('Valider'); // then - assert.dom(duplicateButton).exists(); + assert.strictEqual(currentURL(), '/trainings/3/details'); + const title = await screen.findByRole('heading', { + name: `[Copie] Apprendre à piloter des chauves-souris comme Batman`, + level: 1, + }); + assert.dom(title).exists(); + assert.strictEqual(currentURL(), '/trainings/3/triggers'); }); }); diff --git a/admin/tests/integration/components/trainings/duplicate-training-test.gjs b/admin/tests/integration/components/trainings/duplicate-training-test.gjs new file mode 100644 index 00000000000..9d5ea098fdf --- /dev/null +++ b/admin/tests/integration/components/trainings/duplicate-training-test.gjs @@ -0,0 +1,38 @@ +import { clickByText, render } from '@1024pix/ember-testing-library'; +import { click } from '@ember/test-helpers'; +import DuplicateTraining from 'pix-admin/components/trainings/duplicate-training'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; + +import setupIntlRenderingTest from '../../../helpers/setup-intl-rendering'; + +module('Integration | Component | Trainings | Duplicate training', function (hooks) { + setupIntlRenderingTest(hooks); + + const duplicateTraining = sinon.stub().resolves(true); + + test('it should render the duplicate training modal', async function (assert) { + // when + const screen = await render(); + const duplicateButton = screen.getByRole('button', { name: 'Dupliquer ce contenu formatif' }); + await click(duplicateButton); + + // then + assert.dom(await screen.findByText('Dupliquer le contenu formatif ?')).exists(); + assert.dom(screen.getByText('Cette action dupliquera le contenu formatif avec ses déclencheurs.')).exists(); + assert.dom(await screen.findByRole('button', { name: 'Valider' })).exists(); + assert.dom(screen.getByRole('button', { name: 'Annuler' })).exists(); + assert.dom(screen.getByRole('button', { name: 'Fermer' })).exists(); + }); + + test('it should call the duplicate method on click on submit', async function (assert) { + await render(); + + // when + await clickByText('Valider'); + + // then + sinon.assert.calledOnce(duplicateTraining); + assert.ok(true); + }); +}); From 929ee17d56defc7c6869dda7cb7122ea167bab05 Mon Sep 17 00:00:00 2001 From: dianeCdrPix Date: Tue, 25 Feb 2025 17:38:14 +0100 Subject: [PATCH 4/6] feat(admin): call API to duplicate training then redirect to training details --- admin/app/adapters/training.js | 5 +++++ .../authenticated/trainings/training.js | 22 +++++++++++++++---- .../authenticated/trainings/training.hbs | 14 +++++------- admin/mirage/config.js | 2 ++ admin/mirage/handlers/trainings.js | 13 +++++++++++ .../authenticated/trainings/training-test.js | 1 - admin/tests/unit/adapters/training-test.js | 15 +++++++++++++ 7 files changed, 58 insertions(+), 14 deletions(-) diff --git a/admin/app/adapters/training.js b/admin/app/adapters/training.js index b042f861223..0b8bb17534a 100644 --- a/admin/app/adapters/training.js +++ b/admin/app/adapters/training.js @@ -19,4 +19,9 @@ export default class TrainingAdapter extends ApplicationAdapter { const url = `${this.host}/${this.namespace}/trainings/${trainingId}/target-profiles/${targetProfileId}`; return this.ajax(url, 'DELETE'); } + + async duplicate(trainingId) { + const url = `${this.host}/${this.namespace}/trainings/${trainingId}/duplicate`; + return this.ajax(url, 'POST'); + } } diff --git a/admin/app/controllers/authenticated/trainings/training.js b/admin/app/controllers/authenticated/trainings/training.js index a668afc10e4..b4a44f7540b 100644 --- a/admin/app/controllers/authenticated/trainings/training.js +++ b/admin/app/controllers/authenticated/trainings/training.js @@ -4,8 +4,11 @@ import { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; export default class Training extends Controller { + @service store; @service pixToast; @service accessControl; + @service router; + @service intl; @tracked isEditMode = false; @@ -33,12 +36,23 @@ export default class Training extends Controller { } @action - triggerDuplication() { - this.showModal = true; + async duplicateTraining() { + try { + const adapter = this.store.adapterFor('training'); + const { trainingId: newTrainingId } = await adapter.duplicate(this.model.id); + this.goToNewTrainingDetails(newTrainingId); + this.pixToast.sendSuccessNotification({ + message: this.intl.t('pages.trainings.training.duplicate.notifications.success'), + }); + } catch (error) { + error.errors.forEach((apiError) => { + this.pixToast.sendErrorNotification({ message: apiError.detail }); + }); + } } @action - closeModal() { - this.showModal = false; + goToNewTrainingDetails(newTrainingId) { + this.router.transitionTo('authenticated.trainings.training', newTrainingId); } } diff --git a/admin/app/templates/authenticated/trainings/training.hbs b/admin/app/templates/authenticated/trainings/training.hbs index d6e0e6cae93..18668e7d5ad 100644 --- a/admin/app/templates/authenticated/trainings/training.hbs +++ b/admin/app/templates/authenticated/trainings/training.hbs @@ -35,15 +35,11 @@ {{else}} {{#if this.canEdit}} -
- Modifier - - - -
+
+ Modifier + + +
{{/if}} {{/if}} diff --git a/admin/mirage/config.js b/admin/mirage/config.js index 34caf87eba2..d08e9ce3b17 100644 --- a/admin/mirage/config.js +++ b/admin/mirage/config.js @@ -49,6 +49,7 @@ import { createOrUpdateTrainingTrigger, createTraining, detachTargetProfileFromTraining, + duplicateTraining, findPaginatedTrainingSummaries, getTargetProfileSummariesForTraining, getTraining, @@ -398,6 +399,7 @@ function routes() { this.post('/admin/trainings/:id/attach-target-profiles', attachTargetProfilesToTraining); this.delete('/admin/trainings/:trainingId/target-profiles/:targetProfileId', detachTargetProfileFromTraining); this.put('/admin/trainings/:id/triggers', createOrUpdateTrainingTrigger); + this.post('/admin/trainings/:id/duplicate', duplicateTraining); this.get('/admin/certifications/:id'); this.get('/admin/certifications/:id/certified-profile', (schema, request) => { diff --git a/admin/mirage/handlers/trainings.js b/admin/mirage/handlers/trainings.js index 2c112c4ac55..cd632ee82a1 100644 --- a/admin/mirage/handlers/trainings.js +++ b/admin/mirage/handlers/trainings.js @@ -134,11 +134,24 @@ function createOrUpdateTrainingTrigger(schema, request) { return trainingTrigger; } +function duplicateTraining(schema, request) { + const trainingId = request.params.id; + const training = schema.trainings.find(trainingId); + delete training.attrs.id; + + const { id } = schema.create('training', { + ...training.attrs, + internalTitle: '[Copie] ' + training.attrs.internalTitle, + }); + return { trainingId: id }; +} + export { attachTargetProfilesToTraining, createOrUpdateTrainingTrigger, createTraining, detachTargetProfileFromTraining, + duplicateTraining, findPaginatedTrainingSummaries, getTargetProfileSummariesForTraining, getTraining, diff --git a/admin/tests/acceptance/authenticated/trainings/training-test.js b/admin/tests/acceptance/authenticated/trainings/training-test.js index 3408a2f151b..c5d42e133c8 100644 --- a/admin/tests/acceptance/authenticated/trainings/training-test.js +++ b/admin/tests/acceptance/authenticated/trainings/training-test.js @@ -185,7 +185,6 @@ module('Acceptance | Trainings | Training', function (hooks) { await clickByName('Valider'); // then - assert.strictEqual(currentURL(), '/trainings/3/details'); const title = await screen.findByRole('heading', { name: `[Copie] Apprendre à piloter des chauves-souris comme Batman`, level: 1, diff --git a/admin/tests/unit/adapters/training-test.js b/admin/tests/unit/adapters/training-test.js index e41d9fe5463..7986f9e0042 100644 --- a/admin/tests/unit/adapters/training-test.js +++ b/admin/tests/unit/adapters/training-test.js @@ -44,4 +44,19 @@ module('Unit | Adapter | Training ', function (hooks) { assert.ok(true); }); }); + module('#duplicate', function () { + test('should trigger an ajax call with the right url and method', async function (assert) { + // given + const trainingId = 1; + sinon.stub(adapter, 'ajax').resolves(); + const expectedUrl = `http://localhost:3000/api/admin/trainings/${trainingId}/duplicate`; + + // when + await adapter.duplicate(trainingId); + + // then + sinon.assert.calledWith(adapter.ajax, expectedUrl, 'POST'); + assert.ok(true); + }); + }); }); From 9123f7dc1d80c80586af707edb3b7157209235ec Mon Sep 17 00:00:00 2001 From: dianeCdrPix Date: Wed, 26 Feb 2025 15:33:55 +0100 Subject: [PATCH 5/6] feat(api): transform training.duration into string in training repo for creation --- .../repositories/training-repository.js | 7 +++++ .../repositories/training-repository_test.js | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/api/src/devcomp/infrastructure/repositories/training-repository.js b/api/src/devcomp/infrastructure/repositories/training-repository.js index 3799616f956..cf6c863916c 100644 --- a/api/src/devcomp/infrastructure/repositories/training-repository.js +++ b/api/src/devcomp/infrastructure/repositories/training-repository.js @@ -133,6 +133,9 @@ async function findWithTriggersByCampaignParticipationIdAndLocale({ campaignPart async function create({ training }) { const knexConn = DomainTransaction.getConnection(); + if (typeof training.duration !== 'string') { + training.duration = _transformDurationFormat(training.duration); + } const pickedAttributes = pick(training, [ 'title', 'internalTitle', @@ -186,6 +189,10 @@ async function findPaginatedByUserId({ userId, locale, page }) { return { userRecommendedTrainings, pagination }; } +function _transformDurationFormat(durationObject) { + return `${durationObject.days ?? 0}d${durationObject.hours ?? 0}h${durationObject.minutes ?? 0}m${durationObject.seconds ?? 0}s`; +} + export { create, findPaginatedByUserId, diff --git a/api/tests/devcomp/integration/infrastructure/repositories/training-repository_test.js b/api/tests/devcomp/integration/infrastructure/repositories/training-repository_test.js index 5e58fa0086a..cd847d22b3e 100644 --- a/api/tests/devcomp/integration/infrastructure/repositories/training-repository_test.js +++ b/api/tests/devcomp/integration/infrastructure/repositories/training-repository_test.js @@ -616,6 +616,33 @@ describe('Integration | Repository | training-repository', function () { expect(createdTraining.id).to.exist; expect(createdTraining).to.deep.include({ ...training, duration: { hours: 6 } }); }); + + it('should handle other duration‘s format', async function () { + // given + const training = { + title: 'Titre du training', + internalTitle: 'Titre interne du training', + link: 'https://training-link.org', + type: 'webinaire', + duration: { + hours: 5, + minutes: 30, + }, + locale: 'fr', + editorName: 'Un ministère', + editorLogoUrl: 'https://mon-logo.svg', + }; + + // when + const createdTraining = await trainingRepository.create({ + training, + }); + + // then + expect(createdTraining).to.be.instanceOf(TrainingForAdmin); + expect(createdTraining.id).to.exist; + expect(createdTraining).to.deep.include({ ...training, duration: { hours: 5, minutes: 30 } }); + }); }); describe('#update', function () { From 304099ddc59526fd54359b53cfd4ff061b3eb39a Mon Sep 17 00:00:00 2001 From: dianeCdrPix Date: Wed, 26 Feb 2025 16:42:18 +0100 Subject: [PATCH 6/6] refactor(admin): training edit button modified to align with edit button in target-profile page --- admin/app/templates/authenticated/trainings/training.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/app/templates/authenticated/trainings/training.hbs b/admin/app/templates/authenticated/trainings/training.hbs index 18668e7d5ad..2f216be935c 100644 --- a/admin/app/templates/authenticated/trainings/training.hbs +++ b/admin/app/templates/authenticated/trainings/training.hbs @@ -36,7 +36,7 @@ {{#if this.canEdit}}
- Modifier + {{t "common.actions.edit"}}