From 4e0565464cdbc53a12c19f29a99d912fddbb4d0d Mon Sep 17 00:00:00 2001 From: Xavier Carron <33637571+xav-car@users.noreply.github.com> Date: Fri, 27 Sep 2024 18:01:18 +0200 Subject: [PATCH] feat(admin): add page to update organization import format --- admin/app/adapters/campaigns-import.js | 10 -- admin/app/adapters/import-files.js | 14 +++ .../campaigns/campaigns-import.gjs | 2 +- admin/app/components/administration/nav.gjs | 4 + .../administration/organizations/index.gjs | 3 + .../update-organization-import-format.gjs | 57 +++++++++ admin/app/router.js | 1 + .../administration/organizations.hbs | 1 + admin/mirage/handlers/badge-criteria.js | 2 - .../campaigns/campaigns-import-test.gjs | 2 +- ...update-organization-import-format-test.gjs | 108 ++++++++++++++++++ .../users/campaign-participations-test.gjs | 2 +- .../tests/unit/adapters/import-files-test.js | 22 +++- admin/translations/en.json | 11 ++ admin/translations/fr.json | 11 ++ 15 files changed, 234 insertions(+), 16 deletions(-) delete mode 100644 admin/app/adapters/campaigns-import.js create mode 100644 admin/app/components/administration/organizations/index.gjs create mode 100644 admin/app/components/administration/organizations/update-organization-import-format.gjs create mode 100644 admin/app/templates/authenticated/administration/organizations.hbs create mode 100644 admin/tests/integration/components/administration/organizations/update-organization-import-format-test.gjs diff --git a/admin/app/adapters/campaigns-import.js b/admin/app/adapters/campaigns-import.js deleted file mode 100644 index d4e7976e64c..00000000000 --- a/admin/app/adapters/campaigns-import.js +++ /dev/null @@ -1,10 +0,0 @@ -import ApplicationAdapter from './application'; - -export default class CampaignsImportAdapter extends ApplicationAdapter { - addCampaignsCsv(files) { - if (!files || files.length === 0) return; - - const url = `${this.host}/${this.namespace}/admin/campaigns`; - return this.ajax(url, 'POST', { data: files[0] }); - } -} diff --git a/admin/app/adapters/import-files.js b/admin/app/adapters/import-files.js index e2a6ec4783e..eeab7d2c596 100644 --- a/admin/app/adapters/import-files.js +++ b/admin/app/adapters/import-files.js @@ -9,4 +9,18 @@ export default class ImportFilesAdapter extends ApplicationAdapter { const url = `${this.host}/${this.namespace}/campaigns/archive-campaigns`; return this.ajax(url, 'POST', { data: files[0] }); } + + updateOrganizationImportFormat(files) { + if (!files || files.length === 0) return; + + const url = `${this.host}/${this.namespace}/import-organization-learners-format`; + return this.ajax(url, 'POST', { data: files[0] }); + } + + addCampaignsCsv(files) { + if (!files || files.length === 0) return; + + const url = `${this.host}/${this.namespace}/campaigns`; + return this.ajax(url, 'POST', { data: files[0] }); + } } diff --git a/admin/app/components/administration/campaigns/campaigns-import.gjs b/admin/app/components/administration/campaigns/campaigns-import.gjs index 82051cea956..3398db471a6 100644 --- a/admin/app/components/administration/campaigns/campaigns-import.gjs +++ b/admin/app/components/administration/campaigns/campaigns-import.gjs @@ -16,7 +16,7 @@ export default class CampaignsImport extends Component { async importCampaigns(files) { this.notifications.clearAll(); - const adapter = this.store.adapterFor('campaigns-import'); + const adapter = this.store.adapterFor('import-files'); try { await adapter.addCampaignsCsv(files); this.notifications.success(this.intl.t('components.administration.campaigns-import.notifications.success')); diff --git a/admin/app/components/administration/nav.gjs b/admin/app/components/administration/nav.gjs index e955e79c4e0..3a7e0d60fac 100644 --- a/admin/app/components/administration/nav.gjs +++ b/admin/app/components/administration/nav.gjs @@ -12,6 +12,10 @@ import { t } from 'ember-intl'; {{t "pages.administration.navigation.campaigns.label"}} + + {{t "pages.administration.navigation.organizations.label"}} + + {{t "pages.administration.navigation.certification.label"}} diff --git a/admin/app/components/administration/organizations/index.gjs b/admin/app/components/administration/organizations/index.gjs new file mode 100644 index 00000000000..a6a18df4e7c --- /dev/null +++ b/admin/app/components/administration/organizations/index.gjs @@ -0,0 +1,3 @@ +import UpdateOrganizationImportFormat from './update-organization-import-format'; + + diff --git a/admin/app/components/administration/organizations/update-organization-import-format.gjs b/admin/app/components/administration/organizations/update-organization-import-format.gjs new file mode 100644 index 00000000000..c87e1e7fb1d --- /dev/null +++ b/admin/app/components/administration/organizations/update-organization-import-format.gjs @@ -0,0 +1,57 @@ +import PixButtonUpload from '@1024pix/pix-ui/components/pix-button-upload'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { t } from 'ember-intl'; + +import AdministrationBlockLayout from '../block-layout'; + +export default class UpdateOrganizationImportFormat extends Component { + @service intl; + @service notifications; + @service router; + @service store; + + @action + async uploadOrganizationImportFile(files) { + this.notifications.clearAll(); + const adapter = this.store.adapterFor('import-files'); + try { + await adapter.updateOrganizationImportFormat(files); + this.notifications.success( + this.intl.t('components.administration.organization-import-format.notifications.success'), + ); + } catch (errorResponse) { + const errors = errorResponse.errors; + if (!errors) { + return this.notifications.error(this.intl.t('common.notifications.generic-error')); + } + + errors.forEach((error) => { + switch (error.code) { + case 'MISSING_REQUIRED_FIELD_NAMES': + this.notifications.error(`${error.meta}`, { autoClear: false }); + break; + default: + this.notifications.error(error.detail, { autoClear: false }); + } + }); + } finally { + this.isLoading = false; + } + } + +} diff --git a/admin/app/router.js b/admin/app/router.js index 243c31336f1..5ee9137d2c8 100644 --- a/admin/app/router.js +++ b/admin/app/router.js @@ -141,6 +141,7 @@ Router.map(function () { this.route('administration', function () { this.route('common'); this.route('campaigns'); + this.route('organizations'); this.route('certification'); this.route('deployment'); this.route('access'); diff --git a/admin/app/templates/authenticated/administration/organizations.hbs b/admin/app/templates/authenticated/administration/organizations.hbs new file mode 100644 index 00000000000..2eef8f4c678 --- /dev/null +++ b/admin/app/templates/authenticated/administration/organizations.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/mirage/handlers/badge-criteria.js b/admin/mirage/handlers/badge-criteria.js index e5b798526ef..03f00aefb7e 100644 --- a/admin/mirage/handlers/badge-criteria.js +++ b/admin/mirage/handlers/badge-criteria.js @@ -3,8 +3,6 @@ function updateBadgeCriterion(schema, request) { const badgeCriterionId = request.params.id; const params = JSON.parse(request.requestBody); - console.log(schema); - const badgeCriterion = schema.badgeCriteria.find(badgeCriterionId); badgeCriterion.update(params.data.attributes); diff --git a/admin/tests/integration/components/administration/campaigns/campaigns-import-test.gjs b/admin/tests/integration/components/administration/campaigns/campaigns-import-test.gjs index 7463ed8c9ca..c800a9be63c 100644 --- a/admin/tests/integration/components/administration/campaigns/campaigns-import-test.gjs +++ b/admin/tests/integration/components/administration/campaigns/campaigns-import-test.gjs @@ -16,7 +16,7 @@ module('Integration | Component | administration/campaigns-import', function (h let store, adapter, notificationSuccessStub, clearAllStub, saveAdapterStub, notificationErrorStub; hooks.beforeEach(function () { store = this.owner.lookup('service:store'); - adapter = store.adapterFor('campaigns-import'); + adapter = store.adapterFor('import-files'); saveAdapterStub = sinon.stub(adapter, 'addCampaignsCsv'); notificationSuccessStub = sinon.stub(); notificationErrorStub = sinon.stub().returns(); diff --git a/admin/tests/integration/components/administration/organizations/update-organization-import-format-test.gjs b/admin/tests/integration/components/administration/organizations/update-organization-import-format-test.gjs new file mode 100644 index 00000000000..0c5622c3bb1 --- /dev/null +++ b/admin/tests/integration/components/administration/organizations/update-organization-import-format-test.gjs @@ -0,0 +1,108 @@ +import { render } from '@1024pix/ember-testing-library'; +import Service from '@ember/service'; +import { triggerEvent } from '@ember/test-helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { t } from 'ember-intl/test-support'; +import UpdateOrganizationImportFormat from 'pix-admin/components/administration/organizations/update-organization-import-format'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; + +import setupIntlRenderingTest from '../../../../helpers/setup-intl-rendering'; + +module('Integration | Component | administration/update-organization-import-format', function (hooks) { + setupIntlRenderingTest(hooks); + setupMirage(hooks); + + let store, adapter, notificationSuccessStub, clearAllStub, saveAdapterStub, notificationErrorStub; + hooks.beforeEach(function () { + store = this.owner.lookup('service:store'); + adapter = store.adapterFor('import-files'); + saveAdapterStub = sinon.stub(adapter, 'updateOrganizationImportFormat'); + notificationSuccessStub = sinon.stub(); + notificationErrorStub = sinon.stub().returns(); + + clearAllStub = sinon.stub(); + }); + + module('when import succeeds', function () { + test('it displays a success notification', async function (assert) { + // given + const files = Symbol('file'); + class NotificationsStub extends Service { + success = notificationSuccessStub; + error = notificationErrorStub; + clearAll = clearAllStub; + } + this.owner.register('service:notifications', NotificationsStub); + saveAdapterStub.withArgs([files]).resolves(); + + // when + const screen = await render(); + const input = await screen.findByLabelText( + t('components.administration.organization-import-format.upload-button'), + ); + await triggerEvent(input, 'change', { files: [files] }); + + // then + assert.ok(true); + assert.ok(notificationErrorStub.notCalled); + assert.ok( + notificationSuccessStub.calledWith( + t('components.administration.organization-import-format.notifications.success'), + ), + ); + }); + }); + + module('when import fails', function () { + test('it displays a specific error notification on missing required field', async function (assert) { + // given + const files = Symbol('file'); + class NotificationsStub extends Service { + error = notificationErrorStub; + success = notificationSuccessStub; + clearAll = clearAllStub; + } + saveAdapterStub.withArgs([files]).rejects({ + errors: [{ status: '422', meta: 'POUET', code: 'MISSING_REQUIRED_FIELD_NAMES' }], + }); + this.owner.register('service:notifications', NotificationsStub); + + // when + const screen = await render(); + const input = await screen.findByLabelText( + t('components.administration.organization-import-format.upload-button'), + ); + await triggerEvent(input, 'change', { files: [files] }); + + // then + assert.ok(notificationSuccessStub.notCalled); + assert.ok(notificationErrorStub.calledWithExactly('POUET', { autoClear: false })); + }); + + test('it displays an error notification', async function (assert) { + // given + const files = Symbol('file'); + class NotificationsStub extends Service { + error = notificationErrorStub; + success = notificationSuccessStub; + clearAll = clearAllStub; + } + saveAdapterStub.withArgs([files]).rejects({ + errors: [{ status: '422', title: "Un soucis avec l'import", code: '422', detail: 'Erreur d’import' }], + }); + this.owner.register('service:notifications', NotificationsStub); + + // when + const screen = await render(); + const input = await screen.findByLabelText( + t('components.administration.organization-import-format.upload-button'), + ); + await triggerEvent(input, 'change', { files: [files] }); + + // then + assert.ok(notificationSuccessStub.notCalled); + assert.ok(notificationErrorStub.called); + }); + }); +}); diff --git a/admin/tests/integration/components/users/campaign-participations-test.gjs b/admin/tests/integration/components/users/campaign-participations-test.gjs index 6aa89ff0407..9d1d49c5b18 100644 --- a/admin/tests/integration/components/users/campaign-participations-test.gjs +++ b/admin/tests/integration/components/users/campaign-participations-test.gjs @@ -62,7 +62,7 @@ module('Integration | Component | users | campaign-participation', function (hoo assert.dom(screen.getByRole('link', { name: 'SOMECODE' })).exists(); }); - test('it should display orgnaization learner information', async function (assert) { + test('it should display organization learner information', async function (assert) { // given const participation = EmberObject.create({ organizationLearnerFullName: 'Un nom bien long', diff --git a/admin/tests/unit/adapters/import-files-test.js b/admin/tests/unit/adapters/import-files-test.js index c82ea84a052..55393af4d0c 100644 --- a/admin/tests/unit/adapters/import-files-test.js +++ b/admin/tests/unit/adapters/import-files-test.js @@ -12,7 +12,7 @@ module('Unit | Adapter | ImportFiles', function (hooks) { }); module('#importCampaignsToArchive', function () { - test('should build importCampaignsToArchive url from organizationId', async function (assert) { + test('should build importCampaignsToArchive url', async function (assert) { // when await adapter.importCampaignsToArchive([Symbol()]); @@ -20,4 +20,24 @@ module('Unit | Adapter | ImportFiles', function (hooks) { assert.ok(adapter.ajax.calledWith('http://localhost:3000/api/admin/campaigns/archive-campaigns', 'POST')); }); }); + + module('#updateOrganizationImportFormat', function () { + test('should build updateOrganizationImportFormat url', async function (assert) { + // when + await adapter.updateOrganizationImportFormat([Symbol()]); + + // then + assert.ok(adapter.ajax.calledWith('http://localhost:3000/api/admin/import-organization-learners-format', 'POST')); + }); + }); + + module('#addCampaignsCsv', function () { + test('should build addCampaignsCsv url', async function (assert) { + // when + await adapter.addCampaignsCsv([Symbol()]); + + // then + assert.ok(adapter.ajax.calledWith('http://localhost:3000/api/admin/campaigns', 'POST')); + }); + }); }); diff --git a/admin/translations/en.json b/admin/translations/en.json index e80a03e076b..0e4001ddb73 100644 --- a/admin/translations/en.json +++ b/admin/translations/en.json @@ -68,6 +68,14 @@ }, "upload-button": "Import JSON file" }, + "organization-import-format": { + "title": "Update existing import formats", + "description": "Only existing format imports will be updated. If the name does not match, no format import will be created.", + "notifications": { + "success": "Format imports are updated correctly" + }, + "upload-button": "Importing a JSON file" + }, "organization-tags-import": { "title": "Bulk addition of tags on organisations", "description": "Allow to add tags to organisations. Each line of the CSV file must be made of the ID of an organisation, followed by the name of a tag.", @@ -407,6 +415,9 @@ }, "deployment": { "label": "Deployment" + }, + "organizations": { + "label": "Organizations" } } }, diff --git a/admin/translations/fr.json b/admin/translations/fr.json index 24b625b7949..eee1c067d37 100644 --- a/admin/translations/fr.json +++ b/admin/translations/fr.json @@ -76,6 +76,14 @@ }, "upload-button": "Importer un fichier JSON" }, + "organization-import-format": { + "title": "Mise à jour d'import à format existants", + "description": "Seul les imports à format existants seront mis à jour. Dans le cas où le nom ne correspond pas, il n'y aura pas de création d'import à format.", + "notifications": { + "success": "Les imports à formats sont bien mis à jour" + }, + "upload-button": "Importer un fichier JSON" + }, "organization-tags-import": { "title": "Ajout de tags en masse sur des organisations", "description": "Permet d’ajouter des tags à des organisations. Chaque ligne du fichier CSV doit être constituée de l’ID d’une organisation, suivie du nom d’un tag.", @@ -417,6 +425,9 @@ }, "deployment": { "label": "Déploiement" + }, + "organizations": { + "label": "Organisations" } } },