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/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/Account/UserProjectsList.vue b/src/components/Account/UserProjectsList.vue index 3d7302a19b701d5d3d7363424e1459f849b69017..4e26440a571bfb10b66cb2d96ac28b4d09b26378 100644 --- a/src/components/Account/UserProjectsList.vue +++ b/src/components/Account/UserProjectsList.vue @@ -47,11 +47,11 @@ <div class="description"> <p>{{ project.description }}</p> </div> - <div class="meta"> + <div class="meta top"> <span class="right floated" > - Projet {{ project.moderation ? "" : "non" }} modéré + <strong>Projet {{ project.moderation ? "" : "non" }} modéré</strong> </span> <span> Niveau d'autorisation requis : {{ project.access_level_pub_feature }} @@ -228,6 +228,12 @@ export default { } } +.description { + p { + text-align: justify; + } +} + @media only screen and (min-width: 767px) { .item-content-wrapper { align-items: flex-start; @@ -235,6 +241,12 @@ export default { .middle.aligned.content { width: 100%; padding: 0 0 0 1.5em; + + .meta.top { + span { + line-height: 1.2em; + } + } } } } @@ -244,8 +256,25 @@ export default { align-items: center; .middle.aligned.content { - width: 70%; + width: 80%; padding: 1.5em 0 0; + + .meta.top { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + + .right.floated { + float: none !important; + margin-left: 0 !important; + margin-bottom: 0.5em; + } + + span { + margin: 0.15em 0; + } + } } } } diff --git a/src/components/Feature/Detail/FeatureComments.vue b/src/components/Feature/Detail/FeatureComments.vue index a6d633779638868f9b562d7c435c2776e45f2502..c47c5ec5659b72699b3dffe4bcae00a6f853c892 100644 --- a/src/components/Feature/Detail/FeatureComments.vue +++ b/src/components/Feature/Detail/FeatureComments.vue @@ -207,10 +207,12 @@ export default { 'user', 'isOnline', ]), - ...mapGetters([ 'permissions', ]), + ...mapState('feature', [ + 'currentFeature', + ]), DJANGO_BASE_URL() { return this.$store.state.configuration.VUE_APP_DJANGO_BASE; @@ -231,14 +233,14 @@ export default { if (this.validateForm()) { featureAPI .postComment({ - featureId: this.$route.params.slug_signal, + featureId: this.currentFeature.feature_id, comment: this.comment_form.comment.value, }) .then((response) => { if (response && this.comment_form.attachment_file.file) { featureAPI .postCommentAttachment({ - featureId: this.$route.params.slug_signal, + featureId: this.currentFeature.feature_id, file: this.comment_form.attachment_file.file, fileName: this.comment_form.attachment_file.fileName, title: this.comment_form.attachment_file.title, diff --git a/src/components/Feature/Detail/FeatureHeader.vue b/src/components/Feature/Detail/FeatureHeader.vue index d1ab51b94908bffa2dae66f0f15f177802f1c326..c3784191e44c1abd730f914ceadb8f5189003dc2 100644 --- a/src/components/Feature/Detail/FeatureHeader.vue +++ b/src/components/Feature/Detail/FeatureHeader.vue @@ -2,152 +2,163 @@ <div> <h1 class="ui header"> <div class="content"> - <span - v-if="fastEditionMode && form" - class="form ui half-block" - > - <input - id="feature_detail_title_input" - :value="form.title" - type="text" - required - maxlength="128" - name="title" - @blur="updateTitle" - > - </span> - <span v-else> - {{ currentFeature.title || currentFeature.feature_id }} - </span> - - <div class="ui icon right floated compact buttons"> - <router-link - v-if="displayToListButton" - id="feature-detail-to-features-list" - :to="{ - name: 'liste-signalements', - params: { slug: $route.params.slug }, - }" - custom - > - <div class="ui button tiny-margin teal"> - <i class="ui icon arrow right" /> - Retour à la liste des signalements - </div> - </router-link> - <span - v-if="featuresCount" - id="feature-count" - class="ui button tiny-margin basic" - > - {{ parseInt($route.query.offset) + 1 }} sur {{ featuresCount }} - </span> - <button - v-if="queryparams" - id="previous-feature" - :class="['ui button button-hover-green tiny-margin', { disabled: queryparams.previous < 0 }]" - data-tooltip="Voir le précédent signalement" - data-position="bottom center" - @click="toFeature('previous')" + <div class="two-block"> + <div + v-if="fastEditionMode && form && canEditFeature" + class="form ui half-block" > - <i - class="angle left fitted icon" - aria-hidden="true" - /> - </button> - <button - v-if="queryparams" - id="next-feature" - :class="[ - 'ui button button-hover-green tiny-margin', - { disabled: queryparams.next >= featuresCount } - ]" - data-tooltip="Voir le prochain signalement" - data-position="bottom center" - @click="toFeature('next')" + <input + id="feature_detail_title_input" + :value="form.title" + type="text" + required + maxlength="128" + name="title" + @blur="updateTitle" + > + </div> + <div + v-else + class="ellipsis" > - <i - class="angle right fitted icon" - aria-hidden="true" - /> - </button> + {{ currentFeature.title || currentFeature.feature_id }} + </div> - <button - v-if="fastEditionMode && userCanFastEdit" - id="previous-feature" - :class="['ui button button-hover-orange tiny-margin', { disabled: false }]" - data-tooltip="Enregistrer les modifications" - data-position="bottom center" - @click="$store.dispatch('feature/SEND_FEATURE', $route.name)" + <div + id="feature-actions" + class="ui icon compact buttons" > - <i - class="save fitted icon" - aria-hidden="true" - /> - </button> + <div> + <router-link + v-if="displayToListButton" + id="feature-detail-to-features-list" + :to="{ + name: 'liste-signalements', + params: { slug: $route.params.slug }, + }" + custom + > + <div class="ui button tiny-margin teal"> + <i class="ui icon arrow right" /> + Retour à la liste des signalements + </div> + </router-link> + </div> + <div> + <span + v-if="featuresCount" + id="feature-count" + class="ui button tiny-margin basic disabled no-opacity" + > + {{ parseInt($route.query.offset) + 1 }} sur {{ featuresCount }} + </span> + <button + v-if="queryparams" + id="previous-feature" + :class="['ui button button-hover-green tiny-margin', { disabled: queryparams.previous < 0 }]" + data-tooltip="Voir le précédent signalement" + data-position="bottom center" + @click="toFeature('previous')" + > + <i + class="angle left fitted icon" + aria-hidden="true" + /> + </button> + <button + v-if="queryparams" + id="next-feature" + :class="[ + 'ui button button-hover-green tiny-margin', + { disabled: queryparams.next >= featuresCount } + ]" + data-tooltip="Voir le prochain signalement" + data-position="bottom center" + @click="toFeature('next')" + > + <i + class="angle right fitted icon" + aria-hidden="true" + /> + </button> + </div> + <div> + <button + v-if="fastEditionMode && canEditFeature" + id="save-fast-edit" + :class="['ui button button-hover-orange tiny-margin', { disabled: false }]" + data-tooltip="Enregistrer les modifications" + data-position="bottom center" + @click="validateFastEdition" + > + <i + class="save fitted icon" + aria-hidden="true" + /> + </button> - <router-link - v-if="permissions && permissions.can_create_feature" - id="add-feature" - :to="{ - name: 'ajouter-signalement', - params: { - slug_type_signal: $route.params.slug_type_signal || featureType.slug, - }, - }" - class="ui button button-hover-green tiny-margin" - data-tooltip="Ajouter un signalement" - data-position="bottom center" - > - <i - class="plus icon" - aria-hidden="true" - /> - </router-link> + <router-link + v-if="permissions && permissions.can_create_feature" + id="add-feature" + :to="{ + name: 'ajouter-signalement', + params: { + slug_type_signal: $route.params.slug_type_signal || featureType.slug, + }, + }" + class="ui button button-hover-green tiny-margin" + data-tooltip="Ajouter un signalement" + data-position="bottom center" + > + <i + class="plus icon" + aria-hidden="true" + /> + </router-link> - <router-link - v-if="slugSignal && - ((permissions && permissions.can_update_feature) || - isFeatureCreator || - isModerator) - " - id="edit-feature" - :to="{ - name: 'editer-signalement', - params: { - slug_signal: slugSignal, - slug_type_signal: $route.params.slug_type_signal || featureType.slug, - }, - query: $route.query - }" - class="ui button button-hover-orange tiny-margin" - data-tooltip="Éditer le signalement" - data-position="bottom center" - > - <i - class="inverted grey pencil alternate icon" - aria-hidden="true" - /> - </router-link> + <router-link + v-if="slugSignal && canEditFeature" + id="edit-feature" + :to="{ + name: 'editer-signalement', + params: { + slug_signal: slugSignal, + slug_type_signal: $route.params.slug_type_signal || featureType.slug, + }, + query: $route.query + }" + class="ui button button-hover-orange tiny-margin" + data-tooltip="Éditer le signalement" + data-position="bottom center" + > + <i + class="inverted grey pencil alternate icon" + aria-hidden="true" + /> + </router-link> - <a - v-if="((permissions && permissions.can_update_feature) || isFeatureCreator) && isOnline" - id="currentFeature-delete" - class="ui button button-hover-red tiny-margin" - data-tooltip="Supprimer le signalement" - data-position="bottom right" - @click="$emit('setIsCancelling')" - > - <i - class="inverted grey trash alternate icon" - aria-hidden="true" - /> - </a> + <a + v-if="canDeleteFeature && isOnline" + id="currentFeature-delete" + class="ui button button-hover-red tiny-margin" + data-tooltip="Supprimer le signalement" + data-position="bottom right" + @click="$emit('setIsDeleting')" + > + <i + class="inverted grey trash alternate icon" + aria-hidden="true" + /> + </a> + </div> + </div> </div> - <div class="ui hidden divider" /> + + <!-- <div class="ui hidden divider" /> --> + <div class="sub header prewrap"> <span - v-if="fastEditionMode && form" + v-if="fastEditionMode && canEditFeature && form" class="form ui half-block" > <textarea @@ -196,12 +207,23 @@ export default { type: Boolean, default: false, }, + isFeatureCreator: { + type: Boolean, + default: false, + }, + canEditFeature: { + type: Boolean, + default: false, + }, + canDeleteFeature: { + type: Boolean, + default: false, + }, }, computed: { ...mapState([ 'user', - 'USER_LEVEL_PROJECTS', 'isOnline', ]), ...mapState('feature', [ @@ -212,24 +234,6 @@ export default { 'permissions', ]), - isFeatureCreator() { - if (this.currentFeature && this.user) { - return this.currentFeature.creator === this.user.id; - } - return false; - }, - - isModerator() { - return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Modérateur'; - }, - - userCanFastEdit() { - const superiorRoles = ['contributor', 'super_contributor', 'moderator', 'admin']; - return this.USER_LEVEL_PROJECTS && - superiorRoles.includes(this.USER_LEVEL_PROJECTS[this.$route.params.slug]) || - this.user.is_superuser; - }, - queryparams() { return this.$route.query.offset >= 0 ? { previous: parseInt(this.$route.query.offset) - 1, @@ -258,26 +262,45 @@ export default { updateDescription(e) { this.$store.commit('feature/UPDATE_FORM_FIELD', { name: 'description', value: e.target.value }); + }, + + validateFastEdition() { + this.$store.dispatch('feature/SEND_FEATURE', this.$route.name) + .then(() => this.$emit('updateEvents')); } } }; </script> <style> -#next-feature { - margin-right: .5rem !important; -} #feature-detail-to-features-list { line-height: 0; margin-right: 5px; } -.half-block { - display: inline-block; - width: 50%; -} #feature_detail_title_input { font-weight: bold; font-size: 2em; padding: .25em; } +.two-block { + display: flex; + justify-content: space-between; + margin-bottom: .5em; +} +#feature-actions > div { + margin-left: .5rem; +} +#feature-actions .no-opacity { + opacity: 1 !important; /* overide disabled low opacity to customize button style */ +} + +@media screen and (max-width: 700px) { + .two-block { + flex-direction: column-reverse; + } + #feature-actions.ui.buttons { + flex-direction: column; + align-items: flex-end; + } +} </style> \ No newline at end of file diff --git a/src/components/Feature/Detail/FeatureTable.vue b/src/components/Feature/Detail/FeatureTable.vue index fd124755b3a821ca21da6b3eaab97b48a6e1a627..b0e4a8b8e963be24bac5a544dd6464c03d244929 100644 --- a/src/components/Feature/Detail/FeatureTable.vue +++ b/src/components/Feature/Detail/FeatureTable.vue @@ -23,7 +23,8 @@ <td> <strong class="ui form"> <span - v-if="fastEditionMode && extra_forms.length > 0" + v-if="fastEditionMode && canEditFeature && extra_forms.length > 0" + :id="field.label" > <FeatureExtraForm :field="getExtraForm(field)" @@ -60,7 +61,7 @@ aria-hidden="true" /> <FeatureEditStatusField - v-if="fastEditionMode && form" + v-if="fastEditionMode && canEditFeature && form" :status="form.status.value" class="inline" /> @@ -115,6 +116,13 @@ {{ link.feature_to.created_on }}) </td> </tr> + <tr v-if="linked_features.length === 0"> + <td> + <em> + Aucune liaison associée au signalement. + </em> + </td> + </tr> </tbody> </table> </div> @@ -154,7 +162,11 @@ export default { fastEditionMode: { type: Boolean, default: false, - } + }, + canEditFeature: { + type: Boolean, + default: false, + }, }, computed: { diff --git a/src/components/Feature/Edit/FeatureExtraForm.vue b/src/components/Feature/Edit/FeatureExtraForm.vue index c578e5c75f6642764c7c6204a8e61e0713462c49..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" @@ -63,19 +55,14 @@ :name="field.name" @change="updateStore_extra_form" > - <label - v-if="$route.name === 'editer-signalement'" - :for="field.name" - > - {{ field.label }} + <label :for="field.name"> + {{ 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 }} @@ -88,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 }} @@ -108,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 }} @@ -155,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/Feature/FeatureEditStatusField.vue b/src/components/Feature/FeatureEditStatusField.vue index 806182c3a835cd04a9b8a3d36c3531b7bb7b94bb..3c4133b5c7ff9b8a459ebca9733bd74268604c98 100644 --- a/src/components/Feature/FeatureEditStatusField.vue +++ b/src/components/Feature/FeatureEditStatusField.vue @@ -1,5 +1,8 @@ <template> - <div class="field"> + <div + id="status" + class="field" + > <Dropdown v-if="selectedStatus" :options="allowedStatusChoices" @@ -56,7 +59,7 @@ export default { allowedStatusChoices() { if (this.project && this.currentFeature && this.user) { const isModerate = this.project.moderation; - const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug]; + const userStatus = this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.project.slug]; const isOwnFeature = this.currentFeature.creator === this.user.id; //* si le contributeur est l'auteur du signalement return allowedStatus2change(this.user, isModerate, userStatus, isOwnFeature, /* this.currentRouteName */); } diff --git a/src/components/FeatureType/FeatureTypeCustomForm.vue b/src/components/FeatureType/FeatureTypeCustomForm.vue index d064ee6ca7a27c946f98d3543e65621a946da83b..51f068a1460a50452a0bd20da3def1f4302fb3ea 100644 --- a/src/components/FeatureType/FeatureTypeCustomForm.vue +++ b/src/components/FeatureType/FeatureTypeCustomForm.vue @@ -312,7 +312,7 @@ export default { fillCustomFormData(customFormData) { for (const el in customFormData) { - if (el && this.form[el] && customFormData[el]) { + if (el && this.form[el] && customFormData[el] !== undefined && customFormData[el] !== null) { //* check if is an object, because data from api is a string, while import from django is an object this.form[el].value = customFormData[el].value ? customFormData[el].value @@ -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/ImportTask.vue b/src/components/ImportTask.vue index 026b7cbfe30e8b8ffcc4e7354b33642f2a7b13ec..c5792c73764138b2c6d00795e1109a1222dda73f 100644 --- a/src/components/ImportTask.vue +++ b/src/components/ImportTask.vue @@ -26,7 +26,9 @@ <td> <h4 class="ui header align-right"> <div :data-tooltip="importFile.geojson_file_name"> - {{ importFile.geojson_file_name | subString }} + <div class="ellipsis"> + {{ importFile.geojson_file_name | subString }} + </div> <div class="sub header"> ajouté le {{ importFile.created_on | setDate }} </div> @@ -234,5 +236,9 @@ and also iPads specifically. .margin-left { margin-left: 94%; } + h4.ui.header { + margin-left: 60px; + white-space: nowrap; + } } </style> diff --git a/src/components/Project/Detail/ProjectFeatureTypes.vue b/src/components/Project/Detail/ProjectFeatureTypes.vue index 7cb7968533a7c1f3a17d9d919486a52e05b3075b..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 { @@ -633,7 +634,6 @@ export default { try { fr.readAsText(this.csvFileToImport); fr.onloadend = () => { - // Find csv delimiter const commaDelimited = fr.result.split('\n')[0].includes(','); const semicolonDelimited = fr.result.split('\n')[0].includes(';'); @@ -644,19 +644,16 @@ export default { this.featureTypeImporting = false; return; } - // Check if file contains 'lat' and 'long' fields - const headersLine = - fr.result - .split('\n')[0] - .split(delimiter) - .map(el => { - return el.replace('\r', ''); - }) - .filter(el => { - return el === 'lat' || el === 'lon'; - }); - + const headers = fr.result + .split('\n')[0] + .split(delimiter) + .map(el => { + return el.replace('\r', ''); + }); + const headersCoord = headers.filter(el => { + return el === 'lat' || el === 'lon'; + }); // Look for 2 decimal fields in first line of csv // corresponding to lon and lat const sampleLine = @@ -667,9 +664,13 @@ export default { return !isNaN(el) && el.indexOf('.') !== -1; }) .filter(Boolean); - if (sampleLine.length > 1 && headersLine.length === 2) { + 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 fff79a991ecb1479945455bb91747a99b68c5ed4..69cd85d84687014a27a3c979b22cff889214d757 100644 --- a/src/components/Project/FeaturesListAndMap/FeatureListTable.vue +++ b/src/components/Project/FeaturesListAndMap/FeatureListTable.vue @@ -207,6 +207,7 @@ feature_type_slug: feature.feature_type.slug, }, }" + class="ellipsis space-left" > {{ feature.feature_type.title }} </router-link> @@ -220,6 +221,7 @@ }, query: { ...queryparams, offset: queryparams.offset + index } }" + class="ellipsis space-left" > {{ feature.title || feature.feature_id }} </router-link> @@ -465,7 +467,9 @@ export default { Contributeur : ['draft', 'pending', 'published'], }; - if (this.userStatus === 'Contributeur' && feature.display_creator !== this.user.username) { + if (this.user.is_superuser) { + return true; + } else if (this.userStatus === 'Contributeur' && feature.display_creator !== `${this.user.first_name} ${this.user.last_name}`) { return false; } else if (permissions[this.userStatus]) { return permissions[this.userStatus].includes(feature.status); @@ -725,6 +729,11 @@ and also iPads specifically. text-align: center; margin: .5em 0; } + .space-left { + max-width: 100%; + display: inline-block; + padding-left: 3em; + } } @media only screen and (max-width: 410px) { .ui.table tr td { diff --git a/src/components/Projects/ProjectsListItem.vue b/src/components/Projects/ProjectsListItem.vue index 983186fe97b8adbe9363b8071e465896aad1ddeb..fe3e4d4e908ed04287a73b0e99a2c57f616d1c44 100644 --- a/src/components/Projects/ProjectsListItem.vue +++ b/src/components/Projects/ProjectsListItem.vue @@ -23,7 +23,7 @@ <div class="description"> <p>{{ project.description }}</p> </div> - <div class="meta"> + <div class="meta top"> <span class="right floated"> <strong v-if="project.moderation">Projet modéré</strong> <strong v-else>Projet non modéré</strong> @@ -110,3 +110,36 @@ export default { }; </script> + +<style lang="less" scoped> + +.description { + p { + text-align: justify; + } +} + +@media screen and (max-width: 767px) { + .content { + width: 90% !important; + + .meta.top { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + + .right.floated { + float: none !important; + margin-left: 0 !important; + margin-bottom: 0.5em; + } + + span { + margin: 0.15em 0; + } + } + } +} + +</style> diff --git a/src/components/Projects/ProjectsMenu.vue b/src/components/Projects/ProjectsMenu.vue index 442ab3a5a7e68e24e0c26a5a4ec8cad38f6d0a64..ece71b68de5d7d37283af90a410e65280395f4a9 100644 --- a/src/components/Projects/ProjectsMenu.vue +++ b/src/components/Projects/ProjectsMenu.vue @@ -1,18 +1,21 @@ <template> - <div class="filters-container"> - <div class="ui styled accordion"> + <div id="filters-container"> + <div + class="ui styled accordion" + @click="displayFilters = !displayFilters" + > <div id="filters" class="title collapsible-filters" > FILTRES <i - class="ui icon caret right down" + :class="['ui icon customcaret', { 'collapsed': !displayFilters }]" aria-hidden="true" /> </div> </div> - <div class="ui menu filters hidden"> + <div :class="['ui menu filters', { 'hidden': displayFilters }]"> <div class="item"> <label> Niveau d'autorisation requis @@ -40,7 +43,7 @@ v-on="$listeners" /> </div> - <div class="right item"> + <div class="item"> <label> Recherche par nom </label> @@ -69,6 +72,7 @@ export default { data() { return { + displayFilters: false, moderationOptions: [ { label: 'Tous', @@ -157,26 +161,10 @@ export default { } }, - mounted() { - const el = document.getElementsByClassName('collapsible-filters'); - - el[0].addEventListener('click', function() { - const icon = document.getElementsByClassName('caret'); - icon[0].classList.toggle('right'); - const content = document.getElementsByClassName('filters'); - content[0].classList.toggle('hidden'); - if (content[0].style.maxHeight){ - content[0].style.maxHeight = null; - } else { - content[0].style.maxHeight = content[0].scrollHeight + 5 + 'px'; - } - }); - }, - methods: { ...mapActions('projects', [ 'SEARCH_PROJECTS' - ]) + ]), } }; </script> @@ -189,7 +177,7 @@ export default { transition: @arguments; } -.filters-container { +#filters-container { width: 100%; display: flex; flex-direction: column; @@ -200,6 +188,23 @@ export default { .collapsible-filters { font-size: 1.25em; padding-right: 0; + .customcaret{ + transition: transform .2s ease; + &.collapsed { + transform: rotate(180deg); + } + &::before{ + position: relative; + right: 0; + top: 65%; + color: #999; + margin-top: 4px; + border-color: #999 transparent transparent; + border-style: solid; + border-width: 5px 5px 0; + content: ""; + } + } } } .filters { @@ -207,16 +212,16 @@ export default { height:auto; min-height: 0; max-height:75px; + opacity: 1; margin: 0 0 1em 0; border: none; box-shadow: none; - .transition-properties(max-height 0.2s ease-out;); + .transition-properties(all 0.2s ease-out;); .item { display: flex; flex-direction: column; align-items: flex-start !important; - - padding: 0.4em 0.6em 0.4em 0; + padding: 0.5em; label { margin-bottom: 0.2em; @@ -230,20 +235,43 @@ export default { .item::before { width: 0; } - .right.item { - padding-right: 0; - #search-projects { - width: 100%; - } + #search-projects { + width: 100%; } - .right.item::before { - width: 0; - } } .filters.hidden { - max-height: 0; overflow: hidden; - border: none; + opacity: 0; + max-height: 0; } } + +@media screen and (min-width: 701px) { + .item { + &:first-child { + padding-left: 0; + } + &:last-child { + padding-right: 0; + } + } +} + +@media screen and (max-width: 700px) { + #filters-container { + + .filters { + display: flex; + flex-direction: column; + max-height: 275px; + .transition-properties(all 0.2s ease-out;); + + .item { + width: 100%; + padding-right: 0; + padding-left: 0; + } + } + } +} </style> diff --git a/src/main.js b/src/main.js index 3b9bb6b8e077630ae2382f9dbcdb3cd6aa34df91..cabb80a8ddc6293333db579cbb747d5852b9590b 100644 --- a/src/main.js +++ b/src/main.js @@ -44,7 +44,7 @@ const onConfigLoaded = function(config){ store.commit('SET_CONFIG', config); setInterval(() => { //* check if navigator is online store.commit('SET_IS_ONLINE', navigator.onLine); - }, 5000); + }, 2000); // set title and favico document.title = `${config.VUE_APP_APPLICATION_NAME} ${config.VUE_APP_APPLICATION_ABSTRACT}`; diff --git a/src/services/map-service.js b/src/services/map-service.js index 0bb14fa3a7276e2d9ce9a354d9eb4e0e3e584e2a..63e4204c00689cba3b4120faf5aa77b7abc72ef1 100644 --- a/src/services/map-service.js +++ b/src/services/map-service.js @@ -51,6 +51,7 @@ const mapService = { lng, mapDefaultViewCenter, mapDefaultViewZoom, + maxZoom, zoom, zoomControl = true, interactions = { doubleClickZoom: false, mouseWheelZoom: false, dragPan: true }, @@ -60,7 +61,7 @@ const mapService = { el.innerHTML = ''; } - this.map = new Map({ + const mapOptions = { layers: [], target: el, controls: [ @@ -70,14 +71,17 @@ const mapService = { })], interactions: defaults(interactions), view: new View({ - center: transform([ - !lng ? mapDefaultViewCenter[1] : lng, - !lat ? mapDefaultViewCenter[0] : lat, + center: transform([ //* since 0 is considered false, check for number instead of just defined (though boolean will pass through) + Number(lng) ? lng : mapDefaultViewCenter[1], + Number(lat) ? lat : mapDefaultViewCenter[0], ], 'EPSG:4326', 'EPSG:3857'), - zoom: !zoom ? mapDefaultViewZoom : zoom + zoom: Number(mapDefaultViewZoom) ? mapDefaultViewZoom : zoom, + maxZoom }), - }); + }; + + this.map = new Map(mapOptions); if (zoomControl) { this.map.addControl(new Zoom({ zoomInTipLabel: 'Zoomer', zoomOutTipLabel: 'Dézoomer' })); @@ -557,7 +561,12 @@ const mapService = { </div> ${author} `; - const featureId = feature.getProperties ? feature.getProperties().feature_id || feature.getId() : feature.id; //* feature.id was used with leaflet, with ol feature.getId replace it, but keeping it as fallback can prevent regression + const featureId = + feature.getId() ? + feature.getId() : + feature.getProperties ? + feature.getProperties().feature_id : + feature.id; return { html, feature_type, featureId }; }, diff --git a/src/store/modules/feature.store.js b/src/store/modules/feature.store.js index ef93083a8691c11c3de37a5dbd79082f3058b67f..2466c7c6ad6e4b2c81188aadf9fe694b73d96e6f 100644 --- a/src/store/modules/feature.store.js +++ b/src/store/modules/feature.store.js @@ -215,7 +215,6 @@ const feature = { params: { slug_type_signal: rootState['feature-type'].current_feature_type_slug, slug_signal: featureId, - message: routeName === 'ajouter-signalement' ? 'Le signalement a été crée' : 'Le signalement a été mis à jour' }, }); }); @@ -232,7 +231,6 @@ const feature = { for (const field of state.extra_forms) { extraFormObject[field.name] = field.value; } - //const feature = state.form || state.currentFeature; return { id: state.form.feature_id || state.currentFeature.feature_id, type: 'Feature', diff --git a/src/store/modules/map.store.js b/src/store/modules/map.store.js index a8b3f37b29b4f49fef161c4d5f47d192182e7030..3ae7994329dc93751cd400d5c39892bf842b2850 100644 --- a/src/store/modules/map.store.js +++ b/src/store/modules/map.store.js @@ -104,12 +104,13 @@ const map = { }); }, - INITIATE_MAP({ commit }, el) { //todo: since this function is not anymore called in different components, it would better to move it in project_details.vue + INITIATE_MAP({ commit, rootState }, el) { //todo: since this function is not anymore called in different components, it would better to move it in project_details.vue const mapDefaultViewCenter = [46, 2]; // defaultMapView.center; const mapDefaultViewZoom = 5; // defaultMapView.zoom; mapService.createMap(el, { mapDefaultViewCenter: mapDefaultViewCenter, mapDefaultViewZoom: mapDefaultViewZoom, + maxZoom: rootState.projects.project.map_max_zoom_level, }); const map = { ...mapService.getMap() }; commit('SET_MAP', map); 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/FeatureDetail.vue b/src/views/Feature/FeatureDetail.vue index dc39b0e4733573a96363fa432c3dd587fb2ad6e7..fd547578dd6184020837ccb692fce61e37dcd08f 100644 --- a/src/views/Feature/FeatureDetail.vue +++ b/src/views/Feature/FeatureDetail.vue @@ -13,8 +13,12 @@ :feature-type="featureType" :fast-edition-mode="project.fast_edition_mode" :display-to-list-button="displayToListButton" - @setIsCancelling="isCanceling = true" + :is-feature-creator="isFeatureCreator" + :can-edit-feature="canEditFeature" + :can-delete-feature="canDeleteFeature" + @setIsDeleting="isDeleting = true" @tofeature="pushNgo" + @updateEvents="getFeatureEvents" /> </div> </div> @@ -24,6 +28,7 @@ v-if="project" :feature-type="featureType" :fast-edition-mode="project.fast_edition_mode" + :can-edit-feature="canEditFeature" @tofeature="pushNgo" /> </div> @@ -60,28 +65,33 @@ /> </div> </div> + <div - v-if="isCanceling" + v-if="isDeleting" class="ui dimmer modals visible active" > <div :class="[ - 'ui mini modal subscription', - { 'active visible': isCanceling }, + 'ui mini modal', + { 'active visible': isDeleting }, ]" > <i class="close icon" aria-hidden="true" - @click="isCanceling = false" + @click="isDeleting = false" /> - <div class="ui icon header"> + <div + v-if="isDeleting" + class="ui icon header" + > <i class="trash alternate icon" aria-hidden="true" /> Supprimer le signalement </div> + <div class="actions"> <button type="button" @@ -93,7 +103,61 @@ </div> </div> </div> + + <div + v-if="isLeaving" + class="ui dimmer modals visible active" + > + <div + :class="[ + 'ui mini modal', + { 'active visible': isLeaving }, + ]" + > + <i + class="close icon" + aria-hidden="true" + @click="isLeaving = false" + /> + <div class="ui icon header"> + <i + :class="[project.fast_edition_mode && hasUnsavedChange ? 'sign-out' : 'random', 'icon']" + aria-hidden="true" + /> + Abandonner {{ + project.fast_edition_mode && hasUnsavedChange ? + 'les modifications' : + 'la vue signalement filtré' + }} + </div> + <div class="content"> + {{ + project.fast_edition_mode && hasUnsavedChange ? + 'Les modifications apportées au signalement ne seront pas sauvegardées, continuer ?': + `Vous allez quittez la vue signalement filtré, + l\'ordre des signalements pourrait changer après édition d\'un signalement.` + }} + </div> + <div class="actions"> + <button + type="button" + class="ui green compact button" + @click="stayOnPage" + > + Annuler + </button> + <button + type="button" + class="ui red compact button" + @click="leavePage" + > + Continuer + </button> + </div> + </div> + </div> </div> + <div v-else> Pas de signalement correspondant trouvé </div> @@ -101,7 +165,7 @@ </template> <script> -import { mapState, mapActions, mapMutations } from 'vuex'; +import { mapState, mapActions, mapMutations, mapGetters } from 'vuex'; import mapService from '@/services/map-service'; import axios from '@/axios-client.js'; @@ -136,19 +200,19 @@ export default { }, beforeRouteUpdate (to, from, next) { - let leaving = true; // by default navigate to next route if (this.hasUnsavedChange) { - leaving = this.confirmLeave(); // prompt user that there is unsaved changes or that features order might change + this.confirmLeave(next); // prompt user that there is unsaved changes or that features order might change + } else { + next(); // continue navigation } - next(leaving); }, beforeRouteLeave (to, from, next) { - let leaving = true; // by default navigate to next route if (this.hasUnsavedChange || (from.query.offset >= 0 && to.name === 'editer-signalement')) { - leaving = this.confirmLeave(); // prompt user that there is unsaved changes or that features order might change + this.confirmLeave(next); // prompt user that there is unsaved changes or that features order might change + } else { + next(); // continue navigation } - next(leaving); }, data() { @@ -170,27 +234,37 @@ export default { events: [], featureType: {}, featuresCount: null, - isCanceling: false, + isDeleting: false, + isLeaving: false, slugSignal: '', displayToListButton: false, }; }, computed: { + ...mapState([ + 'USER_LEVEL_PROJECTS', + 'user' + ]), ...mapState('projects', [ 'project' ]), ...mapState('feature-type', [ 'feature_types', - 'feature_type', ]), ...mapState('feature', [ 'currentFeature', 'form', ]), + ...mapGetters('feature-type', [ + 'feature_type', + ]), + ...mapGetters([ + 'permissions', + ]), hasUnsavedChange() { - if (this.form) { + if (this.project.fast_edition_mode && this.form && this.currentFeature) { if (this.form.title !== this.currentFeature.title) return true; if (this.form.description.value !== this.currentFeature.description) return true; if (this.form.status.value !== this.currentFeature.status) return true; @@ -200,6 +274,36 @@ export default { } } return false; + }, + + isFeatureCreator() { + if (this.currentFeature && this.user) { + return this.currentFeature.creator === this.user.id; + } + return false; + }, + + isModerator() { + return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Modérateur'; + }, + + isAdministrator() { + return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Administrateur projet'; + }, + + canEditFeature() { + return (this.permissions && this.permissions.can_update_feature) || + this.isFeatureCreator || + this.isModerator || + this.user.is_superuser; + }, + + canDeleteFeature() { + return (this.permissions && this.permissions.can_delete_feature && this.isFeatureCreator) || + this.isFeatureCreator || + this.isModerator || + this.isAdministrator || + this.user.is_superuser; } }, @@ -283,10 +387,18 @@ export default { } }, - confirmLeave() { - return window.confirm(this.project.fast_edition_mode && this.hasUnsavedChange ? - 'Les modifications apportées au signalement ne seront pas sauvegardées, continuer ?': - 'Vous allez quittez la vue signalement filtré, l\'ordre des signalements pourrait changer après édition d\'un signalement.'); + confirmLeave(next) { + this.next = next; + this.isLeaving = true; + }, + + stayOnPage() { + this.isLeaving = false; + }, + + leavePage() { + this.isLeaving = false; + this.next(); }, async reloadPage() { @@ -354,6 +466,7 @@ export default { this.map = mapService.createMap(this.$refs.map, { mapDefaultViewCenter, mapDefaultViewZoom, + maxZoom: this.project.map_max_zoom_level, interactions : { doubleClickZoom :false, mouseWheelZoom: false, diff --git a/src/views/Feature/FeatureEdit.vue b/src/views/Feature/FeatureEdit.vue index ce39380330ef83d5ac10aa38060f513bc8308e56..35eaa28cc17c4b6e0c15fdfb9ed527e20c95f8b0 100644 --- a/src/views/Feature/FeatureEdit.vue +++ b/src/views/Feature/FeatureEdit.vue @@ -264,9 +264,12 @@ <div v-for="(field, index) in orderedCustomFields" :key="field.field_type + index" - class="field" > - <FeatureExtraForm :field="field" /> + <FeatureExtraForm + :id="field.label" + :field="field" + class="field" + /> {{ field.errors }} </div> @@ -860,6 +863,7 @@ export default { this.map = mapService.createMap(this.$refs.map, { mapDefaultViewCenter, mapDefaultViewZoom, + maxZoom: this.project.map_max_zoom_level, interactions : { doubleClickZoom :false, mouseWheelZoom:true, dragPan:true } }); const currentFeatureId = this.$route.params.slug_signal; diff --git a/src/views/FeatureType/FeatureTypeDetail.vue b/src/views/FeatureType/FeatureTypeDetail.vue index 06b302ae1983a657f27122ef07ea442b8d111e6c..97db52d8babd1925b82899592d1a4b3ee069e619 100644 --- a/src/views/FeatureType/FeatureTypeDetail.vue +++ b/src/views/FeatureType/FeatureTypeDetail.vue @@ -370,12 +370,14 @@ </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', @@ -557,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) => { @@ -588,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 @@ -613,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) @@ -640,7 +621,7 @@ export default { return false; } const sampleLine = - csv + csvString .split('\n')[1] .split(delimiter) .map(el => { @@ -648,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 5682366fa74a2f7e5540ddcefbd9f87884d11fda..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', @@ -247,6 +248,7 @@ export default { 'archived_on', 'deletion_on', 'feature_type', + 'feature_id', 'display_creator', 'display_last_editor', 'project', @@ -638,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() { @@ -664,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 f86c9244e9e77fd13ad692680ef7208400f8ab04..c48c19edff621efe27fed3c0309026c3e0463d23 100644 --- a/src/views/Project/FeaturesListAndMap.vue +++ b/src/views/Project/FeaturesListAndMap.vue @@ -62,7 +62,7 @@ > <div :class="[ - 'ui mini modal subscription', + 'ui mini modal', { 'active visible': isDeleteModalOpen }, ]" > @@ -180,10 +180,11 @@ export default { mounted() { if (!this.project) { // Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh - this.$store.dispatch('projects/GET_PROJECT', this.projectSlug); - this.$store - .dispatch('projects/GET_PROJECT_INFO', this.projectSlug) - .then(() => this.initMap()); + Promise.all([ + this.$store.dispatch('projects/GET_PROJECT', this.projectSlug), + this.$store.dispatch('projects/GET_PROJECT_INFO', this.projectSlug) + ]) + .then(()=> this.initMap()); } else { this.initMap(); } @@ -303,6 +304,7 @@ export default { lng: this.lng, mapDefaultViewCenter, mapDefaultViewZoom, + maxZoom: this.project.map_max_zoom_level, interactions : { doubleClickZoom :false,mouseWheelZoom:true,dragPan:true } }); @@ -485,6 +487,9 @@ export default { margin-left: 50%; visibility: hidden; position: absolute; + #map { + min-height: 0; + } } .map-container.visible { visibility: visible; diff --git a/src/views/Project/ProjectEdit.vue b/src/views/Project/ProjectEdit.vue index 9a6de25196772b62b9d9f30eb3fea53d3fc7eb72..0e788f3f84a33b747d170945eb5d315fef982b37 100644 --- a/src/views/Project/ProjectEdit.vue +++ b/src/views/Project/ProjectEdit.vue @@ -156,55 +156,74 @@ </div> </div> - <div class="field"> - <div class="ui checkbox"> - <input - id="moderation" - v-model="form.moderation" - class="hidden" - type="checkbox" - name="moderation" - > - <label for="moderation">Modération</label> - </div> - </div> - - <div class="field"> - <div class="ui checkbox"> - <input - id="is_project_type" - v-model="form.is_project_type" - class="hidden" - type="checkbox" - name="is_project_type" - > - <label for="is_project_type">Est un projet type</label> - </div> - </div> - - <div class="field"> - <div class="ui checkbox"> - <input - id="generate_share_link" - v-model="form.generate_share_link" - class="hidden" - type="checkbox" - name="generate_share_link" - > - <label for="generate_share_link">Génération d'un lien de partage externe</label> + <div class="two fields"> + <div class="fields grouped"> + <div class="field"> + <div class="ui checkbox"> + <input + id="moderation" + v-model="form.moderation" + class="hidden" + type="checkbox" + name="moderation" + > + <label for="moderation">Modération</label> + </div> + </div> + + <div class="field"> + <div class="ui checkbox"> + <input + id="is_project_type" + v-model="form.is_project_type" + class="hidden" + type="checkbox" + name="is_project_type" + > + <label for="is_project_type">Est un projet type</label> + </div> + </div> + + <div class="field"> + <div class="ui checkbox"> + <input + id="generate_share_link" + v-model="form.generate_share_link" + class="hidden" + type="checkbox" + name="generate_share_link" + > + <label for="generate_share_link">Génération d'un lien de partage externe</label> + </div> + </div> + + <div class="field"> + <div class="ui checkbox"> + <input + id="fast_edition_mode" + v-model="form.fast_edition_mode" + class="hidden" + type="checkbox" + name="fast_edition_mode" + > + <label for="fast_edition_mode">Mode d'édition rapide de signalements</label> + </div> + </div> </div> - </div> - <div class="field"> - <div class="ui checkbox"> - <input - id="fast_edition_mode" - v-model="form.fast_edition_mode" - class="hidden" - type="checkbox" - name="fast_edition_mode" - > - <label for="fast_edition_mode">Mode d'édition rapide de signalements</label> + <div class="field required"> + <label>Niveau de zoom maximum de la carte</label> + <div class="range-container"> + <input + v-model="form.map_max_zoom_level" + type="range" + min="0" + max="22" + step="1" + ><output class="range-output-bubble">{{ + form.map_max_zoom_level + }}</output> + </div> </div> </div> @@ -267,6 +286,7 @@ export default { access_level_arch_feature: { name: '', value: '' }, archive_feature: 0, delete_feature: 0, + map_max_zoom_level: 22, nb_features: 0, nb_published_features: 0, nb_comments: 0, @@ -520,6 +540,7 @@ export default { access_level_pub_feature: this.form.access_level_pub_feature.value, archive_feature: this.form.archive_feature, delete_feature: this.form.delete_feature, + map_max_zoom_level: this.form.map_max_zoom_level, is_project_type: this.form.is_project_type, generate_share_link: this.form.generate_share_link, fast_edition_mode: this.form.fast_edition_mode, diff --git a/src/views/Project/ProjectMembers.vue b/src/views/Project/ProjectMembers.vue index 679f9a59f0296d909dc87961b118b035cbe6dad2..adf13ec5c98f698da9682758e614b4c514ee75c4 100644 --- a/src/views/Project/ProjectMembers.vue +++ b/src/views/Project/ProjectMembers.vue @@ -301,6 +301,11 @@ export default { }, saveMembers() { + this.$store.commit( + 'DISPLAY_LOADER', + 'Mise à jour des membres du projet en cours ...' + ); + const data = this.projectUsers.map((member) => { return { user: member.user, @@ -329,8 +334,10 @@ export default { } ); } + this.$store.commit('DISCARD_LOADER'); }) .catch((error) => { + this.$store.commit('DISCARD_LOADER'); throw error; }); },