diff --git a/conf_apache_dev.md b/conf_apache_dev.md new file mode 100644 index 0000000000000000000000000000000000000000..03d656dfd0c6e211460a41be3ea4b98e472c4c0c --- /dev/null +++ b/conf_apache_dev.md @@ -0,0 +1,39 @@ + +# intro + +ceci permet de faire tourner le front en local sur /geocontrib +et de faire pointer /api sur n'importe quel backend (dev, local ou autre ) + +# configuration apache + +dans la configuration apache generale (httpd.conf ou commande a2enmod ), activer les modules : +* mod_headers +* mod_proxy +* mod_ssl +* mod_proxy_http + + +``` + <Location /geocontrib > + ProxyPass http://localhost:8080/geocontrib + </Location> + + + SSLProxyEngine On + <Location /api > + ProxyPass https://geocontrib.dev.neogeo.fr/geocontrib/api + RequestHeader set Referer https://geocontrib.dev.neogeo.fr/ + </Location> + ``` + + +# configuration projet vueJS + +remplacer dans le fichier config.json du projet +``` +DOMAIN":"http://localhost:8010/", par "DOMAIN":"http://localhost/", +``` +et +``` +"VUE_APP_DJANGO_API_BASE":"http://localhost:8010/api/", par "VUE_APP_DJANGO_API_BASE":"http://localhost/api/", +``` diff --git a/nginx.conf b/nginx.conf index b1a4d31d8d61c35ab78d0b999c673d28a23b5a2d..3ea3849f16c06b3f6da02bfaff5147374320b925 100644 --- a/nginx.conf +++ b/nginx.conf @@ -27,6 +27,18 @@ server { proxy_pass http://geocontrib_site; } + location /geocontrib/cas { + proxy_pass_header Set-Cookie; + proxy_set_header X-NginX-Proxy true; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_read_timeout 300s; + proxy_redirect off; + + proxy_pass http://geocontrib_site; + } + location /geocontrib/admin { proxy_pass_header Set-Cookie; proxy_set_header X-NginX-Proxy true; diff --git a/package.json b/package.json index 8f55afdbe57b2ad2089383f64f4e017bdb6981cb..fa44bcd1207b3c48ee3f17186f546b7cdc42e50b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geocontrib-frontend", - "version": "2.3.2-rc2", + "version": "2.3.2", "private": true, "scripts": { "serve": "npm run init-proxy & npm run init-serve", diff --git a/public/config/config.json b/public/config/config.json index 9651190e5aa2d349aead14b9d85304d92d6f2356..537e865a61060fc9fb1cb617d31a0b89c50d93db 100644 --- a/public/config/config.json +++ b/public/config/config.json @@ -1,6 +1,6 @@ { "BASE_URL":"/geocontrib/", - "DOMAIN":"http://localhost:8010/", + "DOMAIN":"http://localhost/", "NODE_ENV":"development", "VUE_APP_LOCALE":"fr-FR", "VUE_APP_APPLICATION_NAME":"GéoContrib", diff --git a/src/App.vue b/src/App.vue index baf1424a94b25471cd7c4694bc7a68fb40c1b92c..964cfd77ac5e2480361fb249dc59952ae2d3d255 100644 --- a/src/App.vue +++ b/src/App.vue @@ -155,13 +155,14 @@ <main> <div id="content" class="ui stackable grid centered container"> <transition name="fadeDownUp"> - <div v-if="messages && messages.length > 0" class="row"> + <div v-if="messages && messages.length > 0" class="row over-content"> <div class="fourteen wide column"> <div v-for="(message, index) in messages" :key="'message-' + index" - class="ui info message" + :class="['ui', message.level ? message.level : 'info', 'message']" > + <i class="close icon" @click="DISCARD_MESSAGE(message)"></i> <div class="header"> <i class="info circle icon"></i> Informations @@ -197,8 +198,7 @@ <script> import frag from "vue-frag"; -import { mapState } from "vuex"; -import { mapGetters } from "vuex"; +import { mapMutations, mapState, mapGetters } from "vuex"; export default { name: "App", @@ -256,6 +256,7 @@ export default { }, methods: { + ...mapMutations(['DISCARD_MESSAGE']), logout() { this.$store.dispatch("LOGOUT"); }, @@ -383,23 +384,10 @@ footer { 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); - } +.ui.grid > .row.over-content { + position: absolute; + z-index: 99; + opacity: 0.95; } .fadeDownUp-enter-active { @@ -431,6 +419,17 @@ footer { } } +.ui.message > .close.icon { + cursor: pointer; + position: absolute; + margin: 0em; + top: 0.78575em; + right: 0.5em; + opacity: 0.7; + -webkit-transition: opacity 0.1s ease; + transition: opacity 0.1s ease; +} + </style> <style scoped> diff --git a/src/assets/js/map-util.js b/src/assets/js/map-util.js index b89c5a07dcc1d1ed39225ad038c1870d03ab3b36..9c279ef6e2634edb7751f6122e349debf5952bc2 100644 --- a/src/assets/js/map-util.js +++ b/src/assets/js/map-util.js @@ -298,8 +298,7 @@ const mapUtil = { const currentValue = properties[colorsStyle.custom_field_name]; const colorStyle = colorsStyle.colors[currentValue]; return colorStyle ? colorStyle : featureType.color - } - else{ + } else { return featureType.color; } }, @@ -310,7 +309,12 @@ const mapUtil = { vectorTileLayerStyles: { "default": (properties) => { const featureType = featureTypes.find((x) => x.slug.split('-')[0] === '' + properties.feature_type_id); - const color = this.retrieveFeatureColor(featureType, properties) + + const color = this.retrieveFeatureColor(featureType, properties); + const colorValue = + color.value && color.value.length ? + color.value : typeof color === 'string' && color.length ? + color : '#000000'; const hiddenStyle = ({ radius: 0, @@ -318,7 +322,17 @@ const mapUtil = { weight: 0, fill: false, color: featureType.color, - }) + }); + + const defaultStyle = { + radius: 4, + fillOpacity: 0.5, + weight: 3, + fill: true, + color: colorValue, + }; + + // Filtre sur le feature type if (form_filters && form_filters.type.selected) { if (featureType.title !== form_filters.type.selected) { @@ -337,13 +351,7 @@ const mapUtil = { return hiddenStyle; } } - return ({ - radius: 4, - fillOpacity: 0.5, - weight: 3, - fill: true, - color: color, - }); + return defaultStyle; }, }, // subdomains: "0123", diff --git a/src/assets/styles/base.css b/src/assets/styles/base.css index 0777e8e3742e08d03179550ddfff9e40778efb21..6f7a4e7c4823bf725373cdbbab5450879bdf84ed 100644 --- a/src/assets/styles/base.css +++ b/src/assets/styles/base.css @@ -10,6 +10,13 @@ main { padding: 2em 0em; } +/* ---------------------------------- */ + /* UTILS */ +/* ---------------------------------- */ +.ellipsis { + text-overflow: ellipsis; + overflow: hidden; +} /* ---------------------------------- */ /* MAIN */ /* ---------------------------------- */ diff --git a/src/components/ImportTask.vue b/src/components/ImportTask.vue index 8c15409397ac5d0d13a97a7f65e8633ab81fbc6c..9f307cab0b604209193b23289677ab5afe8d2a36 100644 --- a/src/components/ImportTask.vue +++ b/src/components/ImportTask.vue @@ -46,7 +46,7 @@ > <i v-on:click="fetchImports()" - :class="['orange icon', ready ? 'sync' : 'hourglass half']" + :class="['orange icon', ready && !reloading ? 'sync' : 'hourglass half rotate']" /> </span> </td> @@ -68,7 +68,7 @@ export default { }; }, - props: ["data"], + props: ["data", "reloading"], filters: { setDate: function (value) { @@ -122,23 +122,19 @@ export default { padding-top: 1em; } -@keyframes rotateIn { - from { - transform: rotate3d(0, 0, 1, -200deg); - opacity: 0; - } - - to { - transform: translate3d(0, 0, 0); - opacity: 1; - } +i.icon { + width: 20px !important; + height: 20px !important; } -.rotateIn { - animation-name: rotateIn; - transform-origin: center; - animation: 2s; +.rotate { + -webkit-animation:spin 1s linear infinite; + -moz-animation:spin 1s linear infinite; + animation:spin 1s linear infinite; } +@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } +@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } +@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } } /* Max width before this PARTICULAR table gets nasty diff --git a/src/components/feature/FeatureListTable.vue b/src/components/feature/FeatureListTable.vue index 5817a4fa2ab3f7acb9aadf77cef804e68c27e0be..5aa26043cf82cb948b39470a50b4cae4fc274e64 100644 --- a/src/components/feature/FeatureListTable.vue +++ b/src/components/feature/FeatureListTable.vue @@ -1,12 +1,20 @@ <template> <div data-tab="list" class="dataTables_wrapper no-footer"> - <table id="table-features" class="ui compact table"> + <table id="table-features" class="ui compact table dataTable"> <thead> <tr> - <th class="center"></th> + <th class="dt-center"> + <div @click="switchMode" class="switch-buttons pointer" :data-tooltip="`Passer en mode ${mode === 'modify' ? 'suppression':'édition'}`"> + <div><i :class="['icon pencil', {disabled: mode !== 'modify'}]"></i></div> + <span class="grey">| </span> + <div><i :class="['icon trash', {disabled: mode !== 'delete'}]"></i></div> + </div> + </th> + + <th class="dt-center"> + <div class="pointer" @click="changeSort('status')"> - <th class="center"> Statut <i :class="{ @@ -14,21 +22,23 @@ up: isSortedDesc('status'), }" class="icon sort" - @click="changeSort('status')" /> + </div> </th> - <th class="center"> - Type - <i - :class="{ - down: isSortedAsc('feature_type'), - up: isSortedDesc('feature_type'), - }" - class="icon sort" - @click="changeSort('feature_type')" - /> + <th class="dt-center"> + <div class="pointer" @click="changeSort('feature_type')"> + Type + <i + :class="{ + down: isSortedAsc('feature_type'), + up: isSortedDesc('feature_type'), + }" + class="icon sort" + /> + </div> </th> - <th class="center"> + <th class="dt-center"> + <div class="pointer" @click="changeSort('title')"> Nom <i :class="{ @@ -36,10 +46,11 @@ up: isSortedDesc('title'), }" class="icon sort" - @click="changeSort('title')" /> + </div> </th> - <th class="center"> + <th class="dt-center"> + <div class="pointer" @click="changeSort('updated_on')"> Dernière modification <i :class="{ @@ -47,62 +58,57 @@ up: isSortedDesc('updated_on'), }" class="icon sort" - @click="changeSort('updated_on')" /> + </div> </th> - <th class="center" v-if="user"> - Auteur - <i - :class="{ - down: isSortedAsc('display_creator'), - up: isSortedDesc('display_creator'), - }" - class="icon sort" - @click="changeSort('display_creator')" - /> + <th class="dt-center" v-if="user"> + <div class="pointer" @click="changeSort('display_creator')"> + Auteur + <i + :class="{ + down: isSortedAsc('display_creator'), + up: isSortedDesc('display_creator'), + }" + class="icon sort" + /> + </div> </th> - <th class="center" v-if="user"> - Dernier éditeur - <i - :class="{ - down: isSortedAsc('display_last_editor'), - up: isSortedDesc('display_last_editor'), - }" - class="icon sort" - @click="changeSort('display_last_editor')" - /> + <th class="dt-center" v-if="user"> + <div class="pointer" @click="changeSort('display_last_editor')"> + Dernier éditeur + <i + :class="{ + down: isSortedAsc('display_last_editor'), + up: isSortedDesc('display_last_editor'), + }" + class="icon sort" + /> + </div> </th> </tr> </thead> <tbody> <tr v-for="(feature, index) in paginatedFeatures" :key="index"> - <td class="center"> + <td class="dt-center"> <div - class="ui checkbox" - :class=" - feature.properties.creator.username !== user.username && - !user.is_superuser && - !isUserProjectAdministrator - ? 'disabled' - : '' - " + :class="['ui checkbox', {disabled: !checkRights(feature)}]" > <input type="checkbox" + v-model="checked" + @input="storeClickedFeature(feature)" :id="feature.id" :value="feature.id" - v-model="checked" - :disabled=" - feature.properties.creator.username !== user.username && - !user.is_superuser && - !isUserProjectAdministrator - " + :disabled="!checkRights(feature)" + name="select" /> - <label></label> + <label for="select"></label> </div> + <!-- {{canDeleteFeature(feature)}} + {{canEditFeature(feature)}} --> </td> - <td class="center"> + <td class="dt-center"> <div v-if="feature.properties.status.value === 'archived'" data-tooltip="Archivé" @@ -128,7 +134,7 @@ <i class="orange pencil alternate icon"></i> </div> </td> - <td class="center"> + <td class="dt-center"> <router-link :to="{ name: 'details-type-signalement', @@ -140,7 +146,7 @@ {{ feature.properties.feature_type.title }} </router-link> </td> - <td class="center"> + <td class="dt-center"> <router-link :to="{ name: 'details-signalement', @@ -152,13 +158,13 @@ >{{ getFeatureDisplayName(feature) }}</router-link > </td> - <td class="center"> + <td class="dt-center"> {{ feature.properties.updated_on }} </td> - <td class="center" v-if="user"> + <td class="dt-center" v-if="user"> {{ getUserName(feature) }} </td> - <td class="center" v-if="user"> + <td class="dt-center" v-if="user"> {{ feature.properties.display_last_editor }} </td> </tr> @@ -261,18 +267,17 @@ export default { props: [ "paginatedFeatures", "checkedFeatures", + "clickedFeatures", "featuresCount", "pagination", "sort", + "mode" ], computed: { ...mapState(["user"]), ...mapGetters(["project", "permissions"]), - - isUserProjectAdministrator() { - return this.permissions.is_project_administrator; - }, + ...mapState(["user", "USER_LEVEL_PROJECTS"]), checked: { get() { @@ -304,16 +309,18 @@ export default { }, displayedPageNumbers() { + //* s'il y a moins de 5 pages, renvoyer toutes les pages + if (this.lastPageNumber < 5) return this.pageNumbers //* 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)) { + //* à 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) { + //* à 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); @@ -321,6 +328,50 @@ export default { }, methods: { + storeClickedFeature(feature) { + this.clickedFeatures.push({feature_id: feature.id, feature_type: feature.properties.feature_type.slug}) + }, + + canDeleteFeature(feature) { + return feature.properties.creator.username !== this.user.username && + !this.user.is_superuser && + !this.permissions.is_project_administrator + }, + + canEditFeature(feature) { + const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug]; + const access = { + "Administrateur projet" : ["draft", "published", "archived"], + "Modérateur" : ["pending"], + "Super Contributeur" : ["draft", this.project.moderation ? "pending" : "published"], + "Contributeur" : ["draft", this.project.moderation ? "pending" : "published"], + }; + + //if (userStatus === "Super Contributeur" || userStatus === "Contributeur") { //? should super contributeur behave the same, I don't think so + if (userStatus === "Contributeur" && feature.properties.creator.username !== this.user.username) { + return false; + } else if (access[userStatus]) { + return access[userStatus].includes(feature.properties.status.value); + } else { + return false + } + }, + + checkRights(feature) { + switch (this.mode) { + case 'modify': + return this.canEditFeature(feature) + case 'delete': + return this.canDeleteFeature(feature) + } + }, + + switchMode() { + this.$emit('update:mode', this.mode === 'modify' ? 'delete' : 'modify'); + this.$emit('update:clickedFeatures', []); + this.$store.commit("feature/UPDATE_CHECKED_FEATURES", []); + }, + getUserName(feature) { if (!feature.properties.creator) { return " ---- "; @@ -352,6 +403,9 @@ export default { } }, }, + destroyed() { + this.$store.commit("feature/UPDATE_CHECKED_FEATURES", []); + }, }; </script> @@ -361,6 +415,9 @@ export default { position: relative; clear: both; } +table.dataTable th.dt-center, table.dataTable td.dt-center, table.dataTable td.dataTables_empty { + text-align: center; +} .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, @@ -443,7 +500,23 @@ export default { i.icon.sort:not(.down):not(.up) { color: rgb(220, 220, 220); } +.pointer:hover { + cursor: pointer; +} + +.switch-buttons { + display: flex; + justify-content: center; + align-items: baseline; +} +.grey { + color: #bbbbbb; +} + +.ui.dropdown .menu .left.menu, .ui.dropdown > .left.menu .menu { + margin-right: 0 !important; +} /* Max width before this PARTICULAR table gets nasty This query will take effect for any screen smaller than 760px @@ -513,8 +586,8 @@ and also iPads specifically. content: "Auteur"; } - .center { - text-align: right !important; + table.dataTable th.dt-center, table.dataTable td.dt-center, table.dataTable td.dataTables_empty { + text-align: right; } #table-features { diff --git a/src/components/feature_type/SymbologySelector.vue b/src/components/feature_type/SymbologySelector.vue index cd3c2409e6c8a375c139fd075818b7c58b25aa56..32c201bc0748bb34e354670d69974ee016be0af1 100644 --- a/src/components/feature_type/SymbologySelector.vue +++ b/src/components/feature_type/SymbologySelector.vue @@ -14,7 +14,7 @@ v-model.lazy="form.color.value" /> </div> - <div class="required inline field"> + <!-- <div class="required inline field"> <label>Symbole</label> <button class="ui icon button picker-button" @@ -27,7 +27,7 @@ class="icon alt" /> </button> - </div> + </div> --> </div> <div :class="isIconPickerModalOpen ? 'active' : ''" diff --git a/src/service-worker.js b/src/service-worker.js index dac6068b2cb19d00474c971971347d4f6fbb87a1..8a743071b19d358e8943689d387cd3225c77e0c8 100644 --- a/src/service-worker.js +++ b/src/service-worker.js @@ -14,7 +14,7 @@ if (workbox) { // Since we have a SPA here, this should be index.html always. // https://stackoverflow.com/questions/49963982/vue-router-history-mode-with-pwa-in-offline-mode workbox.routing.registerNavigationRoute('/geocontrib/index.html', { - blacklist: [/\/api/,/\/admin/,/\/media/], + blacklist: [/\/api/,/\/admin/,/\/media/,/\/cas/], }) workbox.routing.registerRoute( diff --git a/src/services/feature-api.js b/src/services/feature-api.js index 30b3fb8e55ab30d617b6abf4e2564aa186f86a12..3b21882327d377f1ea9013f83801ad40519f7c49 100644 --- a/src/services/feature-api.js +++ b/src/services/feature-api.js @@ -62,6 +62,21 @@ const featureAPI = { } }, + async updateFeature({ feature_id, feature_type__slug, project__slug, newStatus }) { + let url = `${baseUrl}features/${feature_id}/?feature_type__slug=${feature_type__slug}&project__slug=${project__slug}` + + const response = await axios({ + url, + method: "PATCH", + data: { id: feature_id, status: newStatus, feature_type: feature_type__slug } + }) + if (response.status === 200 && response.data) { + return response; + } else { + return null; + } + }, + async postComment({ featureId, comment }) { const response = await axios.post( `${baseUrl}features/${featureId}/comments/`, { comment } diff --git a/src/services/featureType-api.js b/src/services/featureType-api.js new file mode 100644 index 0000000000000000000000000000000000000000..dca96343ae4d474d3b612c83ce585912162f40ae --- /dev/null +++ b/src/services/featureType-api.js @@ -0,0 +1,22 @@ +import axios from "@/axios-client.js"; +import store from '../store' + +const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; + +const featureTypeAPI = { + async deleteFeatureType(featureType_slug) { + const response = await axios.delete( + `${baseUrl}feature-types/${featureType_slug}` + ); + if ( + response.status === 204 + ) { + return 'success' + } else { + return null; + } + }, + +} + +export default featureTypeAPI; diff --git a/src/services/project-api.js b/src/services/project-api.js index f3d439cd4b4648a8080ec68e540dfbcf451d8810..6c9e7a83746313983b4feb658012b2ea4a985d3f 100644 --- a/src/services/project-api.js +++ b/src/services/project-api.js @@ -35,6 +35,17 @@ const projectAPI = { return null; } }, + + async deleteProject(projectSlug) { + const response = await axios.delete( + `${baseUrl}projects/${projectSlug}` + ); + if ( response.status === 204 ) { + return 'success'; + } else { + return null; + } + }, } export default projectAPI; diff --git a/src/store/index.js b/src/store/index.js index 379fa608c2708203b34037bece57fe1aa6e730af..a6c2008102fb6e5ace554cbc6339f6303893e276 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -93,13 +93,16 @@ export default new Vuex.Store({ SET_EVENTS(state, events) { state.events = events; }, - DISPLAY_MESSAGE(state, comment) { - state.messages = [{ comment }, ...state.messages]; + DISPLAY_MESSAGE(state, message) { + state.messages = [message, ...state.messages]; if (document.getElementById("content")) document.getElementById("content").scrollIntoView({ block: "start", inline: "nearest" }); setTimeout(() => { state.messages = []; }, 3000); }, + DISCARD_MESSAGE(state, message) { + state.messages = state.messages.filter((el) => el.comment !== message.comment) + }, CLEAR_MESSAGES(state) { state.messages = []; }, diff --git a/src/store/modules/feature.js b/src/store/modules/feature.js index fd331b50afb9e4fcb3aa32a7db9b895167c7cf90..436e57097da5f67c7cb72d13e3aa6f1436305790 100644 --- a/src/store/modules/feature.js +++ b/src/store/modules/feature.js @@ -172,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', @@ -188,7 +187,7 @@ const feature = { params: { slug_type_signal: rootState.feature_type.current_feature_type_slug, slug_signal: featureId, - message, + message: routeName === "editer-signalement" ? "Le signalement a été mis à jour" : "Le signalement a été crée" }, }); dispatch("GET_ALL_PROJECTS", null, {root:true}) //* & refresh project list @@ -201,30 +200,32 @@ const feature = { redirect(featureId); } - //* prepare feature data to send - let extraFormObject = {}; //* prepare an object to be flatten in properties of geojson - for (const field of state.extra_form) { - extraFormObject[field.name] = field.value; - } - const geojson = { - "id": state.form.feature_id, - "type": "Feature", - "geometry": state.form.geometry, - "properties": { - "title": state.form.title, - "description": state.form.description.value, - "status": state.form.status.value, - "project": rootState.project_slug, - "feature_type": rootState.feature_type.current_feature_type_slug, - ...extraFormObject + function createGeojson() { //* prepare feature data to send + let extraFormObject = {}; //* prepare an object to be flatten in properties of geojson + for (const field of state.extra_form) { + extraFormObject[field.name] = field.value; + } + return { + "id": state.form.feature_id, + "type": "Feature", + "geometry": state.form.geometry, + "properties": { + "title": state.form.title, + "description": state.form.description.value, + "status": state.form.status.value, + "project": rootState.project_slug, + "feature_type": rootState.feature_type.current_feature_type_slug, + ...extraFormObject + } } } + const geojson = createGeojson(); let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/` if (routeName === "editer-signalement") { - url += `${state.form.feature_id}/?` + - `feature_type__slug=${rootState.feature_type.current_feature_type_slug}` + - `&project__slug=${rootState.project_slug}` + url += `${state.form.feature_id}/? + feature_type__slug=${rootState.feature_type.current_feature_type_slug} + &project__slug=${rootState.project_slug}` } return axios({ @@ -250,7 +251,7 @@ const feature = { } let updateMsg = { project: rootState.project_slug, - type: 'put', + type: routeName === "editer-signalement" ? "put" : "post", featureId: state.form.feature_id, geojson: geojson }; diff --git a/src/store/modules/feature_type.js b/src/store/modules/feature_type.js index 5ad047abd1203dfde68efcb5bd30c3d692334c3b..05a611c4d8cc9dd871f551e321e8c332aaaed7df 100644 --- a/src/store/modules/feature_type.js +++ b/src/store/modules/feature_type.js @@ -194,7 +194,7 @@ const feature_type = { }) .then((response) => { if (response && response.status === 200) { - dispatch("GET_IMPORTS", { + return dispatch("GET_IMPORTS", { feature_type: feature_type_slug }); } @@ -214,12 +214,13 @@ const feature_type = { if (feature_type) { url = url.concat('', `${url.includes('?') ? '&' : '?'}feature_type_slug=${feature_type}`); } - axios + return axios .get(url) .then((response) => { if (response) { commit("SET_IMPORT_FEATURE_TYPES_DATA", response.data); } + return response; }) .catch((error) => { throw (error); diff --git a/src/views/feature/Feature_detail.vue b/src/views/feature/Feature_detail.vue index 1203b2652d9c91605698d6ffcfba74894a23a46a..bf97c9702bd6405d6966a05d84b05fd3e83242dc 100644 --- a/src/views/feature/Feature_detail.vue +++ b/src/views/feature/Feature_detail.vue @@ -504,7 +504,7 @@ export default { }, confirmComment() { - this.$store.commit("DISPLAY_MESSAGE", "Ajout du commentaire confirmé"); + this.$store.commit("DISPLAY_MESSAGE", {comment: "Ajout du commentaire confirmé", level: "positive"}); this.getFeatureEvents(); //* display new comment on the page this.comment_form.attachment_file.file = null; this.comment_form.attachment_file.fileName = ""; diff --git a/src/views/feature/Feature_list.vue b/src/views/feature/Feature_list.vue index e72194a62133f38071157354004fc5d170ad3aa3..9f08055a416e30b2b9627ff828d3ee9d23b37f13 100644 --- a/src/views/feature/Feature_list.vue +++ b/src/views/feature/Feature_list.vue @@ -36,18 +36,19 @@ feature_types.length > 0 && permissions.can_create_feature " + id="button-dropdown" class="item right" > <div - @click="showAddFeature = !showAddFeature" + @click="toggleAddFeature" class="ui dropdown button compact button-hover-green" data-tooltip="Ajouter un signalement" - data-position="bottom left" + data-position="bottom right" > <i class="plus fitted icon"></i> <div v-if="showAddFeature" - class="menu transition visible" + class="menu left transition visible" style="z-index: 9999" > <div class="header">Ajouter un signalement du type</div> @@ -67,13 +68,40 @@ </div> </div> + <div - v-if="checkedFeatures.length" + v-if="checkedFeatures.length > 0 && mode === 'modify'" + @click="toggleModifyStatus" + class="ui dropdown button compact button-hover-green margin-left-25" + data-tooltip="Modifier le statut des Signalements" + data-position="bottom right" + > + <i class="pencil fitted icon"></i> + <div + v-if="showModifyStatus" + class="menu left transition visible" + style="z-index: 9999" + > + <div class="header">Modifier le statut des Signalements</div> + <div class="scrolling menu text-wrap"> + <span + v-for="status in availableStatus" + :key="status.value" + @click="modifyStatus(status.value)" + class="item" + > + {{ status.name }} + </span> + </div> + </div> + </div> + + <div + v-if="checkedFeatures.length > 0 && mode === 'delete'" @click="modalAllDelete" class="ui button compact button-hover-red margin-left-25" data-tooltip="Effacer tous les types de signalements sélectionnés" - data-position="left center" - data-variation="mini" + data-position="bottom right" > <i class="grey trash fitted icon"></i> </div> @@ -136,12 +164,15 @@ <SidebarLayers v-if="basemaps && map" /> </div> <!-- | --> + <!-- v-on:update:clickedFeatures="handleClickedFeatures" --> <FeatureListTable v-show="!showMap" v-on:update:page="handlePageChange" v-on:update:sort="handleSortChange" :paginatedFeatures="paginatedFeatures" - :checkedFeatures.sync="checkedFeatures" + :checkedFeatures="checkedFeatures" + :clickedFeatures.sync="clickedFeatures" + :mode.sync="mode" :featuresCount="featuresCount" :pagination="pagination" :sort="sort" @@ -228,7 +259,9 @@ export default { title: null, }, baseUrl: this.$store.state.configuration.BASE_URL, + clickedFeatures: [], modalAllDeleteOpen: false, + mode: "modify", map: null, zoom: null, lat: null, @@ -250,6 +283,7 @@ export default { }, showMap: true, showAddFeature: false, + showModifyStatus: false, }; }, @@ -298,6 +332,7 @@ export default { }, computed: { + ...mapState(["user", "USER_LEVEL_PROJECTS"]), ...mapGetters([ 'project', 'permissions' ]), @@ -322,6 +357,58 @@ export default { ); }, + availableStatus() { //* attente de réponse sur le ticket + //return this.statusChoices.filter((status) => status) + + if (this.project) { + const isModerate = this.project.moderation; + const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug]; + const isOwnFeature = this.feature + ? this.feature.creator === this.user.id //* prevent undefined feature + : false; //* si le contributeur est l'auteur du signalement + if ( + //* si admin, modérateur ou super contributeur, statuts toujours disponible: Brouillon, Publié, Archivé + userStatus === "Administrateur projet" || + (userStatus === "Super Contributeur" && !isModerate) + ) { + return this.statusChoices.filter((el) => el.value !== "pending"); + } else if (userStatus === "Super Contributeur" && isModerate) { + return this.statusChoices.filter( + (el) => el.value === "draft" || el.value === "pending" + ); + } else if (userStatus === "Modérateur") { + return this.statusChoices.filter( + (el) => el.value === "draft" || el.value === "published" + ); + } else if (userStatus === "Contributeur") { + //* cas particuliers du contributeur + if ( + this.currentRouteName === "ajouter-signalement" || + !isOwnFeature + ) { + //* même cas à l'ajout d'une feature ou si feature n'a pas été créé par le contributeur + return isModerate + ? this.statusChoices.filter( + (el) => el.value === "draft" || el.value === "pending" + ) + : this.statusChoices.filter( + (el) => el.value === "draft" || el.value === "published" + ); + } else { + //* à l'édition d'une feature et si le contributeur est l'auteur de la feature + return isModerate + ? this.statusChoices.filter( + (el) => el.value !== "published" //* toutes sauf "Publié" + ) + : this.statusChoices.filter( + (el) => el.value !== "pending" //* toutes sauf "En cours de publication" + ); + } + } + } + return []; + }, + featureTypeChoices() { return this.feature_types.map((el) => el.title); }, @@ -329,15 +416,76 @@ export default { methods: { ...mapActions('feature', [ - 'GET_PROJECT_FEATURES' + 'GET_PROJECT_FEATURES', + 'SEND_FEATURE' ]), + + toggleAddFeature() { + this.showAddFeature = !this.showAddFeature; + this.showModifyStatus = false; + }, + + toggleModifyStatus() { + this.showModifyStatus = !this.showModifyStatus; + this.showAddFeature = false; + }, + modalAllDelete() { this.modalAllDeleteOpen = !this.modalAllDeleteOpen; }, + clickOutsideDropdown(e) { + if (!e.target.closest("#button-dropdown")) { + this.showModifyStatus = false; + setTimeout(() => { //* timout necessary to give time to click on link to add feature + this.showAddFeature = false; + }, 500); + } + }, + + async modifyStatus(newStatus) { + let errorCount = 0 + const promises = this.checkedFeatures.map((feature_id) => { + let feature = this.clickedFeatures.find((el) => el.feature_id === feature_id) + if (feature) { + return featureAPI.updateFeature({ + feature_id, + feature_type__slug: feature.feature_type, + project__slug: this.$route.params.slug, newStatus + }) + } else { + errorCount += 1; + } + }) + const promisesResult = await Promise.all(promises) + promisesResult.forEach((response) => { + if (response && response.data && response.status === 200) { + this.checkedFeatures.splice(this.checkedFeatures.indexOf(response.data.id), 2); + } else { + errorCount += 1; + } + }) + let message = { + comment: "Tous les signalements ont été modifié avec succès.", + level: "positive" + } + if (errorCount) { + //* display error message + if(errorCount === 1) { + message.comment = "Un signalement n'a pas pu être modifié. (Il reste sélectionné)" + } else { + message.comment = `${errorCount} signalements n'ont pas pu être modifiés. (Ils restent sélectionnés)` + } + message.level = "negative" + } + this.fetchPagedFeatures(); + this.$store.commit("DISPLAY_MESSAGE", message); + }, + + deleteFeature(feature_id) { const url = `${this.API_BASE_URL}features/${feature_id}/?project__slug=${this.project.slug}`; - axios + axios //TODO: REFACTO -> Delete function already exist in store .delete(url, {}) .then(() => { if (!this.modalAllDeleteOpen) { @@ -346,7 +494,6 @@ export default { }) .then(() => { this.fetchPagedFeatures(); - this.getNloadGeojsonFeatures(); this.checkedFeatures.splice(feature_id); }); } @@ -359,8 +506,8 @@ export default { deleteAllFeatureSelection() { let feature = {}; this.checkedFeatures.forEach((feature_id) => { - feature = { feature_id: feature_id }; - this.deleteFeature(feature.feature_id); + feature = { feature_id: feature_id }; // ? Is this usefull ? + this.deleteFeature(feature.feature_id); //? since property feature_id is directly used after... }); this.modalAllDelete(); }, @@ -461,8 +608,8 @@ export default { 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 receiving next & previous url if (newUrl && typeof newUrl === "string") { - //* if receiving next & previous url //newUrl = newUrl.replace("8000", "8010"); //* for dev uncomment to use proxy link url = newUrl; } @@ -550,9 +697,11 @@ export default { this.initMap(); } this.fetchPagedFeatures(); + window.addEventListener("mousedown", this.clickOutsideDropdown); }, destroyed() { + window.removeEventListener("mousedown", this.clickOutsideDropdown); //* allow user to change page if ever stuck on loader this.$store.commit("DISCARD_LOADER"); }, @@ -570,10 +719,6 @@ export default { z-index: 1; } -.center { - text-align: center !important; -} - #feature-list-container { justify-content: flex-start; } @@ -600,20 +745,23 @@ export default { padding: 0 !important; } +.ui.dropdown .menu .left.menu, .ui.dropdown > .left.menu .menu { + margin-right: 0 !important; +} + @media screen and (min-width: 767px) { .twelve-wide { width: 75% !important; } } + @media screen and (max-width: 767px) { #feature-list-container > .mobile-fullwidth { width: 100% !important; } - .no-margin-mobile { margin: 0 !important; } - .no-padding-mobile { padding-top: 0 !important; padding-bottom: 0 !important; @@ -621,10 +769,12 @@ export default { .mobile-column { flex-direction: column !important; } + #button-dropdown { + transform: translate(-50px, -60px); + } #form-filters > .field.column { width: 100% !important; } - .map-container { width: 100%; } diff --git a/src/views/feature_type/Feature_type_detail.vue b/src/views/feature_type/Feature_type_detail.vue index 6cc002d45fe4ad1acdb48a008074eb93b708ba1c..4ae9047be983251672f63302e2545a1ba9dbae48 100644 --- a/src/views/feature_type/Feature_type_detail.vue +++ b/src/views/feature_type/Feature_type_detail.vue @@ -2,7 +2,7 @@ <div v-if="structure" class="row"> <div class="five wide column"> <div class="ui attached secondary segment"> - <h1 class="ui center aligned header"> + <h1 class="ui center aligned header ellipsis"> <img v-if="structure.geom_type === 'point'" class="ui medium image" @@ -72,7 +72,7 @@ :class="loadingImportFile ? 'loading' : ''" > <div class="field"> - <label class="ui icon button" for="json_file"> + <label class="ui icon button ellipsis" for="json_file"> <i class="file icon"></i> <span class="label">{{ fileToImport.name }}</span> </label> @@ -100,6 +100,7 @@ <ImportTask v-if="importFeatureTypeData && importFeatureTypeData.length" :data="importFeatureTypeData" + :reloading="reloadingImport" /> </div> </div> @@ -149,6 +150,15 @@ Pour suivre le statut de l'import, cliquez sur "Importer des Signalements". </p> </div> + <div + v-else-if="waitMessage" + class="ui message info" + > + <p> + L'import des signalements a été lancé. + Vous pourrez suivre le statut de l'import dans quelques instants... + </p> + </div> <div v-for="(feature, index) in lastFeatures" :key="feature.feature_id + index" @@ -243,7 +253,9 @@ export default { }, showImport: false, featuresLoading: true, - loadingImportFile: false + loadingImportFile: false, + waitMessage: false, + reloadingImport: false, }; }, @@ -260,6 +272,9 @@ export default { 'project', 'permissions' ]), + ...mapState([ + 'reloadIntervalId' + ]), ...mapState('feature', [ 'features', 'features_count' @@ -307,9 +322,27 @@ export default { if (newValue.slug){ this.GET_IMPORTS({ feature_type: this.$route.params.feature_type_slug - }); + }) } - } + }, + + importFeatureTypeData: { + deep: true, + handler(newValue) { + if (newValue && newValue.some(el => el.status === 'pending')) { + setTimeout(() => { + this.reloadingImport = true; + this.GET_IMPORTS({ + feature_type: this.$route.params.feature_type_slug + }).then(()=> { + setTimeout(() => { + this.reloadingImport = false; + }, 1000); + }) + }, this.$store.state.configuration.VUE_APP_RELOAD_INTERVAL); + } + } + }, }, created() { @@ -341,9 +374,8 @@ export default { toggleShowImport() { this.showImport = !this.showImport; if (this.showImport) { - this.$store.dispatch("feature_type/GET_IMPORTS", { - feature_type: this.$route.params.feature_type_slug - }); + this.GET_IMPORTS({ + feature_type: this.$route.params.feature_type_slug }); } }, @@ -432,11 +464,14 @@ export default { }, importGeoJson() { + this.waitMessage = true; this.$store.dispatch('feature_type/SEND_FEATURES_FROM_GEOJSON', { slug: this.$route.params.slug, feature_type_slug: this.$route.params.feature_type_slug, fileToImport: this.fileToImport, - }); + }).then(() => { + this.waitMessage = false; + }) }, exportFeatures() { diff --git a/src/views/feature_type/Feature_type_edit.vue b/src/views/feature_type/Feature_type_edit.vue index dfa8937c9abfed92bd24493900020aac8916ea56..823914679f737ce657ffa3fc09024a00b0e0216f 100644 --- a/src/views/feature_type/Feature_type_edit.vue +++ b/src/views/feature_type/Feature_type_edit.vue @@ -104,6 +104,23 @@ :placeholder="'Sélectionner la liste de valeurs'" /> </div> + <div class="colors_selection" id="id_colors_selection" hidden> + <div + v-for="(value, key, index) in form.colors_style.value.colors" + :key="'colors_style-' + index" + > + <div v-if="key" class="color-input"> + <label>{{ key }}</label + ><input + :name="key" + type="color" + :value="value" + @input="setColorStyles" + /> + </div> + </div> + </div> + </div> <span v-if="action === 'duplicate' || action === 'edit'"> </span> diff --git a/src/views/feature_type/Feature_type_symbology.vue b/src/views/feature_type/Feature_type_symbology.vue index f49cb9053473aba19eb9a4ee789cde9316b44406..6baac62261e7ecb6a52ee4a6d6a9f46edbe2891f 100644 --- a/src/views/feature_type/Feature_type_symbology.vue +++ b/src/views/feature_type/Feature_type_symbology.vue @@ -191,13 +191,17 @@ export default { } this.SET_CURRENT_FEATURE_TYPE_SLUG(this.$route.params.slug_type_signal); if (this.feature_type) { - // Init form - this.form.color = JSON.parse(JSON.stringify(this.feature_type.color)); - this.form.icon = JSON.parse(JSON.stringify(this.feature_type.icon)); - this.form.colors_style = { - ...this.form.colors_style, - ...JSON.parse(JSON.stringify(this.feature_type.colors_style)) - }; + this.initForm(); + } else { + this.loading = true; + this.GET_PROJECT_FEATURE_TYPES(this.$route.params.slug) + .then(() => { + this.initForm(); + this.loading = false; + }) + .catch(() => { + this.loading = false; + }); } }, @@ -213,6 +217,19 @@ export default { 'GET_PROJECT_INFO' ]), + initForm() { + this.form.color = JSON.parse(JSON.stringify(this.feature_type.color)); + this.form.icon = JSON.parse(JSON.stringify(this.feature_type.icon)); + this.form.colors_style = { + ...this.form.colors_style, + ...JSON.parse(JSON.stringify(this.feature_type.colors_style)) + }; + if (this.feature_type.colors_style && Object.keys(this.feature_type.colors_style.colors).length > 0) { + this.selectedCustomfield = + this.feature_type.customfield_set.find(el => el.name === this.feature_type.colors_style.custom_field_name).name; + } + }, + setDefaultStyle(e) { const value = e.value; this.form.color = value.color.value; @@ -235,7 +252,7 @@ export default { .then(() => { this.loading = false; this.success = - 'La modification de la symbologie a été prise en compte. Vous allez être redirigé vers la page d\'acceuil du projet.'; + 'La modification de la symbologie a été prise en compte. Vous allez être redirigé vers la page d\'accueil du projet.'; setTimeout(() => { this.$router.push({ name: 'project_detail', diff --git a/src/views/project/Project_detail.vue b/src/views/project/Project_detail.vue index 9cf071f4e9bd662c0b0683157023ccc6a03bb792..a0cd446abded78b8bc41a6cc956d65f3ee7fa81b 100644 --- a/src/views/project/Project_detail.vue +++ b/src/views/project/Project_detail.vue @@ -72,7 +72,7 @@ data-tooltip="S'abonner au projet" data-position="top center" data-variation="mini" - @click="isModalOpen = true" + @click="modalType = 'subscribe'" > <i class="inverted grey envelope icon"></i> </a> @@ -90,6 +90,22 @@ > <i class="inverted grey pencil alternate icon"></i> </router-link> + <a + v-if=" + user_permissions && + user_permissions[project.slug] && + user_permissions[project.slug].is_project_administrator && + isOffline() !== true + " + id="delete-button" + class="ui button button-hover-red" + data-tooltip="Supprimer le projet" + data-position="top center" + data-variation="mini" + @click="modalType = 'deleteProject'" + > + <i class="inverted grey trash icon"></i> + </a> </div> <button v-if="user && user.is_administrator && !isSharedProject && project.generate_share_link" @@ -202,7 +218,7 @@ button button-hover-green " data-tooltip="Ajouter un signalement" - data-position="left center" + data-position="top right" data-variation="mini" > <i class="ui plus icon"></i> @@ -228,7 +244,7 @@ button button-hover-green " data-tooltip="Dupliquer un type de signalement" - data-position="left center" + data-position="top right" data-variation="mini" > <i class="inverted grey copy alternate icon"></i> @@ -241,6 +257,29 @@ Import en cours </div> <div v-else v-frag> + <a + v-if=" + user_permissions && + user_permissions[project.slug] && + user_permissions[project.slug].is_project_administrator && + isOffline() !== true + " + @click="toggleDeleteFeatureType(type)" + class=" + ui + compact + small + icon + right + floated + button button-hover-red + " + data-tooltip="Supprimer le type de signalement" + data-position="top center" + data-variation="mini" + > + <i class="inverted grey trash alternate icon"></i> + </a> <router-link :to="{ name: 'editer-symbologie-signalement', @@ -260,10 +299,10 @@ icon right floated - button button-hover-green + button button-hover-orange " data-tooltip="Éditer la symbologie du type de signalement" - data-position="left center" + data-position="top center" data-variation="mini" > <i class="inverted grey paint brush alternate icon"></i> @@ -287,10 +326,10 @@ icon right floated - button button-hover-green + button button-hover-orange " data-tooltip="Éditer le type de signalement" - data-position="left center" + data-position="top left" data-variation="mini" > <i class="inverted grey pencil alternate icon"></i> @@ -565,32 +604,54 @@ </span> <div - v-if="isModalOpen" + v-if="modalType" class="ui dimmer modals page transition visible active" style="display: flex !important" > <div :class="[ 'ui mini modal subscription', - { 'transition visible active': isModalOpen }, + { 'transition visible active': modalType }, ]" > - <i @click="isModalOpen = false" class="close icon"></i> + <i @click="modalType = false" class="close icon"></i> <div class="ui icon header"> - <i class="envelope icon"></i> - Notifications du projet + <i :class="[modalType === 'subscribe' ? 'envelope' : 'trash', 'icon']"></i> + {{ + modalType === 'subscribe' ? 'Notifications' : 'Suppression' + }} du {{ + modalType === 'deleteFeatureType' ? 'type de signalement ' + featureTypeToDelete.title : 'projet' + }} </div> - <div class="content"> + <div v-if="modalType !== 'subscribe'" > + + <p class="centered-text"> + Confirmez vous la suppression du {{ modalType === 'deleteProject' ? 'projet, ainsi que les types de signalements' : 'type de signalement'}} et tous les signalements associés ? + </p> + <p class="centered-text alert"> + Attention cette action est irreversible ! + </p> + </div> <button - @click="subscribeProject" - :class="['ui compact fluid button', is_suscriber ? 'red' : 'green']" + @click="handleModalClick" + :class="['ui compact fluid button', modalType === 'subscribe' && !is_suscriber ? 'green' : 'red']" > + <span v-if="modalType === 'subscribe'"> {{ is_suscriber ? "Se désabonner de ce projet" : "S'abonner à ce projet" }} + </span> + <span v-else> + Supprimer le + {{ + modalType === 'deleteProject' + ? 'projet' + : 'type de signalement' + }} + </span> </button> </div> </div> @@ -631,6 +692,7 @@ import frag from "vue-frag"; import { mapUtil } from "@/assets/js/map-util.js"; import { mapGetters, mapState, mapActions, mapMutations } from "vuex"; import projectAPI from "@/services/project-api"; +import featureTypeAPI from "@/services/featureType-api"; import featureAPI from "@/services/feature-api"; import axios from "@/axios-client.js"; @@ -668,11 +730,12 @@ export default { geojsonImport: [], fileToImport: { name: "", size: 0 }, slug: this.$route.params.slug, - isModalOpen: false, + modalType: false, is_suscriber: false, tempMessage: null, projectInfoLoading: true, featureTypeImporting: false, + featureTypeToDelete: null, featuresLoading: true, isFileSizeModalOpen: false, // mapFeatures: null, @@ -683,7 +746,7 @@ export default { computed: { ...mapGetters([ 'project', - 'permissions' + 'permissions', ]), ...mapState('feature_type', [ 'feature_types', @@ -695,7 +758,8 @@ export default { ...mapState([ 'last_comments', 'user', - 'reloadIntervalId' + 'user_permissions', + 'reloadIntervalId', ]), ...mapState('map', [ 'map' @@ -741,26 +805,6 @@ export default { } } }, - - features: { - deep: true, - handler(newValue, oldValue) { - if (newValue && newValue.length && newValue !== oldValue) { - mapUtil.addFeatures( - this.features, - {}, - true, - this.feature_types - ); - this.mapLoading = false; - } - } - }, - featuresLoading(newValue) { - if (!newValue && this.features && this.features.length === 0) { - this.mapLoading = false; - } - } }, created() { @@ -775,15 +819,7 @@ export default { }, mounted() { - this.GET_PROJECT_INFO(this.slug) - .then(() => { - this.projectInfoLoading = false; - setTimeout(this.initMap, 1000); - }) - .catch((err) => { - console.error(err) - this.projectInfoLoading = false; - }); + this.retrieveProjectInfo(); if (this.message) { this.tempMessage = this.message; @@ -801,10 +837,12 @@ export default { methods: { ...mapMutations([ 'SET_RELOAD_INTERVAL_ID', - 'CLEAR_RELOAD_INTERVAL_ID' + 'CLEAR_RELOAD_INTERVAL_ID', + 'DISPLAY_MESSAGE', ]), ...mapActions([ - 'GET_PROJECT_INFO' + 'GET_PROJECT_INFO', + 'GET_ALL_PROJECTS', ]), ...mapActions('map', [ 'INITIATE_MAP' @@ -849,6 +887,22 @@ export default { ) }, + retrieveProjectInfo() { + this.GET_PROJECT_INFO(this.slug) + .then(() => { + this.projectInfoLoading = false; + setTimeout(() => { + let map = mapUtil.getMap(); + if (map) map.remove(); + this.initMap(); + }, 1000); + }) + .catch((err) => { + console.error(err) + this.projectInfoLoading = false; + }); + }, + checkForOfflineFeature() { let arraysOffline = []; let localStorageArray = localStorage.getItem("geocontrib_offline"); @@ -980,7 +1034,7 @@ export default { }) .then((data) => { this.is_suscriber = data.is_suscriber; - this.isModalOpen = false; + this.modalType = false; if (this.is_suscriber) this.infoMessage = "Vous êtes maintenant abonné aux notifications de ce projet."; @@ -990,6 +1044,54 @@ export default { setTimeout(() => (this.infoMessage = ""), 3000); }); }, + + deleteProject() { + projectAPI.deleteProject(this.project.slug) + .then((response) => { + if (response === 'success') { + this.GET_ALL_PROJECTS(); + this.$router.push('/'); + this.DISPLAY_MESSAGE(`Le projet ${this.project.title} a bien été supprimé.`) + } else { + this.DISPLAY_MESSAGE(`Une erreur est survenu lors de la suppression du projet ${this.project.title}.`) + } + }) + }, + + deleteFeatureType() { + featureTypeAPI.deleteFeatureType(this.featureTypeToDelete.slug) + .then((response) => { + this.modalType = false; + if (response === 'success') { + this.GET_ALL_PROJECTS(); + this.retrieveProjectInfo(); + this.DISPLAY_MESSAGE(`Le type de signalement ${this.featureTypeToDelete.title} a bien été supprimé.`) + } else { + this.DISPLAY_MESSAGE(`Une erreur est survenu lors de la suppression du type de signalement ${this.featureTypeToDelete.title}.`) + } + this.featureTypeToDelete = null; + }) + }, + + handleModalClick() { + switch (this.modalType) { + case 'subscribe': + this.subscribeProject(); + break; + case 'deleteProject': + this.deleteProject(); + break; + case 'deleteFeatureType': + this.deleteFeatureType(); + break; + } + }, + + toggleDeleteFeatureType(featureType) { + this.featureTypeToDelete = featureType; + this.modalType = 'deleteFeatureType'; + }, + async initMap() { if (this.project && this.permissions.can_view_project) { await this.INITIATE_MAP(this.$refs.map); @@ -1009,6 +1111,7 @@ export default { true, this.$store.state.feature_type.feature_types ); + this.mapLoading = false; this.GET_PROJECT_FEATURES({ project_slug: this.slug, @@ -1114,4 +1217,10 @@ export default { .ui.button, .ui.button .button, .tiny-margin { margin: 0.1rem 0 0.1rem 0.1rem !important; } -</style> \ No newline at end of file +.alert { + color: red; +} +.centered-text { + text-align: center; +} +</style> diff --git a/src/views/project/Project_members.vue b/src/views/project/Project_members.vue index aea8f4b1340c1ea3ed81f8ee426985195632eb74..dc2dff3db450ad731794a04efdeaa16d70b010fb 100644 --- a/src/views/project/Project_members.vue +++ b/src/views/project/Project_members.vue @@ -273,11 +273,14 @@ export default { .then((response) => { if (response.status === 200) { this.$store.dispatch("GET_USER_LEVEL_PROJECTS"); //* update user status in top right menu - this.$store.commit("DISPLAY_MESSAGE", "Permissions mises à jour"); + this.$store.commit("DISPLAY_MESSAGE", {comment: "Permissions mises à jour", level: "positive"}); } else { this.$store.commit( "DISPLAY_MESSAGE", - "Une erreur s'est produite pendant la mises à jour des permissions" + { + comment : "Une erreur s'est produite pendant la mises à jour des permissions", + level: "negative" + } ); } }) diff --git a/src/views/registration/Login.vue b/src/views/registration/Login.vue index f26884b1113a2d6bb9728c6e84c49ed48826a2b5..f9614efa8e415cb53704a50200c3668d964152cb 100644 --- a/src/views/registration/Login.vue +++ b/src/views/registration/Login.vue @@ -102,7 +102,7 @@ export default { if (this.$store.state.user) { this.$store.commit( "DISPLAY_MESSAGE", - "Vous êtes déjà connecté, vous allez être redirigé vers la page d'accueil." + {comment :"Vous êtes déjà connecté, vous allez être redirigé vers la page d'accueil."} ); setTimeout(() => this.$router.push("/"), 3100); }