diff --git a/cypress/fixtures/sims.json b/cypress/fixtures/sims.json index d90944274..5deba54ba 100644 --- a/cypress/fixtures/sims.json +++ b/cypress/fixtures/sims.json @@ -9,6 +9,44 @@ "rangeType": "DATE", "idAttribute": "S.2.2", "value": "2019-01-01T11:00:00.000Z" + }, + { + "rangeType": "RICH_TEXT", + "idAttribute": "S.3.1", + "documents": [ + { + "uri": "uri1-bis", + "url": "http://google.fr?q=url-1", + "updatedDate": "01/01/2019", + "labelLg1": "labelLg1-0", + "labelLg2": "labelLg2-0", + "lang": "fr", + "descriptionLg1": "descriptionLg1", + "descriptionLg2": "descriptionLg2" + }, + { + "uri": "uri2-bis", + "url": "http://google.fr?q=url-2", + "updatedDate": "01/02/2019", + "labelLg1": "labelLg1-1", + "labelLg2": "labelLg2-1", + "descriptionLg1": "descriptionLg1-2", + "descriptionLg2": "descriptionLg2-2" + }, + { + "uri": "uri3-bis", + "url": "http://google.fr?q=url-2", + "labelLg1": "labelLg1-2", + "labelLg2": "labelLg2-2", + "lang": "fr", + "descriptionLg1": "descriptionLg1-2", + "descriptionLg2": "descriptionLg2-2" + } + ] + }, + { + "rangeType": "RICH_TEXT", + "idAttribute": "S.3.2" } ], "labelLg2": "Annual production survey 2012 SIMS", diff --git a/cypress/integration/family.spec.js b/cypress/integration/family.spec.js index 102c74661..73f07c336 100644 --- a/cypress/integration/family.spec.js +++ b/cypress/integration/family.spec.js @@ -22,7 +22,7 @@ describe('Family page', () => { familiesPage.go(); familiesPage.getFamiliesList().should('have.length', 10); familiesPage.getPaginationBlock().should('have.length', 7); - familiesPage.search('ZENIKA'); + familiesPage.search('FAKE DATA'); familiesPage.getFamiliesList().should('have.length', 0); familiesPage.resetSearch(); familiesPage.getFamiliesList().should('have.length', 10); diff --git a/cypress/integration/operations_list.spec.js b/cypress/integration/operations_list.spec.js index ba9a2e1dc..e04bafe08 100644 --- a/cypress/integration/operations_list.spec.js +++ b/cypress/integration/operations_list.spec.js @@ -23,7 +23,7 @@ cy.get('.list-group').should('be.visible'); cy.get('.pagination').should('be.visible'); - cy.get('input').type('Zenika'); + cy.get('input').type('FAKE DATA'); cy.get('h4').should(h4 => { expect(h4.first()).to.contain('0'); }); diff --git a/cypress/integration/po/sims.po.js b/cypress/integration/po/sims.po.js index cb373a711..e9c210c19 100644 --- a/cypress/integration/po/sims.po.js +++ b/cypress/integration/po/sims.po.js @@ -6,13 +6,17 @@ export class SimsViewPage { getUpdateButton() { return 'div:nth-child(4) > a'; } + + getDocumentsBlocForRubric(rubricId) { + return cy.get(rubricId); + } } export class SimsEditPage { getTitle() { return '.page-title-operations'; } - + p; getCancelButton() { return '.btn-line div:first > button'; } diff --git a/cypress/integration/serie.spec.js b/cypress/integration/serie.spec.js index 48297b129..80e67bd6b 100644 --- a/cypress/integration/serie.spec.js +++ b/cypress/integration/serie.spec.js @@ -138,69 +138,4 @@ describe('Series page', () => { cy.get('.row:first-of-type > div.form-group').should('have.length', 2); cy.get('label span.boldRed').should('have.length', 2); }); - - it('should handle multi Select component', () => { - cy.server() - .fixture('series') - .then(json => { - cy.route(Cypress.env('API') + 'operations/series/s1161', json); - }) - - .visit('/operations/series', { - onBeforeLoad(win) { - delete win.fetch; - win.eval(polyfill); - win.fetch = win.unfetch; - }, - }); - - cy.get('.list-group a') - .first() - .click(); - - cy.url().should('include', '/operations/series/'); - - cy.get('.btn-line a') - .first() - .click(); - - cy.url().should('include', '/modify'); - - cy.get('.Select--multi') - .eq(1) - .as('firstMultiSelect') - .get('@firstMultiSelect') - .find('.Select-multi-value-wrapper') - .children('.Select-value') - .then(children => { - cy.get('@firstMultiSelect') - .find('.Select-value') - .should('have.length', children.length); - - cy.get('@firstMultiSelect').click(); - cy.get('@firstMultiSelect').click(); - - cy.get('@firstMultiSelect') - .get('.Select-option') - .first() - .should('be.visible'); - - cy.get('@firstMultiSelect') - .get('.Select-option') - .first() - .click(); - - cy.get('@firstMultiSelect') - .find('.Select-value') - .should('have.length', children.length + 1); - - cy.get('@firstMultiSelect') - .find('.Select-value-icon') - .first() - .click(); - cy.get('@firstMultiSelect') - .find('.Select-value') - .should('have.length', children.length); - }); - }); }); diff --git a/cypress/integration/sims.spec.js b/cypress/integration/sims.spec.js index 42a165765..52aeeaafd 100644 --- a/cypress/integration/sims.spec.js +++ b/cypress/integration/sims.spec.js @@ -37,6 +37,7 @@ describe('SIMS Page', function() { // Visu Page cy.url().should('contains', '/sims/1512'); cy.get(simsViewPage.getTitle()).should('exist'); + cy.get(simsViewPage.getUpdateButton()).click(); // Update page diff --git a/docs/en/getting-started.md b/docs/en/getting-started.md index 82e48babd..640dfb9ea 100644 --- a/docs/en/getting-started.md +++ b/docs/en/getting-started.md @@ -31,3 +31,16 @@ If you're new to JavaScript, you might need to first install [node](https://node `yarn start` will launch the `dev` command defined in the `scripts` section of the same `package.json` file. This command will launch a local web server serving the main HTML file ([src/js/index.html](https://github.com/InseeFr/Bauhaus/blob/master/public/index.html)) and all the relevant assets. `yarn build` will launch the compilation with some optimizations for production. It copies all the static assets and the resulting bundle file in the `dist` folder. + +## Project Structure + +In this paragraph, we will try to explain the rules we defined and try to follow when talking about the structure of the project. + +### SCSS Mixin + +If you have to define SCSS mixin, you have to define them in the `src/styles/mixin.scss` file, and import them in the stylesheet of the React component. + +### I18N + +In order to avoid big i18n file, we try to split this file in smaller files, based on `page` or `feature`. For example, we have a `src/js/i18n/dictionary/operations/documents.js` file for all messages dedicated to the documents feature. +This files have to be imported directly or not in the main file `js/i18n/dictionary/app.js`. \ No newline at end of file diff --git a/docs/fr/getting-started.md b/docs/fr/getting-started.md index f8f2606f3..e3d74e7ec 100644 --- a/docs/fr/getting-started.md +++ b/docs/fr/getting-started.md @@ -31,3 +31,16 @@ Si vous débutez avec ces technologies, vous aurez vraisemblablement besoin d'in `yarn start` démarre un serveur de développement qui sert la page d'accueil de l'application ([src/js/index.html](https://github.com/InseeFr/Bauhaus/blob/master/public/index.html)) et toutes les ressources nécessaires. `yarn build` lance la compilation du code avec des optimisations pour la mise en production. Elle copie toutes les ressources statiques et le fichier `JavaScript` compilé dans le dossier `dist`. + +## Structure du projet + +Dans ce paragraphe, nous allons définir les quelques règles que nous essayons de suivre concernant la structure du projet. + +### Mixin SCSS + +Si vous devez définir des mixins SCSS, vous devez les implémenter dans le fichier `src/styles/mixin.scss` et ensuite importer ce fichier lorsque vous souhaitez l'utiliser. + +### I18N + +Dans le but d'éviter d'avoir un fichier d'i18n trop gros, nous avons commencer à découper ce fichier par `page` ou `fontionnalité`. Il y a par exemple un fichier `src/js/i18n/dictionary/operations/documents.js` pour tous les messages utilisés dans la fonctionnalité de gestions des documents. +Ces fichiers doivent ensuite être importés directement ou indirectement dans le fichier principal `js/i18n/dictionary/app.js` \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json index 950a3d39d..bfa9a0471 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { - "baseUrl": "./src" - } + "baseUrl": "./src", + "checkJs": true, + "jsx": "preserve" + }, + "exclude": ["cypress/**/*.js"] } diff --git a/package-lock.json b/package-lock.json index aaaef79f1..18c8dc772 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2546,6 +2546,30 @@ "unified": "6.2.0" } }, + "@types/cypress": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/cypress/-/cypress-1.1.3.tgz", + "integrity": "sha512-OXe0Gw8LeCflkG1oPgFpyrYWJmEKqYncBsD/J0r17r0ETx/TnIGDNLwXt/pFYSYuYTpzcq1q3g62M9DrfsBL4g==", + "dev": true, + "requires": { + "cypress": "3.2.0" + } + }, + "@types/jest": { + "version": "24.0.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.12.tgz", + "integrity": "sha512-60sjqMhat7i7XntZckcSGV8iREJyXXI6yFHZkSZvCPUeOnEJ/VP1rU/WpEWQ56mvoh8NhC+sfKAuJRTyGtCOow==", + "dev": true, + "requires": { + "@types/jest-diff": "20.0.1" + } + }, + "@types/jest-diff": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jest-diff/-/jest-diff-20.0.1.tgz", + "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", + "dev": true + }, "@types/node": { "version": "11.9.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.9.4.tgz", @@ -4366,9 +4390,9 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, "bootstrap": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.3.7.tgz", - "integrity": "sha1-WjiTlFSfIzMIdaOxUGVldPip63E=" + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", + "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==" }, "boundary": { "version": "1.0.1", diff --git a/package.json b/package.json index f0a0254fe..2d2184d5d 100755 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "babel-polyfill": "6.26.0", - "bootstrap": "3.3.7", + "bootstrap": "3.4.1", "dompurify": "0.8.7", "draft-js": "0.10.4", "draft-js-export-html": "1.2.0", @@ -79,6 +79,8 @@ "@storybook/addon-links": "4.1.12", "@storybook/addons": "4.1.12", "@storybook/react": "4.1.12", + "@types/cypress": "^1.1.3", + "@types/jest": "^24.0.12", "babel-loader": "8.0.5", "concurrently": "3.5.1", "coveralls": "3.0.0", diff --git a/src/app.scss b/src/app.scss index 8404ef660..8e0e1b090 100644 --- a/src/app.scss +++ b/src/app.scss @@ -1,3 +1,5 @@ +@import 'src/styles/mixin.scss'; + // Define concepts colors $color_concepts_1: #044173; $color_concepts_2: #234ca5; @@ -55,18 +57,6 @@ a { margin: 0; } -@mixin btn-group-vertical() { - border-radius: 6px; - border-width: 2px; - border-style: solid; - margin-top: 200px; - - .btn { - margin-top: 15px; - margin-bottom: 15px; - } -} - .btn-group-vertical { border-color: $color_concepts_3; @include btn-group-vertical(); diff --git a/src/js/actions/constants/operations.js b/src/js/actions/constants/operations.js index e329cb735..978a8aea8 100644 --- a/src/js/actions/constants/operations.js +++ b/src/js/actions/constants/operations.js @@ -10,3 +10,4 @@ export * from './operations/series'; export * from './operations/codeList'; export * from './operations/organisations'; export * from './operations/sims'; +export * from './operations/documents'; diff --git a/src/js/actions/constants/operations/documents.js b/src/js/actions/constants/operations/documents.js new file mode 100644 index 000000000..2690d3445 --- /dev/null +++ b/src/js/actions/constants/operations/documents.js @@ -0,0 +1,5 @@ +export const LOAD_OPERATIONS_DOCUMENTS = 'LOAD_OPERATIONS_DOCUMENTS'; +export const LOAD_OPERATIONS_DOCUMENTS_SUCCESS = + 'LOAD_OPERATIONS_DOCUMENTS_SUCCESS'; +export const LOAD_OPERATIONS_DOCUMENTS_FAILURE = + 'LOAD_OPERATIONS_DOCUMENTS_FAILURE'; diff --git a/src/js/actions/operations/documents/list.js b/src/js/actions/operations/documents/list.js new file mode 100644 index 000000000..a5fb8d92b --- /dev/null +++ b/src/js/actions/operations/documents/list.js @@ -0,0 +1,79 @@ +import api from 'js/remote-api/operations-api'; +import * as A from 'js/actions/constants'; +import { sortArray } from 'js/utils/array-utils'; + +const sortByLabel = sortArray('label'); + +export default () => dispatch => { + dispatch({ + type: A.LOAD_OPERATIONS_DOCUMENTS, + payload: {}, + }); + return api.getDocumentsList().then( + results => + dispatch({ + type: A.LOAD_OPERATIONS_DOCUMENTS_SUCCESS, + payload: { results: sortByLabel(results) }, + }), + err => { + const results = [ + { + uri: 'uri1-bis', + url: 'http://google.fr?q=url-1', + updatedDate: '01/01/2019', + lang: 'fr', + labelLg1: 'Document 1 - label en Langue 1', + labelLg2: 'Document 1 - label en Langue 2', + descriptionLg1: 'Description 1 en Langue 1', + descriptionLg2: 'Description 1 en Langue 2', + }, + { + uri: 'uri2-bis', + url: 'http://google.fr?q=url-2', + updatedDate: '01/02/2019', + labelLg1: 'Document 2 - label en Langue 1', + labelLg2: 'Document 2 - label en Langue 2', + descriptionLg1: 'Description 2 en Langue 1', + descriptionLg2: 'Description 2 en Langue 2', + }, + { + uri: 'uri3-bis', + url: 'http://google.fr?q=url-2', + labelLg1: 'Document 3 - label en Langue 1', + labelLg2: 'Document 3 - label en Langue 2', + descriptionLg1: 'Description 3 en Langue 1', + descriptionLg2: 'Description 3 en Langue 2', + lang: 'fr', + }, + + { + uri: 'uri5-bis', + url: 'http://google.fr?q=url-2', + labelLg1: 'Document 5 - label en Langue 1', + labelLg2: 'Document 5 - label en Langue 2', + descriptionLg1: 'Description 5 en Langue 1', + descriptionLg2: 'Description 5 en Langue 2', + lang: 'fr', + }, + { + uri: 'uri4-bis', + url: 'http://google.fr?q=url-2', + updatedDate: '01/02/2019', + labelLg1: 'Document 4 - label en Langue 1', + labelLg2: 'Document 4 - label en Langue 2', + descriptionLg1: 'Description 4 en Langue 1', + descriptionLg2: 'Description 4 en Langue 2', + }, + ]; + dispatch({ + type: A.LOAD_OPERATIONS_DOCUMENTS_SUCCESS, + payload: { results: sortByLabel(results) }, + }); + + /*dispatch({ + type: A.LOAD_OPERATIONS_DOCUMENTS_FAILURE, + payload: { err }, + })*/ + } + ); +}; diff --git a/src/js/actions/operations/families/item.js b/src/js/actions/operations/families/item.js index 0d4ef87f7..29b3747f4 100644 --- a/src/js/actions/operations/families/item.js +++ b/src/js/actions/operations/families/item.js @@ -11,7 +11,10 @@ export const saveFamily = (family, callback) => dispatch => { results => { dispatch({ type: A.SAVE_OPERATIONS_FAMILY_SUCCESS, - payload: family, + payload: { + ...family, + id: family.id ? family.id : results + }, }); callback(results); }, diff --git a/src/js/actions/operations/series/item.js b/src/js/actions/operations/series/item.js index df1f84ab6..9b8035db4 100644 --- a/src/js/actions/operations/series/item.js +++ b/src/js/actions/operations/series/item.js @@ -1,19 +1,22 @@ import api from 'js/remote-api/operations-api'; import * as A from 'js/actions/constants'; -export const saveSerie = (serie, callback) => dispatch => { +export const saveSerie = (series, callback) => dispatch => { dispatch({ type: A.SAVE_OPERATIONS_SERIE, - payload: serie, + payload: series, }); - const method = serie.id ? 'putSeries' : 'postSeries'; - return api[method](serie).then( - results => { + const method = series.id ? 'putSeries' : 'postSeries'; + return api[method](series).then( + id => { dispatch({ type: A.SAVE_OPERATIONS_SERIE_SUCCESS, - payload: serie, + payload: { + ...series, + id + }, }); - callback(results); + callback(id); }, err => { dispatch({ diff --git a/src/js/actions/operations/series/item.spec.js b/src/js/actions/operations/series/item.spec.js index c5c93c538..7a2b86913 100644 --- a/src/js/actions/operations/series/item.spec.js +++ b/src/js/actions/operations/series/item.spec.js @@ -43,7 +43,7 @@ describe('Serie actions', () => { describe('save a serie', () => { it('should call dispatch SAVE_OPERATIONS_SERIE_SUCCESS action with the udpated serie', async () => { api.putSeries = function(id) { - return Promise.resolve(''); + return Promise.resolve('1'); }; const serie = { label: 'aaa', id: '1' }; await saveSerie(serie, () => {})(dispatch); diff --git a/src/js/actions/operations/sims/item.js b/src/js/actions/operations/sims/item.js index f2de6e52d..49da9ac03 100644 --- a/src/js/actions/operations/sims/item.js +++ b/src/js/actions/operations/sims/item.js @@ -3,6 +3,18 @@ import * as A from 'js/actions/constants'; import { LOADING } from 'js/constants'; import { getLabelsFromOperation } from 'js/utils/msd'; +/** + * @typedef {Object} SimsDocuments + * @property {string} uri + * @property {string} url + * @property {string=} updatedDate + * @property {string} labelLg1 + * @property {string} labelLg2 + * @property {string=} lang + * @property {string} descriptionLg1 + * @property {string} descriptionLg2 + */ + export const saveSims = (sims, callback) => (dispatch, getState) => { let promise = Promise.resolve(sims); @@ -61,11 +73,43 @@ export default id => (dispatch, getState) => { ...results, operationsWithoutSims, rubrics: results.rubrics.reduce((acc, rubric) => { + // TO BE DELETED + const documents = [ + { + uri: 'uri1-bis', + url: 'http://google.fr?q=url-1', + updatedDate: '01/01/2019', + lang: 'fr', + labelLg1: 'Document 1 - label en Langue 1', + labelLg2: 'Document 1 - label en Langue 2', + descriptionLg1: 'Description 1 en Langue 1', + descriptionLg2: 'Description 1 en Langue 2', + }, + { + uri: 'uri2-bis', + url: 'http://google.fr?q=url-2', + updatedDate: '01/02/2019', + labelLg1: 'Document 2 - label en Langue 1', + labelLg2: 'Document 2 - label en Langue 2', + descriptionLg1: 'Description 2 en Langue 1', + descriptionLg2: 'Description 2 en Langue 2', + }, + { + uri: 'uri3-bis', + url: 'http://google.fr?q=url-2', + labelLg1: 'Document 3 - label en Langue 1', + labelLg2: 'Document 3 - label en Langue 2', + descriptionLg1: 'Description 3 en Langue 1', + descriptionLg2: 'Description 3 en Langue 2', + lang: 'fr', + }, + ]; return { ...acc, [rubric.idAttribute]: { ...rubric, idMas: rubric.idAttribute, + documents: rubric.idAttribute === 'S.3.1' ? documents : [], }, }; }, {}), diff --git a/src/js/components/administration/roles/home-container.js b/src/js/components/administration/roles/home-container.js index bcc5baba0..90122f10e 100644 --- a/src/js/components/administration/roles/home-container.js +++ b/src/js/components/administration/roles/home-container.js @@ -108,4 +108,7 @@ const mapDispatchToProps = { deleteRole, }; -export default connect(mapStateToProps, mapDispatchToProps)(RolesContainer); +export default connect( + mapStateToProps, + mapDispatchToProps +)(RolesContainer); diff --git a/src/js/components/operations/msd/documents/documents-bloc/index.js b/src/js/components/operations/msd/documents/documents-bloc/index.js new file mode 100644 index 000000000..cb76db4ac --- /dev/null +++ b/src/js/components/operations/msd/documents/documents-bloc/index.js @@ -0,0 +1,171 @@ +import React, { useState, Component } from 'react'; +import { connect } from 'react-redux'; +import { sortArray } from 'js/utils/array-utils'; +import D from 'js/i18n'; +import loadDocuments from 'js/actions/operations/documents/list'; +import './style.scss'; +import { getLang } from 'js/i18n/build-dictionary'; +import { LOADED, NOT_LOADED } from 'js/constants'; +/** + * @typedef {Object} DocumentsBlocProps + * @property {import('js/actions/operations/sims/item').SimsDocuments[]=} documents + * @property {String=} localPrefix + * @property {Boolean=} editMode + * @property {(string) => void } deleteHandler + * @property {(string) => void } addHandler + * @property {import('js/actions/operations/sims/item').SimsDocuments[]} documentStores + */ + +/** + * This component will display a list of documents associated + * to a RICH_TEXT typed rubric of a SIMS + * + * @param {DocumentsBlocProps} props + */ +export function DocumentsBloc({ + documents = [], + localPrefix = 'Lg1', + editMode = false, + deleteHandler, + addHandler, + documentStores = [], +}) { + const currentDocuments = sortArray(`label${localPrefix}`)(documents); + const currentDocumentsIds = currentDocuments.map(doc => doc.uri); + const otherDocuments = sortArray(`label${localPrefix}`)( + documentStores.filter( + document => !currentDocumentsIds.includes(document.uri) + ) + ); + const isSecondLang = localPrefix === 'Lg2'; + const [panelStatus, setPanelStatus] = useState(false); + + function addAsideToTheDocument(document) { + let updatedDate; + if (document.updatedDate) { + updatedDate = new Intl.DateTimeFormat(getLang()).format( + new Date(document.updatedDate) + ); + } + const aside = [document.lang, updatedDate].filter(val => !!val).join('-'); + return { + ...document, + aside, + }; + } + + const defaultBtnBlocFunction = document => ( + + ); + + function displayHTMLForDocument( + document, + btnBlocFunction = defaultBtnBlocFunction + ) { + return ( +
  • + + {document[`label${localPrefix}`]} + + ({document.aside}) + {editMode && !isSecondLang && btnBlocFunction(document)} +
  • + ); + } + return ( + <> + {documents && documents.length > 0 && ( + + )} + + {editMode && !isSecondLang && otherDocuments.length > 0 && ( +
    +
    + +
    + {panelStatus && ( +
    + +
    + )} +
    + )} + + ); +} + +class DocumentsBlocContainer extends Component { + componentWillMount() { + if (this.props.documentStoresStatus === NOT_LOADED) { + this.props.loadDocuments(); + } + } + render() { + return ( + this.props.documentStoresStatus === LOADED && ( + + ) + ); + } +} + +const mapDispatchToProps = { + loadDocuments, +}; + +const mapStateToProps = state => { + return { + documentStoresStatus: state.operationsDocuments.status, + documentStores: state.operationsDocuments.results, + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(DocumentsBlocContainer); diff --git a/src/js/components/operations/msd/documents/documents-bloc/index.spec.js b/src/js/components/operations/msd/documents/documents-bloc/index.spec.js new file mode 100644 index 000000000..ada0d0164 --- /dev/null +++ b/src/js/components/operations/msd/documents/documents-bloc/index.spec.js @@ -0,0 +1,176 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { DocumentsBloc } from './index'; +import { sortArray } from 'js/utils/array-utils'; +import { getLang } from 'js/i18n/build-dictionary'; + +jest.mock('js/components/operations/msd/utils'); + +const documents = [ + { + uri: 'uri1-bis', + url: 'http://google.fr?q=url-1', + updatedDate: '2019-03-04T10:00:00.000Z', + labelLg1: 'B labelLg1-0', + labelLg2: 'B labelLg2-0', + lang: 'fr', + descriptionLg1: 'descriptionLg1', + descriptionLg2: 'descriptionLg2', + aside: `fr-${new Intl.DateTimeFormat(getLang()).format( + new Date('2019-03-04T10:00:00.000Z') + )}`, + }, + { + uri: 'uri2-bis', + url: 'http://google.fr?q=url-2', + updatedDate: '2019-04-04T10:00:00.000Z', + labelLg1: 'A labelLg1-1', + labelLg2: 'A labelLg2-1', + descriptionLg1: 'descriptionLg1-2', + descriptionLg2: 'descriptionLg2-2', + aside: `${new Intl.DateTimeFormat(getLang()).format( + new Date('2019-04-04T10:00:00.000Z') + )}`, + }, + { + uri: 'uri3-bis', + url: 'http://google.fr?q=url-2', + labelLg1: 'Z labelLg1-2', + labelLg2: 'Z labelLg2-2', + lang: 'fr', + descriptionLg1: 'descriptionLg1-2', + descriptionLg2: 'descriptionLg2-2', + aside: 'fr', + }, +]; + +describe('DocumentsBloc', () => { + it('should display nothing if the documents props is not defined', () => { + const general = shallow(); + expect(general.find('.documentsbloc')).toHaveLength(0); + }); + it('should display nothing if the documents props is an empty array', () => { + const general = shallow(); + expect(general.find('.documentsbloc')).toHaveLength(0); + }); + + it('should display nothing if the documents props is an empty array', () => { + const general = shallow( + + ); + expect(general.find('li')).toHaveLength(3); + }); + + it('should display the Lg1 label and description ordered by label', () => { + const general = shallow( + + ); + const orderedList = sortArray('labelLg1')(documents); + + general.find('li').forEach((li, i) => { + expect(li.html()).toEqual( + `
  • ${ + orderedList[i].labelLg1 + }(${orderedList[i].aside})
  • ` + ); + }); + }); + it('should display the Lg2 label and description ordered by label', () => { + const general = shallow( + + ); + const orderedList = sortArray('labelLg2')(documents); + + general.find('li').forEach((li, i) => { + expect(li.html()).toEqual( + `
  • ${ + orderedList[i].labelLg2 + }(${orderedList[i].aside})
  • ` + ); + }); + }); + + describe.each` + lang | expectedEdit | expectedView + ${'Lg2'} | ${0} | ${0} + ${'Lg1'} | ${3} | ${0} + `('$a + $b', ({ lang, expectedEdit, expectedView }) => { + it('should not display delete buttons', () => { + const general = shallow( + + ); + + expect(general.find('.documentsbloc__delete')).toHaveLength(expectedView); + }); + + it('should display zero delete buttons', () => { + const general = shallow( + + ); + + expect(general.find('.documentsbloc__delete')).toHaveLength(expectedEdit); + }); + }); + + it('should not display the Add Document button if there is not more document to add', () => { + const general = shallow( + + ); + + expect(general.find('.documentsbloc__add')).toHaveLength(0); + }); + it('should display the Add Document button if there is more than on document available', () => { + const general = shallow( + + ); + + expect(general.find('.documentsbloc__add')).toHaveLength(0); + }); + + it('should not display the Add Document button for Lg2', () => { + const general = shallow( + + ); + + expect(general.find('.documentsbloc__add')).toHaveLength(0); + }); +}); diff --git a/src/js/components/operations/msd/documents/documents-bloc/style.scss b/src/js/components/operations/msd/documents/documents-bloc/style.scss new file mode 100644 index 000000000..9eab727b1 --- /dev/null +++ b/src/js/components/operations/msd/documents/documents-bloc/style.scss @@ -0,0 +1,17 @@ +@import 'src/styles/mixin.scss'; + +.documentsbloc { + i { + margin-left: 10px; + } + + &__btn { + @include reset-btn(); + } + &__delete { + float: right; + } + &__filepicker { + padding-left: 0; + } +} diff --git a/src/js/components/operations/msd/layout/style.scss b/src/js/components/operations/msd/layout/style.scss index 30129e882..9168e6a6c 100644 --- a/src/js/components/operations/msd/layout/style.scss +++ b/src/js/components/operations/msd/layout/style.scss @@ -70,19 +70,6 @@ } } -@mixin panel-trigger() { - margin-top: 20px; - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; - padding: 6px 5px 2px 5px; - position: absolute; - margin-top: 20px; - background-color: $color_operations_3; - color: #fff; - text-align: center; - text-transform: uppercase; -} - .msd__panel-trigger { &_right { @include panel-trigger(); diff --git a/src/js/components/operations/msd/pages/sims-creation/sims-field.js b/src/js/components/operations/msd/pages/sims-creation/sims-field.js index d80184ba6..8faa9c3fe 100644 --- a/src/js/components/operations/msd/pages/sims-creation/sims-field.js +++ b/src/js/components/operations/msd/pages/sims-creation/sims-field.js @@ -9,6 +9,7 @@ import SelectRmes from 'js/components/shared/select-rmes'; import { Note } from 'js/components/shared/note/note'; import './sims-field.scss'; +import DocumentsBloc from '../../documents/documents-bloc'; const { RICH_TEXT, TEXT, DATE, CODE_LIST, ORGANIZATION } = rangeType; @@ -21,20 +22,52 @@ class Field extends PureComponent { secondLang: PropTypes.bool, }; - handleTextInput = value => { + _handleChange(override) { this.props.handleChange({ id: this.props.msd.idMas, - override: { [this.props.secondLang ? 'labelLg2' : 'labelLg1']: value }, + override, + }); + } + + handleTextInput = value => { + this._handleChange({ + [this.props.secondLang ? 'labelLg2' : 'labelLg1']: value, }); }; + handleCodeListInput = value => { - this.props.handleChange({ - id: this.props.msd.idMas, - override: { codeList: this.props.msd.codeList, value }, - }); + this._handleChange({ codeList: this.props.msd.codeList, value }); }; + + /** + * @param {String} value The new value of the date input + */ handleDateInput = value => { - this.props.handleChange({ id: this.props.msd.idMas, override: { value } }); + this._handleChange({ value }); + }; + + /** + * Handler when the user click on a button in order to delete a document + * @param {String} uri The uri of the document that we should remove + */ + handleDeleteDocument = uri => { + const documents = this.props.currentSection.documents || []; + + this._handleChange({ + documents: documents.filter(doc => doc.uri !== uri), + }); + }; + + /** + * Handler when the user add a new document to a rubric + * @param {import('js/actions/operations/sims/item').SimsDocuments} document + */ + handleAddDocument = document => { + const documents = this.props.currentSection.documents || []; + + this._handleChange({ + documents: [...documents, document], + }); }; render() { @@ -98,11 +131,20 @@ class Field extends PureComponent { /> )} {msd.rangeType === RICH_TEXT && ( - + <> + + + )} {msd.rangeType === CODE_LIST && codesList && ( diff --git a/src/js/components/operations/msd/pages/sims-visualisation/index.js b/src/js/components/operations/msd/pages/sims-visualisation/index.js index dbf8c3854..8acc16861 100644 --- a/src/js/components/operations/msd/pages/sims-visualisation/index.js +++ b/src/js/components/operations/msd/pages/sims-visualisation/index.js @@ -7,6 +7,7 @@ import CheckSecondLang from 'js/components/shared/second-lang-checkbox'; import Button from 'js/components/shared/button'; import { markdownToHtml } from 'js/utils/html'; import { Note } from 'js/components/shared/note/note'; +import DocumentsBloc from 'js/components/operations/msd/documents/documents-bloc/index.js'; const { RICH_TEXT, TEXT, DATE, CODE_LIST, ORGANIZATION } = rangeType; @@ -40,13 +41,19 @@ export default function SimsVisualisation({ currentSection.rangeType === DATE && stringToDate(currentSection.value)} {currentSection.rangeType === RICH_TEXT && ( -
    + <> +
    + + )} {currentSection.rangeType === CODE_LIST && codesLists[currentSection.codeList] && ( diff --git a/src/js/i18n/build-dictionary.js b/src/js/i18n/build-dictionary.js index deb79e0cc..8a2ed26c7 100644 --- a/src/js/i18n/build-dictionary.js +++ b/src/js/i18n/build-dictionary.js @@ -1,7 +1,7 @@ import appD from './dictionary/app'; import conceptsD from './dictionary/concepts'; import classificationsD from './dictionary/classifications'; -import operationsD from './dictionary/operations'; +import operationsD from './dictionary/operations/index.js'; import 'moment/locale/en-gb'; import 'moment/locale/fr'; diff --git a/src/js/i18n/dictionary/app.js b/src/js/i18n/dictionary/app.js index 4bed51e90..bb5839a37 100644 --- a/src/js/i18n/dictionary/app.js +++ b/src/js/i18n/dictionary/app.js @@ -1,3 +1,4 @@ +import btnD from './generic/btn'; const dictionary = { welcome: { fr: 'Application de gestion des métadonnées de référence', @@ -219,91 +220,7 @@ const dictionary = { fr: 'Liens', en: 'Links', }, - // Buttons - btnReturn: { - fr: 'Retour', - en: 'Back', - }, - btnReturnCurrent: { - fr: 'Retour à la version courante', - en: 'Back to current version', - }, - btnNewMale: { - fr: 'Nouveau', - en: 'New', - }, - btnNewFemale: { - fr: 'Nouvelle', - en: 'New', - }, - btnUpdate: { - fr: 'Modifier', - en: 'Update', - }, - btnDuplicate: { - fr: 'Dupliquer', - en: 'Duplicate', - }, - btnSave: { - fr: 'Sauvegarder', - en: 'Save', - }, - btnCompare: { - fr: 'Comparer', - en: 'Compare', - }, - btnCancel: { - fr: 'Annuler', - en: 'Cancel', - }, - btnClose: { - fr: 'Fermer', - en: 'Close', - }, - btnAdd: { - fr: 'Ajouter', - en: 'Add', - }, - btnMinorVersion: { - fr: 'Ecraser la version', - en: 'Overwrite version', - }, - btnMajorVersion: { - fr: 'Nouvelle version', - en: 'New version', - }, - btnExport: { - fr: 'Exporter', - en: 'Export', - }, - btnPdf: { - fr: 'Exporter en PDF', - en: 'Export as PDF', - }, - btnOdt: { - fr: 'Exporter en ODT', - en: 'Export as ODT', - }, - btnSend: { - fr: 'Envoyer', - en: 'Send', - }, - btnValid: { - fr: 'Publier', - en: 'Publish', - }, - btnReinitialize: { - fr: 'Réinitialiser', - en: 'Reinitialize', - }, - btnTree: { - fr: "Voir l'arborescence", - en: 'View tree', - }, - btnDelete: { - fr: 'Supprimer', - en: 'Delete', - }, + ...btnD, // Links narrowerTitle: { fr: 'Parent', diff --git a/src/js/i18n/dictionary/generic/btn.js b/src/js/i18n/dictionary/generic/btn.js new file mode 100644 index 000000000..16d0949f8 --- /dev/null +++ b/src/js/i18n/dictionary/generic/btn.js @@ -0,0 +1,86 @@ +export default { + btnReturn: { + fr: 'Retour', + en: 'Back', + }, + btnReturnCurrent: { + fr: 'Retour à la version courante', + en: 'Back to current version', + }, + btnNewMale: { + fr: 'Nouveau', + en: 'New', + }, + btnNewFemale: { + fr: 'Nouvelle', + en: 'New', + }, + btnUpdate: { + fr: 'Modifier', + en: 'Update', + }, + btnDuplicate: { + fr: 'Dupliquer', + en: 'Duplicate', + }, + btnSave: { + fr: 'Sauvegarder', + en: 'Save', + }, + btnCompare: { + fr: 'Comparer', + en: 'Compare', + }, + btnCancel: { + fr: 'Annuler', + en: 'Cancel', + }, + btnClose: { + fr: 'Fermer', + en: 'Close', + }, + btnAdd: { + fr: 'Ajouter', + en: 'Add', + }, + btnMinorVersion: { + fr: 'Ecraser la version', + en: 'Overwrite version', + }, + btnMajorVersion: { + fr: 'Nouvelle version', + en: 'New version', + }, + btnExport: { + fr: 'Exporter', + en: 'Export', + }, + btnPdf: { + fr: 'Exporter en PDF', + en: 'Export as PDF', + }, + btnOdt: { + fr: 'Exporter en ODT', + en: 'Export as ODT', + }, + btnSend: { + fr: 'Envoyer', + en: 'Send', + }, + btnValid: { + fr: 'Valider', + en: 'Validate', + }, + btnReinitialize: { + fr: 'Réinitialiser', + en: 'Reinitialize', + }, + btnTree: { + fr: "Voir l'arborescence", + en: 'View tree', + }, + btnDelete: { + fr: 'Supprimer', + en: 'Delete', + }, +}; diff --git a/src/js/i18n/dictionary/operations/documents.js b/src/js/i18n/dictionary/operations/documents.js new file mode 100644 index 000000000..e5f3a3fdd --- /dev/null +++ b/src/js/i18n/dictionary/operations/documents.js @@ -0,0 +1,6 @@ +export default { + addDocument: { + fr: 'Ajoutez un document', + en: 'Add a document', + }, +}; diff --git a/src/js/i18n/dictionary/operations.js b/src/js/i18n/dictionary/operations/index.js similarity index 97% rename from src/js/i18n/dictionary/operations.js rename to src/js/i18n/dictionary/operations/index.js index 77b05cece..f7288bf9f 100644 --- a/src/js/i18n/dictionary/operations.js +++ b/src/js/i18n/dictionary/operations/index.js @@ -1,3 +1,5 @@ +import documentsD from 'js/i18n/dictionary/operations/documents'; + const dictionary = { operationsTitle: { fr: 'Opérations', @@ -180,6 +182,7 @@ const dictionary = { en: '{{OPERATION_LABEL}} SIMS', fr: "SIMS de l'opération {{OPERATION_LABEL}}", }, + ...documentsD, }; export default dictionary; diff --git a/src/js/reducers/operations/documents.js b/src/js/reducers/operations/documents.js new file mode 100644 index 000000000..2db4a64b7 --- /dev/null +++ b/src/js/reducers/operations/documents.js @@ -0,0 +1,35 @@ +import { LOADED, LOADING, ERROR, NOT_LOADED } from 'js/constants'; +import { + LOAD_OPERATIONS_DOCUMENTS, + LOAD_OPERATIONS_DOCUMENTS_SUCCESS, + LOAD_OPERATIONS_DOCUMENTS_FAILURE, +} from 'js/actions/constants/operations/documents'; + +/** + * + * @param {SimsDoc} state + * @param {*} action + */ +export const operationsDocuments = function( + state = { status: NOT_LOADED }, + action +) { + switch (action.type) { + case LOAD_OPERATIONS_DOCUMENTS: + return { + status: LOADING, + }; + case LOAD_OPERATIONS_DOCUMENTS_SUCCESS: + return { + status: LOADED, + results: action.payload.results, + }; + case LOAD_OPERATIONS_DOCUMENTS_FAILURE: + return { + status: ERROR, + err: action.payload.err, + }; + default: + return state; + } +}; diff --git a/src/js/reducers/operations/index.js b/src/js/reducers/operations/index.js index aebafd371..e895bf76f 100644 --- a/src/js/reducers/operations/index.js +++ b/src/js/reducers/operations/index.js @@ -1,7 +1,15 @@ import * as A from 'js/actions/constants'; import { LOADED, LOADING, ERROR } from 'js/constants'; import * as currentReducers from 'js/reducers/operations/current'; +import * as documentsReducers from 'js/reducers/operations/documents'; +import { sortArray } from 'js/utils/array-utils'; +const sortByLabel = sortArray('label'); + +/** + * + * @param {Array} param List of Redux event + */ function makeReducers([ GET_ITEMS, GET_ITEMS_SUCCESS, @@ -26,9 +34,23 @@ function makeReducers([ }; case SAVE_ITEM_SUCCESS: if (!state.results) return state; + + /** + * When we add / update a new object, we must first remove this updated item from + * the previous list. + * + * Finally, we should sort by label again + */ + const tail = state.results.filter(obj => obj.id !== action.payload.id); return { status: state.status, - results: [...state.results, action.payload], + results: sortByLabel([ + ...tail, + { + id: action.payload.id, + label: action.payload.prefLabelLg1, + }, + ]), }; default: return state; @@ -36,14 +58,22 @@ function makeReducers([ }; } -const operationsSeriesList = makeReducers([ - A.LOAD_OPERATIONS_SERIES_LIST, - A.LOAD_OPERATIONS_SERIES_LIST_SUCCESS, - A.LOAD_OPERATIONS_SERIES_LIST_FAILURE, -]); +/** + * @typedef {Object} ActionType + * @property {string} types + * @property {Object} payload + */ +/** + * Reducer to store the state of any asynchronous operations. + * The boolean state is used to display / hide a spinner + * + * @param {Boolean} state + * @param {ActionType} action + * @returns {Boolean} + */ const operationsAsyncTask = function(state = false, action) { - switch (action.type) { + switch (action.types) { case A.SAVE_OPERATIONS_INDICATOR: case A.SAVE_OPERATIONS_SERIE: case A.SAVE_OPERATIONS_FAMILY: @@ -63,17 +93,28 @@ const operationsAsyncTask = function(state = false, action) { return state; } }; -const operationsOperationsList = makeReducers([ - A.LOAD_OPERATIONS_OPERATIONS_LIST, - A.LOAD_OPERATIONS_OPERATIONS_LIST_SUCCESS, - A.LOAD_OPERATIONS_OPERATIONS_LIST_FAILURE, -]); const operationsFamiliesList = makeReducers([ A.LOAD_OPERATIONS_FAMILIES_LIST, A.LOAD_OPERATIONS_FAMILIES_LIST_SUCCESS, A.LOAD_OPERATIONS_FAMILIES_LIST_FAILURE, + A.SAVE_OPERATIONS_FAMILY_SUCCESS, ]); + +const operationsSeriesList = makeReducers([ + A.LOAD_OPERATIONS_SERIES_LIST, + A.LOAD_OPERATIONS_SERIES_LIST_SUCCESS, + A.LOAD_OPERATIONS_SERIES_LIST_FAILURE, + A.SAVE_OPERATIONS_SERIE_SUCCESS, +]); + +const operationsOperationsList = makeReducers([ + A.LOAD_OPERATIONS_OPERATIONS_LIST, + A.LOAD_OPERATIONS_OPERATIONS_LIST_SUCCESS, + A.LOAD_OPERATIONS_OPERATIONS_LIST_FAILURE, + A.SAVE_OPERATIONS_OPERATION_SUCCESS, +]); + const operationsMetadataStructureList = makeReducers([ A.LOAD_OPERATIONS_METADATASTRUCTURE_LIST, A.LOAD_OPERATIONS_METADATASTRUCTURE_LIST_SUCCESS, @@ -94,4 +135,5 @@ export default { operationsIndicatorsList, operationsAsyncTask, ...currentReducers, + ...documentsReducers, }; diff --git a/src/js/remote-api/operations-api.js b/src/js/remote-api/operations-api.js index 2b817ef09..6a225d70c 100644 --- a/src/js/remote-api/operations-api.js +++ b/src/js/remote-api/operations-api.js @@ -9,6 +9,7 @@ const api = { getSeriesList: () => ['series'], getOperationsList: () => ['operations'], getFamiliesList: () => ['families'], + getDocumentsList: () => ['documents'], getMetadataStructureList: () => ['metadataStructureDefinition'], getMetadataAttributesList: () => ['metadataAttributes'], @@ -33,7 +34,7 @@ const api = { }, body: JSON.stringify(family), }, - () => {}, + res => Promise.resolve(family.id), ], postFamily: family => [ `family`, @@ -43,7 +44,7 @@ const api = { }, body: JSON.stringify(family), }, - res => res.text().then(id => id), + res => res.text(), ], putSeries: series => [ `series/${series.id}`, @@ -53,7 +54,7 @@ const api = { }, body: JSON.stringify(series), }, - () => {}, + res => Promise.resolve(series.id), ], postSeries: series => [ `series`, @@ -63,7 +64,7 @@ const api = { }, body: JSON.stringify(series), }, - res => res.text().then(id => id), + res => res.text(), ], putOperation: operation => [ `operation/${operation.id}`, @@ -73,7 +74,7 @@ const api = { }, body: JSON.stringify(operation), }, - () => {}, + res => Promise.resolve(operation.id), ], postOperation: operation => [ `operation`, @@ -83,7 +84,7 @@ const api = { }, body: JSON.stringify(operation), }, - res => res.text().then(id => id), + res => res.text(), ], getOperationsWithoutReport: idSerie => [ `series/${idSerie}/operationsWithoutReport`, diff --git a/src/styles/mixin.scss b/src/styles/mixin.scss new file mode 100644 index 000000000..fd24d39c2 --- /dev/null +++ b/src/styles/mixin.scss @@ -0,0 +1,49 @@ +@mixin panel-trigger() { + margin-top: 20px; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + padding: 6px 5px 2px 5px; + position: absolute; + margin-top: 20px; + background-color: $color_operations_3; + color: #fff; + text-align: center; + text-transform: uppercase; +} + + +@mixin reset-btn() { + border: none; + margin: 0; + padding: 0; + width: auto; + overflow: visible; + + background: transparent; + + /* inherit font & color from ancestor */ + color: inherit; + font: inherit; + + /* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */ + line-height: normal; + + /* Corrects font smoothing for webkit */ + -webkit-font-smoothing: inherit; + -moz-osx-font-smoothing: inherit; + + /* Corrects inability to style clickable `input` types in iOS */ + -webkit-appearance: none; +} + +@mixin btn-group-vertical() { + border-radius: 6px; + border-width: 2px; + border-style: solid; + margin-top: 200px; + + .btn { + margin-top: 15px; + margin-bottom: 15px; + } +} \ No newline at end of file