diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bd01e22edd47c77eb35b09eff7b41198f1463721..8ae10fb420e5c853a1b2b129380a52dffff24a5c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,16 +25,14 @@ build testing docker image: only: - develop tags: - - build - image: - name: gcr.io/kaniko-project/executor:debug - entrypoint: [""] + - build_docker + variables: + DOCKER_TAG: testing script: - - mkdir -p /kaniko/.docker - - export - - echo "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination neogeo/geocontrib-front:testing - - echo Image docker neogeo/geocontrib-front:testing livrée + - cat $DOCKER_PASSWORD | docker login --username $DOCKER_LOGIN --password-stdin + - docker-compose build geocontrib-front + - docker-compose push geocontrib-front + - echo Image docker neogeo/geocontrib-front:${DOCKER_TAG} livrée deploy testing docker image: stage: deploy @@ -52,36 +50,30 @@ build stable docker image: only: - master tags: - - build - image: - name: gcr.io/kaniko-project/executor:debug - entrypoint: [""] + - build_docker + variables: + DOCKER_TAG: latest script: - - mkdir -p /kaniko/.docker - - echo "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - - - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination neogeo/geocontrib-front:latest - - echo Image docker neogeo/geocontrib:latest livrée - + - cat $DOCKER_PASSWORD | docker login --username $DOCKER_LOGIN --password-stdin + - docker-compose build geocontrib-front + - docker-compose push geocontrib-front + - echo Image docker neogeo/geocontrib-front:${DOCKER_TAG} livrée build tagged docker image: stage: build only: - tags tags: - - build - image: - name: gcr.io/kaniko-project/executor:debug - entrypoint: [""] + - build_docker + variables: + DOCKER_TAG: $CI_COMMIT_TAG script: # Don't build tag id package.json as wrong version - grep "\"version\":.\"$CI_COMMIT_TAG\"" package.json - - mkdir -p /kaniko/.docker - - echo "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - - - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination neogeo/geocontrib-front:$CI_COMMIT_TAG - - echo Image docker neogeo/geocontrib-front:$CI_COMMIT_TAG livrée - + - cat $DOCKER_PASSWORD | docker login --username $DOCKER_LOGIN --password-stdin + - docker-compose build geocontrib-front + - docker-compose push geocontrib-front + - echo Image docker neogeo/geocontrib-front:${DOCKER_TAG} livrée sonarqube-check: image: diff --git a/docker-compose.yaml b/docker-compose.yaml index 18f164891441f5ecbbaa1bbf86ba056f826570f6..a250dbb007c4693ba0d4c4e033be50cc1ed6fa28 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,7 +2,7 @@ version: "3" services: geocontrib-front: - image: neogeo/geocontrib-front:geocontrib-latest + image: neogeo/geocontrib-front:${DOCKER_TAG:-testing} build: . environment: - BASE_URL=${BASE_URL} diff --git a/package-lock.json b/package-lock.json index 0910a9aac51e1d483ac0dfd470833f00f33d1a22..d597b30628b53ba076f5b3085493f3d10e2a5a1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "geocontrib-frontend", - "version": "3.0.2", + "version": "3.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2562,6 +2562,16 @@ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", @@ -2571,6 +2581,34 @@ "array-uniq": "^1.0.1" } }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true + }, "dir-glob": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", @@ -2642,6 +2680,13 @@ "slash": "^2.0.0" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -2679,6 +2724,28 @@ "requires": { "minipass": "^3.1.1" } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "vue-loader-v16": { + "version": "npm:vue-loader@16.8.3", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz", + "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==", + "dev": true, + "optional": true, + "requires": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "loader-utils": "^2.0.0" + } } } }, @@ -3561,8 +3628,7 @@ "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "bn.js": { "version": "5.2.0", @@ -5207,6 +5273,16 @@ } } }, + "csvtojson": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/csvtojson/-/csvtojson-2.0.10.tgz", + "integrity": "sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ==", + "requires": { + "bluebird": "^3.5.1", + "lodash": "^4.17.3", + "strip-bom": "^2.0.0" + } + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -8321,6 +8397,11 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==" + }, "is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -12273,6 +12354,14 @@ } } }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "requires": { + "is-utf8": "^0.2.0" + } + }, "strip-comments": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-1.0.2.tgz", @@ -13257,75 +13346,6 @@ } } }, - "vue-loader-v16": { - "version": "npm:vue-loader@16.8.3", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz", - "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==", - "dev": true, - "optional": true, - "requires": { - "chalk": "^4.1.0", - "hash-sum": "^2.0.0", - "loader-utils": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "optional": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "optional": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "optional": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "optional": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "optional": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "vue-multiselect": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-2.1.6.tgz", diff --git a/package.json b/package.json index e354da93825ab0839f017a113dacfe453884496e..0ec65a9cb1fbf857590a549731a5ea76fd2682db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geocontrib-frontend", - "version": "3.1.0", + "version": "3.2.0", "private": true, "scripts": { "serve": "npm run init-proxy & npm run init-serve", @@ -21,6 +21,7 @@ "@turf/helpers": "^6.5.0", "axios": "^0.21.1", "core-js": "^3.20.2", + "csvtojson": "^2.0.10", "lodash": "^4.17.21", "ol": "6.8.1", "ol-mapbox-style": "^6.8.3", diff --git a/src/App.vue b/src/App.vue index b45820b6b21df309dee4b1368a6b29b23238866e..56520c47cf02a578d7bc5d8d768073dcd549b360 100644 --- a/src/App.vue +++ b/src/App.vue @@ -66,71 +66,4 @@ export default { ]) }, }; -</script> - -<style> -.vertical { - flex-direction: column; - justify-content: center; -} - -.leaflet-container { - background: white !important; -} - -.flex { - display: flex; -} - -/* keep above loader */ -#menu-dropdown { - z-index: 1001; -} - -@media screen and (max-width: 985px) { - .abstract{ - display: none !important; - } -} - -@media screen and (min-width: 560px) { - .mobile { - display: none !important; - } - #app-header { - min-width: 560px; - } - .menu.container { - width: auto !important; - } - .push-right-desktop { - margin-left: auto; - } -} - -@media screen and (max-width: 590px) { - .desktop { - display: none !important; - } - div.dropdown-list { - width: 100vw; - left: -70px !important; /* should be the same than belows */ - } - .menu.container a.header { - width: 70px; - } - .menu.container a.header > img { - margin: 0; - } - #menu-dropdown { - width: calc(100vw - 70px); - justify-content: space-between; - } - #menu-dropdown > span { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } -} - -</style> +</script> \ No newline at end of file diff --git a/src/assets/js/utils.js b/src/assets/js/utils.js index 0680fcdfad82586d6325e4cc501bf9873abf4d7c..85e7a1b11496402eb57d688f13dcd2d187a90665 100644 --- a/src/assets/js/utils.js +++ b/src/assets/js/utils.js @@ -12,30 +12,4 @@ export function fileConvertSizeToMo(aSize){ aSize = Math.abs(parseInt(aSize, 10)); const def = [1024*1024, 'Mo', 1]; return (aSize/def[0]).toFixed(def[2]); -} - -export function csvToJson(csv, delimiter) { - const result = []; - - const allLines = csv.split('\n'); - const headers = allLines[0].split(delimiter).map(el => { - return el.replace('\r', ''); - }); - const [, ...lines] = allLines; - - for (const line of lines) { - if (line) { - const obj = {}; - const currentLine = line.split(delimiter).map(el => { - return el.replace('\r', ''); - }); - - for (let i = 0; i < headers.length; i++) { - obj[headers[i]] = currentLine[i]; - } - - result.push(obj); - } - } - return JSON.parse(JSON.stringify(result)); -} +} \ No newline at end of file diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 77f4c4ac316b16c6c5882abf6238d3b3775ac210..ec2a5d07f5d4fa8812cd2b2d8da1dedcca000a10 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -22,8 +22,15 @@ :class="['ui dropdown item', { 'active visible': menuIsOpen }]" @click="menuIsOpen = !menuIsOpen" > - <!-- empty span to occupy space for style if no project --> - <span> + <div + v-if="!isOnline" + class="crossed-out mobile" + > + <i + class="wifi icon" + /> + </div> + <span class="expand-center"> <span v-if="project"> Projet : {{ project.title }} </span> </span> <i @@ -155,6 +162,20 @@ </span> </div> <div class="desktop flex push-right-desktop"> + <div + v-if="!isOnline" + class="item" + > + <span + data-tooltip="Vous êtes hors-ligne, + vos changements pourront être envoyés au serveur au retour de la connexion" + data-position="bottom right" + > + <div class="crossed-out"> + <i class="wifi icon"/> + </div> + </span> + </div> <router-link :is="isOnline ? 'router-link' : 'span'" v-if="user" @@ -301,6 +322,86 @@ export default { </script> <style lang="less" scoped> +.vertical { + flex-direction: column; + justify-content: center; +} + +.flex { + display: flex; +} + +/* keep above loader */ +#menu-dropdown { + z-index: 1001; +} + +.expand-center { + width: 100%; + text-align: center; +} + +.crossed-out { + position: relative; + padding: .2em; + &::before { + content: ""; + position: absolute; + top: 45%; + left: -8%; + width: 100%; + border-top: 2px solid #ee2e24; + transform: rotate(45deg); + box-shadow: 0px 0px 0px 1px #373636; + border-radius: 3px; + } +} + +@media screen and (max-width: 985px) { + .abstract{ + display: none !important; + } +} + +@media screen and (min-width: 560px) { + .mobile { + display: none !important; + } + #app-header { + min-width: 560px; + } + .menu.container { + width: auto !important; + } + .push-right-desktop { + margin-left: auto; + } +} + +@media screen and (max-width: 590px) { + .desktop { + display: none !important; + } + div.dropdown-list { + width: 100vw; + left: -70px !important; /* should be the same than belows */ + } + .menu.container a.header { + width: 70px; + } + .menu.container a.header > img { + margin: 0; + } + #menu-dropdown { + width: calc(100vw - 70px); + //justify-content: space-between; + } + #menu-dropdown > span { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } +} .menu.container { position: relative; @@ -348,4 +449,4 @@ export default { height: 100% !important; } -</style> +</style> \ No newline at end of file diff --git a/src/components/Feature/Edit/FeatureExtraForm.vue b/src/components/Feature/Edit/FeatureExtraForm.vue index 8d09d514788bca922e52c7a66dce5982522a5ec2..36f4130b18a0fb6a3503c35eb256e1ef209d3bd7 100644 --- a/src/components/Feature/Edit/FeatureExtraForm.vue +++ b/src/components/Feature/Edit/FeatureExtraForm.vue @@ -1,9 +1,7 @@ <template> - <div - v-if="field && field.field_type === 'char'" - > + <div v-if="field && field.field_type === 'char'"> <label - v-if="$route.name === 'editer-signalement'" + v-if="displayLabels" :for="field.name" > {{ field.label }} @@ -17,11 +15,9 @@ > </div> - <div - v-else-if="field && field.field_type === 'list'" - > + <div v-else-if="field && field.field_type === 'list'"> <label - v-if="$route.name === 'editer-signalement'" + v-if="displayLabels" :for="field.name" > {{ field.label }} @@ -32,11 +28,9 @@ :selection.sync="selected_extra_form_list" /> </div> - <div - v-else-if="field && field.field_type === 'integer'" - > + <div v-else-if="field && field.field_type === 'integer'"> <label - v-if="$route.name === 'editer-signalement'" + v-if="displayLabels" :for="field.name" > {{ field.label }} @@ -52,9 +46,7 @@ > </div> </div> - <div - v-else-if="field && field.field_type === 'boolean'" - > + <div v-else-if="field && field.field_type === 'boolean'"> <div class="ui checkbox"> <input :id="field.name" @@ -64,15 +56,13 @@ @change="updateStore_extra_form" > <label :for="field.name"> - {{ $route.name === 'editer-signalement' ? field.label : '' }} + {{ displayLabels ? field.label : '' }} </label> </div> </div> - <div - v-else-if="field && field.field_type === 'date'" - > + <div v-else-if="field && field.field_type === 'date'"> <label - v-if="$route.name === 'editer-signalement'" + v-if="displayLabels" :for="field.name" > {{ field.label }} @@ -85,11 +75,9 @@ @blur="updateStore_extra_form" > </div> - <div - v-else-if="field && field.field_type === 'decimal'" - > + <div v-else-if="field && field.field_type === 'decimal'"> <label - v-if="$route.name === 'editer-signalement'" + v-if="displayLabels" :for="field.name" > {{ field.label }} @@ -105,11 +93,9 @@ > </div> </div> - <div - v-else-if="field && field.field_type === 'text'" - > + <div v-else-if="field && field.field_type === 'text'"> <label - v-if="$route.name === 'editer-signalement'" + v-if="displayLabels" :for="field.name" > {{ field.label }} @@ -152,6 +138,10 @@ export default { this.$store.commit('feature/UPDATE_EXTRA_FORM', newExtraForm); }, }, + + displayLabels() { + return this.$route.name === 'editer-signalement' || this.$route.name === 'ajouter-signalement'; + } }, methods: { diff --git a/src/components/FeatureType/FeatureTypeCustomForm.vue b/src/components/FeatureType/FeatureTypeCustomForm.vue index e71b17585637336eace2b1f6e7fe029e79f4afcc..51f068a1460a50452a0bd20da3def1f4302fb3ea 100644 --- a/src/components/FeatureType/FeatureTypeCustomForm.vue +++ b/src/components/FeatureType/FeatureTypeCustomForm.vue @@ -365,53 +365,46 @@ export default { return occurences.length === 1; }, - checkFilledOptions() { - if (this.form.field_type.value === 'list') { - if (this.form.options.value.length < 1) { - return false; - } else if ( - this.form.options.value.length === 1 && - this.form.options.value[0] === '' - ) { - return false; - } - } - return true; + checkListOptions() { + if (this.form.field_type.value !== 'list') return true; + return this.form.options.value.length >= 2 && !this.form.options.value.includes(''); }, checkCustomForm() { this.form.label.errors = []; this.form.name.errors = []; this.form.options.errors = []; + let isValid = true; if (!this.form.label.value) { //* vérifier que le label est renseigné this.form.label.errors = ['Veuillez compléter ce champ.']; - return false; + isValid = false; } else if (!this.form.name.value) { //* vérifier que le nom est renseigné this.form.name.errors = ['Veuillez compléter ce champ.']; - return false; + isValid = false; } else if (!this.hasRegularCharacters(this.form.name.value)) { //* vérifier qu'il n'y a pas de caractères spéciaux this.form.name.errors = [ 'Veuillez utiliser seulement les caratères autorisés.', ]; - return false; + isValid = false; } else if (!this.checkUniqueName()) { //* vérifier si les noms sont pas dupliqués this.form.name.errors = [ 'Les champs personnalisés ne peuvent pas avoir des noms similaires.', ]; - return false; - } else if (!this.checkFilledOptions()) { + isValid = false; + } else if (!this.checkListOptions()) { //* s'il s'agit d'un type liste, vérifier que le champ option est bien renseigné this.form.options.errors = ['Veuillez compléter ce champ.']; - return false; + isValid = false; } else if (this.hasDuplicateOptions()) { //* pour le cas d'options dupliqués - return false; + isValid = false; } - return true; + if (!isValid) document.getElementById(`custom_form-${this.form.position.value}`).scrollIntoView({ block: 'start', inline: 'nearest' }); + return isValid; }, }, }; diff --git a/src/components/Project/Detail/ProjectFeatureTypes.vue b/src/components/Project/Detail/ProjectFeatureTypes.vue index fa88de3e66e19b648fc845c0f5258eff69964bc2..f98acb30e2b0eb3b59d8be7f9adca67a805e03a8 100644 --- a/src/components/Project/Detail/ProjectFeatureTypes.vue +++ b/src/components/Project/Detail/ProjectFeatureTypes.vue @@ -426,10 +426,11 @@ </template> <script> +import { csv } from 'csvtojson'; import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'; -import { fileConvertSizeToMo, csvToJson } from '@/assets/js/utils'; +import { fileConvertSizeToMo } from '@/assets/js/utils'; import FeatureTypeLink from '@/components/FeatureType/FeatureTypeLink'; export default { @@ -665,7 +666,11 @@ export default { .filter(Boolean); if (sampleLine.length > 1 && headersCoord.length === 2) { this.csvError = null; - this.csvImport = csvToJson(fr.result, delimiter); + csv() + .fromString(fr.result) + .then((jsonObj)=>{ + this.csvImport = jsonObj; + }); this.featureTypeImporting = false; //* stock filename to import features afterward this.SET_FILE_TO_IMPORT(this.csvFileToImport); diff --git a/src/components/Project/FeaturesListAndMap/FeatureListTable.vue b/src/components/Project/FeaturesListAndMap/FeatureListTable.vue index 006a27a2bec416dd3789244d2abc3fef9a6a32ef..aa6aa2a8e9698a43156387298640995f4f72ed4a 100644 --- a/src/components/Project/FeaturesListAndMap/FeatureListTable.vue +++ b/src/components/Project/FeaturesListAndMap/FeatureListTable.vue @@ -1,7 +1,10 @@ <template> <div> <div class="ui form"> - <div class="inline fields"> + <div + v-if="isOnline" + class="inline fields" + > <label data-tooltip="Choisir un type de sélection de signalements pour effectuer une action" data-position="bottom left" @@ -57,6 +60,7 @@ <thead> <tr> <th + v-if="isOnline" scope="col" class="dt-center" > @@ -68,7 +72,7 @@ class="dt-center" > <div - class="pointer" + :class="isOnline ? 'pointer' : 'disabled'" @click="changeSort('status')" > Statut @@ -87,7 +91,7 @@ class="dt-center" > <div - class="pointer" + :class="isOnline ? 'pointer' : 'disabled'" @click="changeSort('feature_type')" > Type @@ -106,7 +110,7 @@ class="dt-center" > <div - class="pointer" + :class="isOnline ? 'pointer' : 'disabled'" @click="changeSort('title')" > Nom @@ -125,7 +129,7 @@ class="dt-center" > <div - class="pointer" + :class="isOnline ? 'pointer' : 'disabled'" @click="changeSort('updated_on')" > Dernière modification @@ -145,7 +149,7 @@ class="dt-center" > <div - class="pointer" + :class="isOnline ? 'pointer' : 'disabled'" @click="changeSort('display_creator')" > Auteur @@ -165,7 +169,7 @@ class="dt-center" > <div - class="pointer" + :class="isOnline ? 'pointer' : 'disabled'" @click="changeSort('display_last_editor')" > Dernier éditeur @@ -186,7 +190,10 @@ v-for="(feature, index) in paginatedFeatures" :key="index" > - <td class="dt-center"> + <td + v-if="isOnline" + class="dt-center" + > <div :class="['ui checkbox', {disabled: !checkRights(feature)}]" > @@ -310,7 +317,7 @@ sur {{ featuresCount }} éléments </div> <div - v-if="pageNumbers.length > 1" + v-if="pageNumbers.length > 1 && isOnline" id="table-features_paginate" class="dataTables_paginate paging_simple_numbers" > @@ -429,7 +436,11 @@ export default { computed: { ...mapGetters(['permissions']), - ...mapState(['user', 'USER_LEVEL_PROJECTS']), + ...mapState([ + 'user', + 'USER_LEVEL_PROJECTS', + 'isOnline' + ]), ...mapState('projects', ['project']), ...mapState('feature', ['clickedFeatures', 'massMode']), @@ -556,6 +567,7 @@ export default { }, changeSort(column) { + if (!this.isOnline) return; if (this.sort.column === column) { //changer only order this.$emit('update:sort', { diff --git a/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue b/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue index 763887b2f192dcc327a75dd45ad5cd0633a3ea7f..20c771d0d40474d6ec1d490cd36c0e4d09140a2b 100644 --- a/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue +++ b/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue @@ -86,7 +86,7 @@ </div> </div> <div - v-if="checkedFeatures.length > 0 && massMode.includes('edit')" + v-if="checkedFeatures.length > 0 && massMode.includes('edit') && isOnline" class="ui dropdown button compact button-hover-green tiny-margin-left" :data-tooltip="`Modifier le${massMode.includes('status') ? ' statut' : 's attributs'} des signalements`" data-position="bottom right" @@ -117,7 +117,7 @@ </div> </div> <div - v-if="checkedFeatures.length > 0 && massMode === 'delete-features'" + v-if="checkedFeatures.length > 0 && massMode === 'delete-features' && isOnline" class="ui button compact button-hover-red tiny-margin-left" data-tooltip="Supprimer tous les signalements sélectionnés" data-position="bottom right" @@ -138,7 +138,7 @@ > <div id="type" - class="field column" + :class="['field column', { 'disabled': !isOnline }]" > <label>Type</label> <Dropdown @@ -151,7 +151,7 @@ </div> <div id="statut" - class="field column" + :class="['field column', { 'disabled': !isOnline }]" > <label>Statut</label> <!-- //* giving an object mapped on key name --> @@ -165,7 +165,7 @@ </div> <div id="name" - class="field column" + :class="['field column', { 'disabled': !isOnline }]" > <label>Nom</label> <div class="ui icon input"> @@ -260,7 +260,8 @@ export default { computed: { ...mapState([ 'user', - 'USER_LEVEL_PROJECTS' + 'USER_LEVEL_PROJECTS', + 'isOnline' ]), ...mapState('feature', [ 'checkedFeatures', diff --git a/src/main.js b/src/main.js index 0bae0e95ba2fa6a2b0778efdde6df71763a4fe88..cabb80a8ddc6293333db579cbb747d5852b9590b 100644 --- a/src/main.js +++ b/src/main.js @@ -26,7 +26,7 @@ Vue.config.productionTip = false; // gestion mise à jour du serviceWorker et du precache var refreshing=false; -if(navigator.serviceWorker){ +if (navigator.serviceWorker) { navigator.serviceWorker.addEventListener('controllerchange', () => { // We'll also need to add 'refreshing' to our data originally set to false. if (refreshing) { diff --git a/src/utils/index.js b/src/utils/index.js index 432cd2970a33e4be66ba5293a3501b9e7f76b2d5..a89354b8305aaae4b1d12e04003990b7e25cda55 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -58,4 +58,29 @@ export function allowedStatus2change(user, isModerate, userStatus, isOwnFeature, } } return []; +} + +export function transformProperties(prop) { + const type = typeof prop; + const date = new Date(prop); + const regInteger = /^-*?\d+$/; + const regFloat = /^-*?\d*?\.\d+$/; + const regText = /[\r\n]/; + if (type === 'boolean' || prop.toLowerCase() === 'true' || prop.toLowerCase() === 'False') { + return 'boolean'; + } else if (regInteger.test(prop) || Number.isSafeInteger(prop)) { + return 'integer'; + } else if ( + type === 'string' && + ['/', ':', '-'].some((el) => prop.includes(el)) && // check for chars found in datestring + date instanceof Date && + !isNaN(date.valueOf()) + ) { + return 'date'; + } else if (regFloat.test(prop) || type === 'number' && !isNaN(parseFloat(prop))) { + return 'decimal'; + } else if (regText.test(prop)) { + return 'text'; + } + return 'char'; //* string by default, most accepted type in database } \ No newline at end of file diff --git a/src/views/Feature/FeatureEdit.vue b/src/views/Feature/FeatureEdit.vue index 6059ea53eff334338c572a54415fadaac7b9905b..35eaa28cc17c4b6e0c15fdfb9ed527e20c95f8b0 100644 --- a/src/views/Feature/FeatureEdit.vue +++ b/src/views/Feature/FeatureEdit.vue @@ -264,11 +264,11 @@ <div v-for="(field, index) in orderedCustomFields" :key="field.field_type + index" - class="field" > <FeatureExtraForm :id="field.label" :field="field" + class="field" /> {{ field.errors }} </div> diff --git a/src/views/FeatureType/FeatureTypeDetail.vue b/src/views/FeatureType/FeatureTypeDetail.vue index 2c20b6c90632cacfd92a6942e2b0539e14cb7a9c..97db52d8babd1925b82899592d1a4b3ee069e619 100644 --- a/src/views/FeatureType/FeatureTypeDetail.vue +++ b/src/views/FeatureType/FeatureTypeDetail.vue @@ -40,10 +40,12 @@ </div> </div> <div class="value"> - {{ features_count }} + {{ isOnline ? features_count : '?' }} </div> - <div class="label"> - Signalement{{ features.length > 1 ? "s" : "" }} + <div + class="label" + > + Signalement{{ features.length > 1 || !isOnline ? "s" : "" }} </div> </div> @@ -221,7 +223,10 @@ </div> </div> - <div class="nine wide column"> + <div + v-if="isOnline" + class="nine wide column" + > <h3 class="ui header"> Derniers signalements </h3> @@ -347,17 +352,32 @@ </router-link> <br> </div> + <div + v-else + class="nine wide column" + > + <h3 class="ui header"> + Derniers signalements + </h3> + <div class="ui message info"> + <p> + Information non disponible en mode déconnecté. + </p> + </div> + </div> </div> </div> </template> <script> +import { csv } from 'csvtojson'; + import { mapActions, mapMutations, mapGetters, mapState } from 'vuex'; -import { formatStringDate } from '@/utils'; +import { formatStringDate, transformProperties } from '@/utils'; import ImportTask from '@/components/ImportTask'; import featureAPI from '@/services/feature-api'; -import { fileConvertSizeToMo, csvToJson } from '@/assets/js/utils'; +import { fileConvertSizeToMo } from '@/assets/js/utils'; // TODO: refactor with above utils, those files are similar export default { name: 'FeatureTypeDetail', @@ -539,26 +559,6 @@ export default { } }, - transformProperties(prop) { - const type = typeof prop; - const date = new Date(prop); - if (type === 'boolean') { - return 'boolean'; - } else if (Number.isSafeInteger(prop)) { - return 'integer'; - } else if ( - type === 'string' && - ['/', ':', '-'].some((el) => prop.includes(el)) && // check for chars found in datestring - date instanceof Date && - !isNaN(date.valueOf()) - ) { - return 'char'; - } else if (type === 'number' && !isNaN(parseFloat(prop))) { - return 'decimal'; - } - return 'char'; //* string by default, most accepted type in database - }, - checkJsonValidity(json) { this.importError = ''; const fields = this.structure.customfield_set.map((el) => { @@ -570,9 +570,10 @@ export default { }); for (const feature of json.features) { for (const { name, field_type, options } of fields) { - if (name in feature.properties) { - const fieldInFeature = feature.properties[name]; - const customType = this.transformProperties(fieldInFeature); + const properties = feature.properties || feature; + if (name in properties) { + const fieldInFeature = properties[name]; + const customType = transformProperties(fieldInFeature); //* if custom field value is not null, then check validity of field if (fieldInFeature !== null) { //* if field type is list, it's not possible to guess from value type @@ -595,22 +596,20 @@ export default { return true; }, - checkCsvValidity(csv) { + async checkCsvValidity(csvString) { this.importError = ''; - - // Find csv delimiter - const commaDelimited = csv.split('\n')[0].includes(','); - const semicolonDelimited = csv.split('\n')[0].includes(';'); + // Find csvString delimiter + const commaDelimited = csvString.split('\n')[0].includes(','); + const semicolonDelimited = csvString.split('\n')[0].includes(';'); const delimiter = commaDelimited && !semicolonDelimited ? ',' : semicolonDelimited ? ';' : false; if ((commaDelimited && semicolonDelimited) || !delimiter) { this.importError = `Le fichier ${this.csvFileToImport.name} n'est pas formaté correctement`; return false; } - // Check if file contains 'lat' and 'long' fields const headersLine = - csv + csvString .split('\n')[0] .replace(/(\r\n|\n|\r)/gm, '') .split(delimiter) @@ -622,7 +621,7 @@ export default { return false; } const sampleLine = - csv + csvString .split('\n')[1] .split(delimiter) .map(el => { @@ -630,55 +629,8 @@ export default { }) .filter(Boolean); if (sampleLine.length > 1 && headersLine.length === 2) { - const fields = this.structure.customfield_set.map((el) => { - return { - name: el.name, - field_type: el.field_type, - options: el.options, - }; - }); - const csvFeatures = csvToJson(csv, delimiter); - for (const feature of csvFeatures) { - for (let { name, field_type, options } of fields) { - if (name in feature) { - const fieldInFeature = feature[name]; - - // overide some specific cases on date type data - if ( - typeof fieldInFeature === 'string' && - ['/', ':', '-'].some((el) => fieldInFeature.includes(el)) && - (new Date(fieldInFeature)) instanceof Date && - !isNaN((new Date(fieldInFeature)).valueOf()) - ) { - field_type = 'char'; - } else if ( - field_type === 'date' && - ((new Date(fieldInFeature)) instanceof Date) - ) { - field_type = 'char'; - } - - const customType = this.transformProperties(fieldInFeature); - //* if custom field value is not null, then check validity of field - if (fieldInFeature !== null) { - //* if field type is list, it's not possible to guess from value type - if (field_type === 'list') { - //*then check if the value is an available option - if (fieldInFeature && !options.includes(fieldInFeature)) { - this.importError = `Le fichier est invalide: la valeur [ ${fieldInFeature} ] n'est pas une option valide - pour le champ personnalisé "${name}".`; - return false; - } - } else if (customType !== field_type) { - //* check if custom field value match - this.importError = `Le fichier est invalide: Un champ de type ${field_type} ne peut pas avoir la valeur [ ${fieldInFeature} ]`; - return false; - } - } - } - } - } - return true; + const features = await csv().fromString(csvString); + return this.checkJsonValidity({ features }); } else { return false; } diff --git a/src/views/FeatureType/FeatureTypeEdit.vue b/src/views/FeatureType/FeatureTypeEdit.vue index 315acba70feeaf31c5d1beb13cbc259fbb08a0a3..47bad6dceb3668063f9c76a1b9de51d7c13fdc7c 100644 --- a/src/views/FeatureType/FeatureTypeEdit.vue +++ b/src/views/FeatureType/FeatureTypeEdit.vue @@ -159,6 +159,7 @@ import { mapGetters, mapState, mapMutations, mapActions } from 'vuex'; import Dropdown from '@/components/Dropdown.vue'; import FeatureTypeCustomForm from '@/components/FeatureType/FeatureTypeCustomForm.vue'; +import { transformProperties } from'@/utils'; export default { name: 'FeatureTypeEdit', @@ -639,23 +640,21 @@ export default { return 'point'; }, - transformProperties(prop) { - const type = typeof prop; - const date = new Date(prop); - if (type === 'boolean') { - return 'boolean'; - } else if (Number.isSafeInteger(prop)) { - return 'integer'; - } else if ( - type === 'string' && - date instanceof Date && - !isNaN(date.valueOf()) - ) { - return 'date'; - } else if (type === 'number' && !isNaN(parseFloat(prop))) { - return 'decimal'; + buildCustomForm(properties) { + for (const [key, val] of Object.entries(properties)) { + //* check that the property is not a keyword from the backend or map style + // todo: add map style keywords + if (!this.reservedKeywords.includes(key)) { + const customForm = { + label: { value: key || '' }, + name: { value: key || '' }, + position: this.dataKey, // * use dataKey already incremented by addCustomForm + field_type: { value: transformProperties(val) }, // * guessed from the type + options: { value: [] }, // * not available in export + }; + this.addCustomForm(customForm); + } } - return 'char'; //* string by default, most accepted type in database }, importGeoJsonFeatureType() { @@ -665,93 +664,16 @@ export default { this.form.title.value = properties.feature_type; this.form.geom_type.value = this.translateLabel(geometry.type); this.updateStore(); //* register title & geom_type in store - - //* loop properties to create a customForm for each of them - for (const [key, val] of Object.entries(properties)) { - //* check that the property is not a keyword from the backend or map style - // todo: add map style keywords - if (!this.reservedKeywords.includes(key)) { - const customForm = { - label: { value: key || '' }, - name: { value: key || '' }, - position: this.dataKey, // * use dataKey already incremented by addCustomForm - field_type: { value: this.transformProperties(val) }, // * guessed from the type - options: { value: [] }, // * not available in export - }; - this.addCustomForm(customForm); - } - } + this.buildCustomForm(properties); } }, importCSVFeatureType() { if (this.csv.length) { this.updateStore(); //* register title & geom_type in store - // List fileds for user to select coords fields - // this.csvFields = - // Object.keys(this.csv[0]) - // .map(el => { - // return { - // field: el, - // x: false, - // y:false - // }; - // }); - for (const [key, val] of Object.entries(this.csv[0])) { - //* check that the property is not a keyword from the backend or map style - // todo: add map style keywords - if (!this.reservedKeywords.includes(key)) { - const customForm = { - label: { value: key || '' }, - name: { value: key || '' }, - position: this.dataKey, // * use dataKey already incremented by addCustomForm - field_type: { value: this.transformProperties(val) }, // * guessed from the type - options: { value: [] }, // * not available in export - }; - this.addCustomForm(customForm); - } - } + this.buildCustomForm(this.csv[0]); } }, - - // pickXcsvCoordField(e) { - // this.csvFields.forEach(el => { - // if (el.field === e.field) { - // el.x = true; - // } else { - // el.x = false; - // } - // }); - // }, - // pickYcsvCoordField(e) { - // this.csvFields.forEach(el => { - // if (el.field === e.field) { - // el.y = true; - // } else { - // el.y = false; - // } - // }); - // }, - // setCSVCoordsFields() { - // const xField = this.csvFields.find(el => el.x === true).field; - // const yField = this.csvFields.find(el => el.y === true).field; - // this.csvFields = null; - - // for (const [key, val] of Object.entries(this.csv[0])) { - // //* check that the property is not a keyword from the backend or map style - // // todo: add map style keywords - // if (!this.reservedKeywords.includes(key) && key !== xField && key !== yField) { - // const customForm = { - // label: { value: key || '' }, - // name: { value: key || '' }, - // position: this.dataKey, // * use dataKey already incremented by addCustomForm - // field_type: { value: this.transformProperties(val) }, // * guessed from the type - // options: { value: [] }, // * not available in export - // }; - // this.addCustomForm(customForm); - // } - // } - // } }, }; </script> diff --git a/src/views/Project/FeaturesListAndMap.vue b/src/views/Project/FeaturesListAndMap.vue index 897fd9ff8e285317d1be6ccd85efabe141aafe55..43eb47962ce31760671be3539af681dd03218551 100644 --- a/src/views/Project/FeaturesListAndMap.vue +++ b/src/views/Project/FeaturesListAndMap.vue @@ -14,7 +14,7 @@ /> <div - :class="['ui tab active map-container', {visible: showMap}]" + :class="['ui tab active map-container', { 'visible': showMap }]" data-tab="map" > <div