diff --git a/package.json b/package.json index 9c54f9e0edfd6ca848a1c84a8bcde9746b2e0b28..3aab9d9ac4a43cd270fd2e234771ef76aede3a57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geocontrib-frontend", - "version": "2.3.1", + "version": "2.3.2-rc1", "private": true, "scripts": { "serve": "npm run init-proxy & npm run init-serve", diff --git a/src/App.vue b/src/App.vue index b356b5c01613825879292d3e248f95963068fff8..349930d3af73b77c5b6e5aea03345776cb02a346 100644 --- a/src/App.vue +++ b/src/App.vue @@ -151,6 +151,7 @@ </header> <main> <div id="content" class="ui stackable grid centered container"> + <transition name="fadeDownUp"> <div v-if="messages && messages.length > 0" class="row"> <div class="fourteen wide column"> <div @@ -169,6 +170,7 @@ </div> </div> </div> + </transition> <div :class="{ active: loader.isLoading }" class="ui inverted dimmer"> <div class="ui text loader"> {{ loader.message }} @@ -373,5 +375,54 @@ footer { box-shadow: none !important; transition: none !important; } + + +.bounce-enter-active { + animation: bounce-in .5s; +} +.bounce-leave-active { + animation: bounce-in .5s reverse; +} +@keyframes bounce-in { + 0% { + transform: scale(0); + } + 50% { + transform: scale(1.5); + } + 100% { + transform: scale(1); + } +} + +.fadeDownUp-enter-active { + animation: fadeInDown .5s; +} +.fadeDownUp-leave-active { + animation: fadeOutUp .5s; +} +@keyframes fadeOutUp { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + transform: translate3d(0, -100%, 0); + } +} + +@keyframes fadeInDown { + from { + opacity: 0; + transform: translate3d(0, -100%, 0); + } + + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + </style> \ No newline at end of file diff --git a/src/assets/js/map-util.js b/src/assets/js/map-util.js index 007e92077be4966ccd56b99d188e39ba24dc9faa..10535ded16b11cdcfff46c84a9313c101265f0d2 100644 --- a/src/assets/js/map-util.js +++ b/src/assets/js/map-util.js @@ -49,7 +49,7 @@ L.TileLayer.BetterWMS = L.TileLayer.WMS.extend({ getFeatureInfo: function (evt) { if (this.wmsParams.basemapId != undefined) { - const queryableLayerSelected = document.getElementById(`queryable-layers-selector-${this.wmsParams.basemapId}`).getElementsByClassName('selected')[0].innerHTML; + const queryableLayerSelected = document.getElementById(`queryable-layers-selector-${this.wmsParams.basemapId}`).getElementsByClassName('selected')[0].textContent; if (queryableLayerSelected.trim() === this.wmsParams.title.trim()) { // Make an AJAX request to the server and hope for the best var params = this.getFeatureInfoUrl(evt.latlng); @@ -181,7 +181,7 @@ const mapUtil = { ], !zoom ? mapDefaultViewZoom : zoom ); - + map.setMaxBounds( [[-90,-180], [90,180]] ) if (zoomControl) { L.control .zoom({ @@ -226,6 +226,7 @@ const mapUtil = { layers.forEach((layer) => { if (layer) { const options = layer.options; + options.noWrap=true; if (options) { options.opacity = layer.opacity; @@ -250,6 +251,7 @@ const mapUtil = { } }); } else { + optionsMap.noWrap=true; L.tileLayer(serviceMap, optionsMap).addTo(map); } }, @@ -307,6 +309,7 @@ const mapUtil = { addVectorTileLayer: function (url, project_slug, featureTypes, form_filters) { layerMVT = L.vectorGrid.protobuf(url, { + noWrap:true, vectorTileLayerStyles: { "default": (properties) => { const featureType = featureTypes.find((x) => x.slug.split('-')[0] === '' + properties.feature_type_id); diff --git a/src/components/ImportTask.vue b/src/components/ImportTask.vue index 693df72ea8f4d5af0308f219582ccd5022f4bcf4..8c15409397ac5d0d13a97a7f65e8633ab81fbc6c 100644 --- a/src/components/ImportTask.vue +++ b/src/components/ImportTask.vue @@ -103,7 +103,8 @@ export default { feature_type: this.$route.params.feature_type_slug }); this.$store.dispatch('feature/GET_PROJECT_FEATURES', { - project_slug: this.$route.params.slug + project_slug: this.$route.params.slug, + feature_type__slug: this.$route.params.feature_type_slug }) //* show that the action was triggered, could be improved with animation (doesn't work) this.ready = false; diff --git a/src/components/feature/FeatureListTable.vue b/src/components/feature/FeatureListTable.vue index dfba1b87b37cd5777d577bf6c5c9d0bc09068156..5817a4fa2ab3f7acb9aadf77cef804e68c27e0be 100644 --- a/src/components/feature/FeatureListTable.vue +++ b/src/components/feature/FeatureListTable.vue @@ -10,33 +10,33 @@ Statut <i :class="{ - down: isSortedAsc('statut'), - up: isSortedDesc('statut'), + down: isSortedAsc('status'), + up: isSortedDesc('status'), }" class="icon sort" - @click="changeSort('statut')" + @click="changeSort('status')" /> </th> <th class="center"> Type <i :class="{ - down: isSortedAsc('type'), - up: isSortedDesc('type'), + down: isSortedAsc('feature_type'), + up: isSortedDesc('feature_type'), }" class="icon sort" - @click="changeSort('type')" + @click="changeSort('feature_type')" /> </th> <th class="center"> Nom <i :class="{ - down: isSortedAsc('nom'), - up: isSortedDesc('nom'), + down: isSortedAsc('title'), + up: isSortedDesc('title'), }" class="icon sort" - @click="changeSort('nom')" + @click="changeSort('title')" /> </th> <th class="center"> @@ -75,7 +75,7 @@ </tr> </thead> <tbody> - <tr v-for="(feature, index) in sortedFeatures" :key="index"> + <tr v-for="(feature, index) in paginatedFeatures" :key="index"> <td class="center"> <div class="ui checkbox" @@ -153,7 +153,6 @@ > </td> <td class="center"> - <!-- |date:'Ymd' --> {{ feature.properties.updated_on }} </td> <td class="center" v-if="user"> @@ -187,7 +186,7 @@ class="dataTables_paginate paging_simple_numbers" > <a - @click="$emit('update:page', 'previous');" + @click="$emit('update:page', 'previous')" id="table-features_previous" :class="[ 'paginate_button previous', @@ -199,8 +198,20 @@ >Précédent</a > <span> + <span v-if="pagination.currentPage >= 5"> + <a + key="page1" + @click="$emit('update:page', 1)" + class="paginate_button" + aria-controls="table-features" + data-dt-idx="1" + tabindex="0" + >{{ 1 }}</a + > + <span class="ellipsis">…</span> + </span> <a - v-for="pageNumber in pageNumbers" + v-for="pageNumber in displayedPageNumbers" :key="'page' + pageNumber" @click="$emit('update:page', pageNumber)" :class="[ @@ -212,8 +223,19 @@ tabindex="0" >{{ pageNumber }}</a > + <span v-if="(lastPageNumber - pagination.currentPage) >= 4"> + <span class="ellipsis">…</span> + <a + :key="'page' + lastPageNumber" + @click="$emit('update:page', lastPageNumber)" + class="paginate_button" + aria-controls="table-features" + data-dt-idx="1" + tabindex="0" + >{{ lastPageNumber }}</a + > + </span> </span> - <a id="table-features_next" :class="[ @@ -223,7 +245,7 @@ aria-controls="table-features" data-dt-idx="7" tabindex="0" - @click="$emit('update:page', 'next');" + @click="$emit('update:page', 'next')" >Suivant</a > </div> @@ -241,18 +263,9 @@ export default { "checkedFeatures", "featuresCount", "pagination", - "pageNumbers", + "sort", ], - data() { - return { - sort: { - column: "", - ascending: true, - }, - }; - }, - computed: { ...mapState(["user"]), ...mapGetters(["project", "permissions"]), @@ -261,50 +274,6 @@ export default { return this.permissions.is_project_administrator; }, - sortedFeatures() { - let sortedFeatures = [...this.paginatedFeatures]; - // Ajout du tri - if (this.sort.column !== "") { - sortedFeatures = sortedFeatures.sort((a, b) => { - let aProp = this.getFeatureDisplayName(a); - let bProp = this.getFeatureDisplayName(b); - if (this.sort.column === "statut") { - aProp = a.properties.status.value; - bProp = b.properties.status.value; - } else if (this.sort.column === "type") { - aProp = a.properties.feature_type.title; - bProp = b.properties.feature_type.title; - } else if (this.sort.column === "updated_on") { - aProp = a.properties.updated_on; - bProp = b.properties.updated_on; - } else if (this.sort.column === "display_creator") { - aProp = a.properties.display_creator; - bProp = b.properties.display_creator; - } - //ascending - if (this.sort.ascending) { - if (aProp < bProp) { - return -1; - } - if (aProp > bProp) { - return 1; - } - return 0; - } else { - //descending - if (aProp < bProp) { - return 1; - } - if (aProp > bProp) { - return -1; - } - return 0; - } - }); - } - return sortedFeatures; - }, - checked: { get() { return this.checkedFeatures; @@ -319,6 +288,36 @@ export default { ? this.featuresCount : this.pagination.end; }, + + pageNumbers() { + const totalPages = Math.ceil( + this.featuresCount / this.pagination.pagesize + ); + return [...Array(totalPages).keys()].map((pageNumb) => { + ++pageNumb; + return pageNumb; + }); + }, + + lastPageNumber() { + return this.pageNumbers.slice(-1)[0]; + }, + + displayedPageNumbers() { + //* si la page courante est inférieur à 5, la liste commence à l'index 0 et on retourne 5 pages + let firstPageInList = 0; + let pagesQuantity = 5; + //* à partir de la 5ième page et jusqu'à la 4ième page avant la fin : n'afficher que 3 page entre les ellipses et la page courante doit être au milieu + if (this.pagination.currentPage >= 5 && !(this.lastPageNumber - this.pagination.currentPage < 4)) { + firstPageInList = this.pagination.currentPage - 2; + pagesQuantity = 3 + } + //* a partir de 4 résultat avant la fin afficher seulement les 5 derniers résultats + if (this.lastPageNumber - this.pagination.currentPage < 4) { + firstPageInList = this.lastPageNumber - 5; + } + return this.pageNumbers.slice(firstPageInList, firstPageInList + pagesQuantity); + }, }, methods: { @@ -342,10 +341,14 @@ export default { changeSort(column) { if (this.sort.column === column) { //changer order - this.sort.ascending = !this.sort.ascending; + this.$emit("update:sort", { + column: this.sort.column, + ascending: !this.sort.ascending, + }); } else { this.sort.column = column; this.sort.ascending = true; + this.$emit("update:sort", { column, ascending: true }); } }, }, @@ -433,6 +436,10 @@ export default { box-shadow: none; } +.dataTables_wrapper .dataTables_paginate .ellipsis { + padding: 0 1em; +} + i.icon.sort:not(.down):not(.up) { color: rgb(220, 220, 220); } diff --git a/src/components/feature_type/SymbologySelector.vue b/src/components/feature_type/SymbologySelector.vue index 73d5ab4279e35eef88cc55a5d8c209f53da08460..7e730930a5b193d63e737131ce32735c825e764c 100644 --- a/src/components/feature_type/SymbologySelector.vue +++ b/src/components/feature_type/SymbologySelector.vue @@ -114,7 +114,7 @@ export default { created() { this.form.color.value = this.initColor; - this.form.icon = this.initIcon; + if (this.initIcon) this.form.icon = this.initIcon; this.$emit('set', { name: this.title, value: this.form diff --git a/src/store/modules/feature.js b/src/store/modules/feature.js index 30735fef4da3ee7b0715a189b148848cbe7284aa..427e005e781c9649968f5d0f1c4959e8775ae8ee 100644 --- a/src/store/modules/feature.js +++ b/src/store/modules/feature.js @@ -1,12 +1,6 @@ import axios from '@/axios-client.js'; import router from '../../router' -// axios.defaults.headers.common['X-CSRFToken'] = (name => { -// var re = new RegExp(name + "=([^;]+)"); -// var value = re.exec(document.cookie); -// return (value !== null) ? unescape(value[1]) : null; -// })('csrftoken'); - const feature = { namespaced: true, @@ -98,7 +92,6 @@ const feature = { }, ADD_ATTACHMENT_TO_DELETE(state, attachementId) { - // state.attachmentFormset = state.attachmentFormset.filter(el => el.id !== attachementId); state.attachmentsToDelete.push(attachementId); }, @@ -144,7 +137,6 @@ const feature = { commit("SET_FEATURES", features); const features_count = response.data.count; commit("SET_FEATURES_COUNT", features_count); - //dispatch("map/ADD_FEATURES", null, { root: true }); //todo: should check if map was initiated } return response; }) @@ -180,7 +172,6 @@ const feature = { SEND_FEATURE({ state, rootState, commit, dispatch }, routeName) { commit("DISPLAY_LOADER", "Le signalement est en cours de création", { root: true }) const message = routeName === "editer-signalement" ? "Le signalement a été mis à jour" : "Le signalement a été crée"; - function redirect(featureId) { dispatch( 'GET_PROJECT_FEATURE', @@ -228,110 +219,66 @@ const feature = { } } + let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/` if (routeName === "editer-signalement") { - return axios - .put(`${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/${state.form.feature_id}/?` + - `feature_type__slug=${rootState.feature_type.current_feature_type_slug}` + - `&project__slug=${rootState.project_slug}` - , geojson) - .then((response) => { - if (response.status === 200 && response.data) { - if (state.attachmentFormset.length > 0 || state.linkedFormset.length > 0) { - handleOtherForms(response.data.id) - } else { - redirect(response.data.id) - } - } - }) - .catch((error) => { - commit("DISCARD_LOADER", null, { root: true }) - if (error.message === "Network Error" || window.navigator.onLine === false) { - let arraysOffline = []; - let localStorageArray = localStorage.getItem("geocontrib_offline"); - if (localStorageArray) { - arraysOffline = JSON.parse(localStorageArray); - } - let updateMsg = { - project: rootState.project_slug, - type: 'put', - featureId: state.form.feature_id, - geojson: geojson - }; - arraysOffline.push(updateMsg); - localStorage.setItem("geocontrib_offline", JSON.stringify(arraysOffline)); - router.push({ - name: "offline-signalement", - params: { - slug_type_signal: rootState.feature_type.current_feature_type_slug - }, - }); + url += `${state.form.feature_id}/?` + + `feature_type__slug=${rootState.feature_type.current_feature_type_slug}` + + `&project__slug=${rootState.project_slug}` + } + return axios({ + url, + method: routeName === "editer-signalement" ? "PUT" : "POST", + data: geojson + }).then((response) => { + if ((response.status === 200 || response.status === 201) && response.data) { + if (state.attachmentFormset.length > 0 || state.linkedFormset.length > 0 || state.attachmentFormset.length > 0 || state.attachmentsToDelete.length > 0) { + handleOtherForms(response.data.id) + } else { + redirect(response.data.id) } - else { - console.log(error) - throw error; + } + }) + .catch((error) => { + commit("DISCARD_LOADER", null, { root: true }) + if (error.message === "Network Error" || window.navigator.onLine === false) { + let arraysOffline = []; + let localStorageArray = localStorage.getItem("geocontrib_offline"); + if (localStorageArray) { + arraysOffline = JSON.parse(localStorageArray); } - + let updateMsg = { + project: rootState.project_slug, + type: 'put', + featureId: state.form.feature_id, + geojson: geojson + }; + arraysOffline.push(updateMsg); + localStorage.setItem("geocontrib_offline", JSON.stringify(arraysOffline)); + router.push({ + name: "offline-signalement", + params: { + slug_type_signal: rootState.feature_type.current_feature_type_slug + }, + }); + } + else { + console.error(error) throw error; - }); - } else { - return axios - .post(`${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/`, geojson) - .then((response) => { - if (response.status === 201 && response.data) { - if (state.attachmentFormset.length > 0 || state.linkedFormset.length > 0) { - handleOtherForms(response.data.id) - } else { - redirect(response.data.id) - } - } - }) - .catch((error) => { - commit("DISCARD_LOADER", null, { root: true }) - if (error.message === "Network Error" || window.navigator.onLine === false) { - let arraysOffline = []; - let localStorageArray = localStorage.getItem("geocontrib_offline"); - if (localStorageArray) { - arraysOffline = JSON.parse(localStorageArray); - } - let updateMsg = { - project: rootState.project_slug, - type: 'post', - geojson: geojson - }; - arraysOffline.push(updateMsg); - localStorage.setItem("geocontrib_offline", JSON.stringify(arraysOffline)); - router.push({ - name: "offline-signalement", - params: { - slug_type_signal: rootState.feature_type.current_feature_type_slug - }, - }); - - } - else { - console.log(error) - throw error; - } - - }); - } - // this.$store.dispatch("GET_ALL_PROJECTS"), //* & refresh project list + } + throw error; + }); }, async SEND_ATTACHMENTS({ state, rootState, dispatch }, featureId) { const DJANGO_API_BASE = rootState.configuration.VUE_APP_DJANGO_API_BASE; - + function addFile(attachment, attchmtId) { let formdata = new FormData(); formdata.append("file", attachment.fileToImport, attachment.fileToImport.name); return axios .put(`${DJANGO_API_BASE}features/${featureId}/attachments/${attchmtId}/upload-file/`, formdata) .then((response) => { - console.log(response) - if (response && response.status === 200) { - console.log(response.status) - } return response; }) .catch((error) => { @@ -345,37 +292,25 @@ const feature = { formdata.append("title", attachment.title); formdata.append("info", attachment.info); - if (!attachment.id) { //* used to check if doesn't exist in DB and should be send through post (useless now) - return axios - .post(`${DJANGO_API_BASE}features/${featureId}/attachments/`, formdata) - .then((response) => { - console.log(response) - if (response && response.status === 201 && attachment.fileToImport) { - console.log(response.status) - return addFile(attachment, response.data.id); - } - return response - }) - .catch((error) => { - console.error(error); - return error - }); - } else { - return axios - .put(`${DJANGO_API_BASE}features/${featureId}/attachments/${attachment.id}/`, formdata) - .then((response) => { - console.log(response) - if (response && response.status === 200 && attachment.fileToImport) { - console.log(response.status) - return addFile(attachment, response.data.id); - } - }) - .catch((error) => { - console.error(error); - return error - }); - + let url = `${DJANGO_API_BASE}features/${featureId}/attachments/` + if (attachment.id) { + url += `${attachment.id}/` } + + return axios({ + url, + method: attachment.id ? "PUT" : "POST", + data: formdata + }).then((response) => { + if (response && (response.status === 200 || response.status === 201) && attachment.fileToImport) { + return addFile(attachment, response.data.id); + } + return response + }) + .catch((error) => { + console.error(error); + return error + }); } function deleteAttachement(attachmentsId, featureId) { @@ -386,6 +321,7 @@ const feature = { return dispatch("DELETE_ATTACHMENTS", payload) .then((response) => response); } + const promisesResult = await Promise.all([ ...state.attachmentFormset.map((attachment) => putOrPostAttachement(attachment)), ...state.attachmentsToDelete.map((attachmentsId) => deleteAttachement(attachmentsId, featureId)) diff --git a/src/store/modules/feature_type.js b/src/store/modules/feature_type.js index bfe624e5e33a984ec4457f05345015daa2d8538d..5ad047abd1203dfde68efcb5bd30c3d692334c3b 100644 --- a/src/store/modules/feature_type.js +++ b/src/store/modules/feature_type.js @@ -1,16 +1,20 @@ import axios from '@/axios-client.js'; -// axios.defaults.headers.common['X-CSRFToken'] = (name => { -// var re = new RegExp(name + "=([^;]+)"); -// var value = re.exec(document.cookie); -// return (value !== null) ? unescape(value[1]) : null; -// })('csrftoken'); - const getColorsStyles = (customForms) => customForms.filter(customForm => customForm.options && customForm.options.length).map(el => { //* in dropdown, value is the name and name is the label to be displayed, could be changed... return { value: el.name, name: el.label, options: el.options } }); +const pending2draftFeatures = (features) => { + let result = [] + for (let el of features) { + if (el.properties.status === "pending") { + el.properties.status = "draft" + } + result.push(el) + } + return result; +} const feature_type = { namespaced: true, @@ -159,12 +163,25 @@ const feature_type = { }); }, - SEND_FEATURES_FROM_GEOJSON({ state, dispatch }, payload) { + async SEND_FEATURES_FROM_GEOJSON({ state, dispatch, rootGetters }, payload) { const { feature_type_slug } = payload; if (state.fileToImport.size > 0) { let formData = new FormData(); - formData.append('json_file', state.fileToImport); + + if (!rootGetters.project.moderation) { + const textFile = await state.fileToImport.text(); + const geojson = JSON.parse(textFile); + const unmoderatedFeatures = pending2draftFeatures(geojson.features); + const newGeojson= { + "type": "FeatureCollection", "features": unmoderatedFeatures + }; + const newFile = new File([JSON.stringify(newGeojson)], state.fileToImport.name, {type: state.fileToImport.type}); + formData.append('json_file', newFile); + } else { + formData.append('json_file', state.fileToImport); + } + formData.append('feature_type_slug', feature_type_slug); let url = this.state.configuration.VUE_APP_DJANGO_API_BASE + diff --git a/src/views/Index.vue b/src/views/Index.vue index 33ced862f9a5074b1d1184ce1d75dbb4877b4ecd..96f4a9b021ac0c93a4ba879913061afbbf09986c 100644 --- a/src/views/Index.vue +++ b/src/views/Index.vue @@ -62,7 +62,7 @@ >Niveau d'autorisation requis : {{ project.access_level_pub_feature }}</span ><br /> - <span> + <span v-if="user"> Mon niveau d'autorisation : <span v-if="USER_LEVEL_PROJECTS && project">{{ USER_LEVEL_PROJECTS[project.slug] diff --git a/src/views/feature/Feature_detail.vue b/src/views/feature/Feature_detail.vue index 9a8b3e83a9bc8c6a63b76c810a75bf08e48ed5cb..f14f24213bdcbb6724d3199aa492664df66e6f0c 100644 --- a/src/views/feature/Feature_detail.vue +++ b/src/views/feature/Feature_detail.vue @@ -238,20 +238,22 @@ </div> <div - v-if="permissions && permissions.can_create_feature" + v-if="permissions && permissions.can_create_feature && isOffline() !== true" class="ui segment" > <form id="form-comment" class="ui form" - method="POST" - enctype="multipart/form-data" > <div class="required field"> <label :for="comment_form.comment.id_for_label" >Ajouter un commentaire</label > - {{ comment_form.comment.errors }} + <ul v-if="comment_form.comment.errors" class="errorlist"> + <li> + {{ comment_form.comment.errors }} + </li> + </ul> <textarea v-model="comment_form.comment.value" :name="comment_form.comment.html_name" @@ -280,12 +282,12 @@ </div> <div class="field"> <input - v-model="comment_form.title.value" + v-model="comment_form.attachment_file.title" type="text" - :name="comment_form.title.html_name" - :id="comment_form.title.id_for_label" + name="title" + id="title" /> - {{ comment_form.title.errors }} + {{ comment_form.attachment_file.errors }} </div> </div> <ul v-if="comment_form.attachment_file.errors" class="errorlist"> @@ -294,7 +296,6 @@ </li> </ul> <button - v-if="isOffline() !== true" @click="postComment" type="button" class="ui compact green icon button" @@ -351,12 +352,6 @@ import { mapUtil } from "@/assets/js/map-util.js"; import featureAPI from "@/services/feature-api"; import axios from '@/axios-client.js'; -// axios.defaults.headers.common['X-CSRFToken'] = (name => { -// var re = new RegExp(name + "=([^;]+)"); -// var value = re.exec(document.cookie); -// return (value !== null) ? unescape(value[1]) : null; -// })('csrftoken'); - export default { name: "Feature_detail", @@ -372,22 +367,15 @@ export default { comment_form: { attachment_file: { errors: null, - value: null, + title: "", file: null, }, - title: { - id_for_label: "title", - html_name: "title", - errors: null, - value: null, - }, comment: { id_for_label: "add-comment", html_name: "add-comment", - errors: null, + errors: "", value: null, }, - non_field_errors: [], }, }; }, @@ -467,8 +455,18 @@ export default { this.addFeatureToMap(); }, + validateForm() { + this.comment_form.comment.errors = "" + if (!this.comment_form.comment.value) { + this.comment_form.comment.errors = "Le commentaire ne peut pas être vide" + return false; + } + return true; + }, + postComment() { - featureAPI + if (this.validateForm()) { + featureAPI .postComment({ featureId: this.$route.params.slug_signal, comment: this.comment_form.comment.value, @@ -479,9 +477,9 @@ export default { .postCommentAttachment({ featureId: this.$route.params.slug_signal, file: this.comment_form.attachment_file.file, - fileName: this.comment_form.title.file, + fileName: this.comment_form.attachment_file.fileName, + title: this.comment_form.attachment_file.title, commentId: response.data.id, - title: response.data.comment, }) .then(() => { this.confirmComment(); @@ -490,15 +488,15 @@ export default { this.confirmComment(); } }); + } }, confirmComment() { this.$store.commit("DISPLAY_MESSAGE", "Ajout du commentaire confirmé"); this.getFeatureEvents(); //* display new comment on the page this.comment_form.attachment_file.file = null; - this.comment_form.attachment_file.value = null; - this.comment_form.title.file = null; - this.comment_form.title.value = null; + this.comment_form.attachment_file.fileName = ""; + this.comment_form.attachment_file.title = ""; this.comment_form.comment.value = null; }, @@ -520,19 +518,19 @@ export default { // * read image file const files = e.target.files || e.dataTransfer.files; - const _this = this; //* 'this' is different in onload function - function handleFile(isValid) { + const handleFile = (isValid) => { if (isValid) { - const period = files[0].name.lastIndexOf("."); - const fileName = files[0].name.substring(0, period); - const fileExtension = files[0].name.substring(period + 1); - const shortName = fileName.slice(0, 10) + "[...]." + fileExtension; - _this.comment_form.attachment_file.file = files[0]; //* store the file to post later - _this.comment_form.attachment_file.value = shortName; //* for display - _this.comment_form.title.value = shortName; - _this.comment_form.attachment_file.errors = null; + this.comment_form.attachment_file.file = files[0]; //* store the file to post afterwards + let title = files[0].name + this.comment_form.attachment_file.fileName = title; //* name of the file + const fileExtension = title.substring(title.lastIndexOf(".") + 1); + if ((title.length - fileExtension.length) > 11) { + title = title.slice(0, 10) + "[...]." + fileExtension; + } + this.comment_form.attachment_file.title = title; //* title for display + this.comment_form.attachment_file.errors = null; } else { - _this.comment_form.attachment_file.errors = + this.comment_form.attachment_file.errors = "Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu."; } } diff --git a/src/views/feature/Feature_list.vue b/src/views/feature/Feature_list.vue index 408091df72d8955bc4a332ddd4fef83f14128038..c31ad0879058b2a0a117cd71012851e6df2bb8c3 100644 --- a/src/views/feature/Feature_list.vue +++ b/src/views/feature/Feature_list.vue @@ -86,7 +86,6 @@ <div class="field wide four column no-margin-mobile"> <label>Type</label> <Dropdown - v-on:update:selection="updateTypeFeatures" :options="featureTypeChoices" :selected="form.type.selected" :selection.sync="form.type.selected" @@ -98,7 +97,6 @@ <label>Statut</label> <!-- //* giving an object mapped on key name --> <Dropdown - v-on:update:selection="updateStatusFeatures" :options="statusChoices" :selected="form.status.selected.name" :selection.sync="form.status.selected" @@ -141,11 +139,12 @@ <FeatureListTable v-show="!showMap" v-on:update:page="handlePageChange" + v-on:update:sort="handleSortChange" :paginatedFeatures="paginatedFeatures" :checkedFeatures.sync="checkedFeatures" :featuresCount="featuresCount" :pagination="pagination" - :pageNumbers="pageNumbers" + :sort="sort" /> <!-- MODAL ALL DELETE FEATURE TYPE --> @@ -201,17 +200,12 @@ export default { data() { return { - modalAllDeleteOpen: false, form: { type: { - selected: null, - choices: [], + selected: "", }, status: { - selected: { - name: null, - value: null, - }, + selected: "", choices: [ { name: "Brouillon", @@ -233,26 +227,40 @@ export default { }, title: null, }, - paginatedFeatures: [], baseUrl: this.$store.state.configuration.BASE_URL, + modalAllDeleteOpen: false, map: null, zoom: null, lat: null, lng: null, featuresCount: 0, - next: null, - previous: null, + paginatedFeatures: [], pagination: { currentPage: 1, pagesize: 15, start: 0, end: 15, }, + previous: null, + next: null, + sort: { + column: "", + ascending: true, + }, showMap: true, showAddFeature: false, }; }, + watch: { + "form.type.selected"() { + this.fetchPagedFeatures(); + }, + "form.status.selected.value"() { + this.fetchPagedFeatures(); + }, + }, + computed: { ...mapGetters(["project", "permissions"]), ...mapState("feature", ["checkedFeatures"]), @@ -273,16 +281,6 @@ export default { featureTypeChoices() { return this.feature_types.map((el) => el.title); }, - - pageNumbers() { - const totalPages = Math.ceil( - this.featuresCount / this.pagination.pagesize - ); - return [...Array(totalPages).keys()].map((pageNumb) => { - ++pageNumb; - return pageNumb; - }); - }, }, methods: { @@ -349,7 +347,7 @@ export default { mapDefaultViewZoom, }); - this.getBbox2FIt(); + this.fetchBboxNfit(); document.addEventListener("change-layers-order", (event) => { // Reverse is done because the first layer in order has to be added in the map in last. @@ -372,7 +370,7 @@ export default { }, 1000); }, - getBbox2FIt(queryParams) { + fetchBboxNfit(queryParams) { featureAPI .getFeaturesBbox(this.project.slug, queryParams) .then((bbox) => { @@ -388,86 +386,42 @@ export default { return featureType ? featureType.slug : null; }, - buildFilterParams({ filterType, filterValue }) { - let params = ""; - let typeFilter, statusFilter; - //*** feature type ***// - if (filterType === "featureType") { - if (filterValue === "" && !this.form.type.selected) { - //* s'il y n'avait pas de filtre et qu'il a été supprimé --> ne pas mettre à jour les features - return "abort"; - } else if (filterValue !== undefined && filterValue !== null) { - //* s'il y a un nouveau filtre --> ajouter une params - typeFilter = this.getFeatureTypeSlug(filterValue); - } //* sinon il n'y a pas de param ajouté, ce qui supprime la query - - //*** status ***// - } else if (filterType === "status") { - if (filterValue === "" && !this.form.status.selected.value) { - return "abort"; - } else if (filterValue !== undefined && filterValue !== null) { - statusFilter = filterValue.value; - } - } - - //* after possibilities of aborting features fetch, empty geojson to make sure even no result would update - - if ( - (filterType === undefined || filterType === "status") && - this.form.type.selected - ) { - //* s'il y a déjà un filtre sélectionné, maintenir le params - typeFilter = this.getFeatureTypeSlug(this.form.type.selected); - } - if ( - (filterType === undefined || filterType === "featureType") && - this.form.status.selected.value - ) { - statusFilter = this.form.status.selected.value; - } - - if (typeFilter) { - let typeParams = `&feature_type_slug=${typeFilter}`; - params += typeParams; - } - if (statusFilter) { - let statusParams = `&status__value=${statusFilter}`; - params += statusParams; - } - - //*** title ***// - if (this.form.title) { - params += `&title=${this.form.title}`; + getAvalaibleField(orderField) { + let result = orderField; + if (orderField === "display_creator") { + result = "creator"; + } else if (orderField === "display_last_editor") { + result = "last_editor"; } - this.getBbox2FIt(params); - return params; - }, - - updateTypeFeatures(filterValue) { - //* only update:selection custom event can trigger the filter update, - //* but it happens before the value is updated, thus using selected value from event to update query - this.fetchPagedFeatures({ filterType: "featureType", filterValue }); + return result; }, - updateStatusFeatures(filterValue) { - this.fetchPagedFeatures({ filterType: "status", filterValue }); + buildQueryString() { + let urlParams = ""; + let typeFilter = this.getFeatureTypeSlug(this.form.type.selected); + let statusFilter = this.form.status.selected.value; + + if (typeFilter) urlParams += `&feature_type_slug=${typeFilter}`; + if (statusFilter) urlParams += `&status__value=${statusFilter}`; + if (this.form.title) urlParams += `&title=${this.form.title}`; + if (this.sort.column) { + urlParams += `&ordering=${ + this.sort.ascending ? "-" : "" + }${this.getAvalaibleField(this.sort.column)}`; + } + return urlParams; }, - fetchPagedFeatures(params) { - this.onFilterChange(); //* temporary, use paginated event to watch change in filters, to modify geojson on map + fetchPagedFeatures(newUrl) { + this.onFilterChange(); //* use paginated event to watch change in filters and modify features on map let url = `${this.API_BASE_URL}projects/${this.$route.params.slug}/feature-paginated/?output=geojson&limit=${this.pagination.pagesize}&offset=${this.pagination.start}`; - - if (params) { - if (typeof params === "object") { - const filterParams = this.buildFilterParams(params); - if (filterParams === "abort") return; - url += filterParams; - } else { - //console.error("ONLY FOR DEV !!!!!!!!!!!!!"); - //params = params.replace("8000", "8010"); //* for dev uncomment to use proxy link - url = params; - } + if (newUrl && typeof newUrl === "string") { + //* if receiving next & previous url + //newUrl = newUrl.replace("8000", "8010"); //* for dev uncomment to use proxy link + url = newUrl; } + const queryString = this.buildQueryString(); + url += queryString; this.$store.commit( "DISPLAY_LOADER", @@ -480,6 +434,8 @@ export default { this.next = data.next; this.paginatedFeatures = data.results.features; } + //* bbox needs to be updated with the same filters + if (queryString) this.fetchBboxNfit(queryString); this.$store.commit("DISCARD_LOADER"); }); }, @@ -497,6 +453,14 @@ export default { } }, + handleSortChange(sort) { + this.sort = sort; + this.fetchPagedFeatures({ + filterType: undefined, + filterValue: undefined, + }); + }, + toPage(pageNumber) { const toAddOrRemove = (pageNumber - this.pagination.currentPage) * this.pagination.pagesize; diff --git a/src/views/feature_type/Feature_type_detail.vue b/src/views/feature_type/Feature_type_detail.vue index 7e74e6aa5c6e973acefe9d8666909a179f229c36..07dbfc9c86a745b70a2737aefed54f505a5c0641 100644 --- a/src/views/feature_type/Feature_type_detail.vue +++ b/src/views/feature_type/Feature_type_detail.vue @@ -298,7 +298,7 @@ export default { this.showImport = !this.showImport; if (this.showImport) { this.$store.dispatch("feature_type/GET_IMPORTS", { - feature_type: this.structure.slug + feature_type: this.$route.params.feature_type_slug }); } }, @@ -411,7 +411,7 @@ export default { const response = await this.$store.dispatch('feature/GET_PROJECT_FEATURES', { project_slug: this.$route.params.slug, - feature_type__slug : this.structure.slug, + feature_type__slug : this.$route.params.feature_type_slug, ordering: '-created_on', limit: '5' }) @@ -425,7 +425,7 @@ export default { 'structure'(newValue){ if (newValue.slug){ this.$store.dispatch("feature_type/GET_IMPORTS", { - feature_type: this.structure.slug + feature_type: this.$route.params.feature_type_slug }); } } diff --git a/src/views/feature_type/Feature_type_symbology.vue b/src/views/feature_type/Feature_type_symbology.vue index f1d82fe21f8c5f0471e52bc4b8dd09090414b227..67d0c7efa1956e69ced868544e0cc3702da6071f 100644 --- a/src/views/feature_type/Feature_type_symbology.vue +++ b/src/views/feature_type/Feature_type_symbology.vue @@ -23,6 +23,7 @@ <SymbologySelector v-if="feature_type" :initColor="feature_type.color" + :initIcon="feature_type.icon" :geomType="feature_type.geom_type" @set="setDefaultStyle" /> @@ -60,7 +61,9 @@ > <SymbologySelector :title="option" - :initColor="feature_type.colors_style.value.colors[option].value" + :initColor="feature_type.colors_style.value.colors[option] ? + feature_type.colors_style.value.colors[option].value : + feature_type.colors_style.value.colors[option]" :initIcon="feature_type.colors_style.value.icons[option]" :geomType="feature_type.customfield_set.geomType" @set="setColorsStyle" diff --git a/src/views/project/Project_detail.vue b/src/views/project/Project_detail.vue index ad24ad07663e5f0b2635d1e019ddabf63c7c8236..0b65164ca8331b3de91a551c4cba825a69f955ea 100644 --- a/src/views/project/Project_detail.vue +++ b/src/views/project/Project_detail.vue @@ -108,7 +108,7 @@ <h3 class="ui header">Types de signalements</h3> <div class="ui middle aligned divided list"> <div - :class="{ active: featureTypeLoading }" + :class="{ active : projectInfoLoading }" class="ui inverted dimmer" > <div class="ui text loader"> @@ -221,15 +221,15 @@ <div v-else v-frag> <router-link :to="{ - name: 'editer-type-signalement', + name: 'editer-symbologie-signalement', params: { slug_type_signal: type.slug }, }" v-if=" project && - type.is_editable && + type.geom_type === 'point' && permissions && permissions.can_create_feature_type && - isOffline() !== true + isOffline() != true " class=" ui @@ -240,24 +240,23 @@ floated button button-hover-green " - data-tooltip="Éditer le type de signalement" + data-tooltip="Éditer la symbologie du type de signalement" data-position="left center" data-variation="mini" > - <i class="inverted grey pencil alternate icon"></i> + <i class="inverted grey paint brush alternate icon"></i> </router-link> <router-link :to="{ - name: 'editer-symbologie-signalement', + name: 'editer-type-signalement', params: { slug_type_signal: type.slug }, }" v-if=" project && type.is_editable && - type.geom_type === 'point' && permissions && permissions.can_create_feature_type && - isOffline() != true + isOffline() !== true " class=" ui @@ -268,11 +267,11 @@ floated button button-hover-green " - data-tooltip="Éditer la symbologie du type de signalement" + data-tooltip="Éditer le type de signalement" data-position="left center" data-variation="mini" > - <i class="inverted grey paint brush alternate icon"></i> + <i class="inverted grey pencil alternate icon"></i> </router-link> </div> </div> @@ -407,6 +406,14 @@ <div class="content"> <div class="center aligned header">Derniers commentaires</div> <div class="center aligned description"> + <div + :class="{ active: projectInfoLoading }" + class="ui inverted dimmer" + > + <div class="ui text loader"> + Récupération des commentaires en cours... + </div> + </div> <div class="ui relaxed list"> <div v-for="(item, index) in last_comments" @@ -627,7 +634,7 @@ export default { isModalOpen: false, is_suscriber: false, tempMessage: null, - featureTypeLoading: true, + projectInfoLoading: true, featureTypeImporting: false, featuresLoading: true, isFileSizeModalOpen: false @@ -912,11 +919,11 @@ export default { mounted() { this.GET_PROJECT_INFO(this.slug) .then(() => { - this.featureTypeLoading = false; + this.projectInfoLoading = false; setTimeout(this.initMap, 1000); }) .catch(() => { - this.featureTypeLoading = false; + this.projectInfoLoading = false; }); this.GET_PROJECT_FEATURES({ project_slug: this.slug, @@ -938,6 +945,10 @@ export default { setTimeout(() => (this.tempMessage = null), 5000); //* hide message after 5 seconds } }, + + destroyed() { + this.CLEAR_RELOAD_INTERVAL_ID(); + }, }; </script> diff --git a/src/views/registration/Login.vue b/src/views/registration/Login.vue index 0f62078398f1fe07998e86e91d73c4c2601cc8cf..f26884b1113a2d6bb9728c6e84c49ed48826a2b5 100644 --- a/src/views/registration/Login.vue +++ b/src/views/registration/Login.vue @@ -2,10 +2,7 @@ <div> <div class="row"> <div class="fourteen wide column"> - <img - class="ui centered small image" - :src="logo" - /> + <img class="ui centered small image" :src="logo" /> <h2 class="ui center aligned icon header"> <div class="content"> {{ APPLICATION_NAME }} @@ -60,7 +57,6 @@ </template> <script> - export default { name: "Login", data() { @@ -101,5 +97,15 @@ export default { .catch(); }, }, + + mounted() { + if (this.$store.state.user) { + this.$store.commit( + "DISPLAY_MESSAGE", + "Vous êtes déjà connecté, vous allez être redirigé vers la page d'accueil." + ); + setTimeout(() => this.$router.push("/"), 3100); + } + }, }; </script> \ No newline at end of file