diff --git a/package.json b/package.json index 07dc504cde35df55e6c8a36fac8195321eae375b..fa44bcd1207b3c48ee3f17186f546b7cdc42e50b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geocontrib-frontend", - "version": "2.3.2-rc4", + "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 5141ee679697634a15694d30513b8ee8d5afeed6..4593a147766d307f0891937622a7b0ee3b46dee3 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/assets/styles/base.css b/src/assets/styles/base.css index 4882654c99b1a5663812d7a9ae9947cf797b5a4e..54967e93dfa00977620d28a5f91802b0bee582ec 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/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 bcc864f81a3779dfade408de3c5e189901a97311..93c19e017d8ceb9bc5fcd7a4537dfa9274e36419 100644 --- a/src/services/project-api.js +++ b/src/services/project-api.js @@ -54,8 +54,18 @@ const projectAPI = { console.error(error); throw error; } - } + }, -}; + 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/modules/feature_type.store.js b/src/store/modules/feature_type.store.js index 5ad047abd1203dfde68efcb5bd30c3d692334c3b..05a611c4d8cc9dd871f551e321e8c332aaaed7df 100644 --- a/src/store/modules/feature_type.store.js +++ b/src/store/modules/feature_type.store.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_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 90488ea26afed108904d912e535260b15fd7c0e9..c01b58538f9adec34edc21ffef3ce1eefc959298 100644 --- a/src/views/feature_type/Feature_type_edit.vue +++ b/src/views/feature_type/Feature_type_edit.vue @@ -102,6 +102,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 24afe7b5e9951d50108670a5903a509a95302961..8517b9d61174957002b43aeda0724d6644b6a077 100644 --- a/src/views/project/Project_detail.vue +++ b/src/views/project/Project_detail.vue @@ -75,7 +75,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> @@ -93,6 +93,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> <div class="ui hidden divider"></div> <div class="sub header"> @@ -180,7 +196,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> @@ -206,7 +222,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> @@ -219,6 +235,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', @@ -238,10 +277,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> @@ -265,10 +304,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> @@ -543,32 +582,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> @@ -609,6 +670,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"; @@ -645,11 +707,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, @@ -674,7 +737,8 @@ export default { ...mapState([ 'last_comments', 'user', - 'reloadIntervalId' + 'user_permissions', + 'reloadIntervalId', ]), ...mapState('map', [ 'map' @@ -734,15 +798,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; @@ -760,10 +816,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' @@ -794,6 +852,22 @@ export default { return false; }, + 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"); @@ -926,7 +1000,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."; @@ -936,6 +1010,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); @@ -1049,3 +1171,12 @@ export default { color: teal; } </style> + +<style scoped> +.alert { + color: red; +} +.centered-text { + text-align: center; +} +</style>