<template> <div id="feature-detail"> <div v-if="currentFeature" class="ui grid stackable" > <div class="row"> <div class="sixteen wide column"> <FeatureHeader v-if="project" :features-count="featuresCount" :slug-signal="slugSignal" :feature-type="feature_type" :fast-edition-mode="project.fast_edition_mode" :is-feature-creator="isFeatureCreator" :can-edit-feature="canEditFeature" :can-delete-feature="canDeleteFeature" @fastEditFeature="validateFastEdition" @setIsDeleting="isDeleting = true" @tofeature="pushNgo" /> </div> </div> <div class="row"> <div class="eight wide column"> <FeatureTable v-if="project" ref="featureTable" :feature-type="feature_type" :fast-edition-mode="project.fast_edition_mode" :can-edit-feature="canEditFeature" @tofeature="pushNgo" /> </div> <div class="eight wide column"> <div class="map-container"> <div id="map" ref="map" /> <SidebarLayers v-if="basemaps && map" ref="sidebar" /> <div id="popup" class="ol-popup" > <a id="popup-closer" href="#" class="ol-popup-closer" /> <div id="popup-content" /> </div> </div> </div> </div> <div class="row"> <div class="eight wide column"> <FeatureAttachements :attachments="attachments" /> </div> <div class="eight wide column"> <FeatureComments :events="events" @fetchEvents="getFeatureEvents" /> </div> </div> <div v-if="isDeleting" class="ui dimmer modals visible active" > <div :class="[ 'ui mini modal', { 'active visible': isDeleting }, ]" > <i class="close icon" aria-hidden="true" @click="isDeleting = false" /> <div v-if="isDeleting" class="ui icon header" > <i class="trash alternate icon" aria-hidden="true" /> Supprimer le signalement </div> <div class="actions"> <button type="button" class="ui red compact fluid button" @click="deleteFeature" > Confirmer la suppression </button> </div> </div> </div> <div v-if="isLeaving" class="ui dimmer modals visible active" > <div :class="[ 'ui mini modal', { 'active visible': isLeaving }, ]" > <i class="close icon" aria-hidden="true" @click="isLeaving = false" /> <div class="ui icon header"> <i class="sign-out icon" aria-hidden="true" /> Abandonner les modifications </div> <div class="content"> Les modifications apportées au signalement ne seront pas sauvegardées, continuer ? </div> <div class="actions"> <button type="button" class="ui green compact button" @click="stayOnPage" > <i class="close icon" aria-hidden="true" /> Annuler </button> <button type="button" class="ui red compact button" @click="leavePage" > Continuer <i class="arrow right icon" aria-hidden="true" /> </button> </div> </div> </div> </div> <div v-else> Pas de signalement correspondant trouvé </div> </div> </template> <script> import { mapState, mapActions, mapMutations, mapGetters } from 'vuex'; import mapService from '@/services/map-service'; import axios from '@/axios-client.js'; import featureAPI from '@/services/feature-api'; import FeatureHeader from '@/components/Feature/Detail/FeatureHeader'; import FeatureTable from '@/components/Feature/Detail/FeatureTable'; import FeatureAttachements from '@/components/Feature/Detail/FeatureAttachements'; import FeatureComments from '@/components/Feature/Detail/FeatureComments'; import SidebarLayers from '@/components/Map/SidebarLayers'; import { buffer } from 'ol/extent'; export default { name: 'FeatureDetail', components: { FeatureHeader, FeatureTable, FeatureAttachements, FeatureComments, SidebarLayers, }, beforeRouteUpdate (to, from, next) { if (this.hasUnsavedChange && !this.isSavingChanges) { this.confirmLeave(next); // prompt user that there is unsaved changes or that features order might change } else { next(); // continue navigation } }, beforeRouteLeave (to, from, next) { if (this.hasUnsavedChange && !this.isSavingChanges) { this.confirmLeave(next); // prompt user that there is unsaved changes or that features order might change } else { next(); // continue navigation } }, data() { return { attachments: [], comment_form: { attachment_file: { errors: null, title: '', file: null, }, comment: { id_for_label: 'add-comment', html_name: 'add-comment', errors: '', value: null, }, }, events: [], featuresCount: null, isDeleting: false, isLeaving: false, isSavingChanges: false, map: null, slugSignal: '', }; }, computed: { ...mapState([ 'USER_LEVEL_PROJECTS', 'user' ]), ...mapState('projects', [ 'project' ]), ...mapState('feature-type', [ 'feature_types', ]), ...mapState('feature', [ 'currentFeature', 'form', ]), ...mapGetters('feature-type', [ 'feature_type', ]), ...mapGetters([ 'permissions', ]), ...mapState('map', [ 'basemaps', ]), hasUnsavedChange() { if (this.project.fast_edition_mode && this.form && this.currentFeature && this.currentFeature.properties) { if (this.form.title !== this.currentFeature.properties.title) return true; if (this.form.description.value !== this.currentFeature.properties.description) return true; if (this.form.status.value !== this.currentFeature.properties.status) return true; for (const xForm of this.$store.state.feature.extra_forms) { const originalValue = this.currentFeature.properties[xForm.name]; if (xForm.value !== originalValue) return true; } } return false; }, isFeatureCreator() { if (this.currentFeature && this.currentFeature.properties && this.user) { return this.currentFeature.properties.creator === this.user.id; } return false; }, isModerator() { return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Modérateur'; }, isAdministrator() { return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Administrateur projet'; }, canEditFeature() { return (this.permissions && this.permissions.can_update_feature) || this.isFeatureCreator || this.isModerator || this.user.is_superuser; }, canDeleteFeature() { return (this.permissions && this.permissions.can_delete_feature && this.isFeatureCreator) || this.isFeatureCreator || this.isModerator || this.isAdministrator || this.user.is_superuser; }, }, watch: { '$route.query'(newValue, oldValue) { if (newValue !== oldValue) { //* Navigate back or forward to the previous or next URL this.initPage(); //* doesn't update the page at query changes, thus it is done manually here } }, }, mounted() { this.initPage(); }, beforeDestroy() { this.$store.commit('CLEAR_MESSAGES'); this.$store.dispatch('CANCEL_CURRENT_SEARCH_REQUEST'); }, methods: { ...mapMutations([ 'DISPLAY_LOADER', 'DISCARD_LOADER' ]), ...mapMutations('feature', [ 'SET_CURRENT_FEATURE' ]), ...mapMutations('feature-type', [ 'SET_CURRENT_FEATURE_TYPE_SLUG' ]), ...mapActions('projects', [ 'GET_PROJECT', 'GET_PROJECT_INFO' ]), ...mapActions('feature', [ 'GET_PROJECT_FEATURE', 'GET_PROJECT_FEATURES' ]), async initPage() { await this.getPageInfo(); if (this.currentFeature) this.initMap(); }, async getPageInfo() { if (this.$route.params.slug_signal && this.$route.params.slug_type_signal) { // if coming from the route with an id this.SET_CURRENT_FEATURE_TYPE_SLUG(this.$route.params.slug_type_signal); this.slugSignal = this.$route.params.slug_signal; } //* else it would be retrieve after fetchFilteredFeature with offset this.DISPLAY_LOADER('Recherche du signalement'); let promises = []; //* Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh if (!this.project) { promises.push( this.GET_PROJECT(this.$route.params.slug), this.GET_PROJECT_INFO(this.$route.params.slug), ); } //* changement de requête selon s'il y a un id ou un offset(dans le cas du parcours des signalements filtrés) if (this.$route.query.offset >= 0) { promises.push(this.fetchFilteredFeature()); } else if (!this.currentFeature || this.currentFeature.id !== this.slugSignal) { promises.push( this.GET_PROJECT_FEATURE({ project_slug: this.$route.params.slug, feature_id: this.slugSignal, }) ); } await axios.all(promises); this.DISCARD_LOADER(); if (this.currentFeature) { this.getFeatureEvents(); this.getFeatureAttachments(); this.getLinkedFeatures(); if (this.project.fast_edition_mode) { this.$store.commit('feature/INIT_FORM'); this.$store.dispatch('feature/INIT_EXTRA_FORMS'); } } }, confirmLeave(next) { this.next = next; this.isLeaving = true; }, stayOnPage() { this.isLeaving = false; }, leavePage() { this.isLeaving = false; this.next(); }, async reloadPage() { await this.getPageInfo(); mapService.removeFeatures(); this.addFeatureToMap(); }, pushNgo(newEntry) { this.$router.push(newEntry) //* update the params or queries in the route/url .then(() => { this.reloadPage(); }) .catch(() => true); //* catch error if navigation get aborted (in beforeRouteUpdate) }, goBackToProject(message) { this.$router.push({ name: 'project_detail', params: { slug: this.$route.params.slug, message, }, }); }, deleteFeature() { this.isDeleting = false; this.DISPLAY_LOADER('Suppression du signalement en cours...'); this.$store .dispatch('feature/DELETE_FEATURE', { feature_id: this.currentFeature.id }) .then(async (response) => { this.DISCARD_LOADER(); if (response.status === 200) { this.goBackToProject({ comment: 'Le signalement a bien été supprimé', level: 'positive' }); } else { this.$store.commit('DISPLAY_MESSAGE', { comment: 'Une erreur est survenue pendant la suppression du signalement', level: 'negative' }); } }); }, fetchFilteredFeature() { // TODO : if no query for sort, use project default ones const queryString = new URLSearchParams({ ...this.$route.query }); const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature-paginated/?limit=1&${queryString}&output=geojson`; return featureAPI.getPaginatedFeatures(url) .then((data) => { if (data && data.results && data.results.features && data.results.features[0]) { this.featuresCount = data.count; this.previous = data.previous; this.next = data.next; const currentFeature = data.results.features[0]; this.slugSignal = currentFeature.id; this.SET_CURRENT_FEATURE(currentFeature); this.SET_CURRENT_FEATURE_TYPE_SLUG(currentFeature.properties.feature_type.slug); return { feature_id: currentFeature.id }; } return; }); }, initMap() { var mapDefaultViewCenter = this.$store.state.configuration.DEFAULT_MAP_VIEW.center; var mapDefaultViewZoom = this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom; this.map = mapService.createMap(this.$refs.map, { mapDefaultViewCenter, mapDefaultViewZoom, maxZoom: this.project.map_max_zoom_level, interactions : { doubleClickZoom :false, mouseWheelZoom: true, dragPan: true } }); // Update link to feature list with map zoom and center mapService.addMapEventListener('moveend', function () { // update link to feature list with map zoom and center /*var $featureListLink = $("#feature-list-link") var baseUrl = $featureListLink.attr("href").split("?")[0] $featureListLink.attr("href", baseUrl +`?zoom=${this.map.getZoom()}&lat=${this.map.getCenter().lat}&lng=${this.map.getCenter().lng}`)*/ }); // Load the layers. // - if one basemap exists, we load the layers of the first one // - if not, load the default map and service options let layersToLoad = null; const baseMaps = this.basemaps; var layers = this.$store.state.map.availableLayers; if (baseMaps && baseMaps.length > 0) { const basemapIndex = 0; layersToLoad = baseMaps[basemapIndex].layers; layersToLoad.forEach((layerToLoad) => { layers.forEach((layer) => { if (layer.id === layerToLoad.id) { layerToLoad = Object.assign(layerToLoad, layer); } }); }); layersToLoad.reverse(); } mapService.addLayers( layersToLoad, this.$store.state.configuration.DEFAULT_BASE_MAP_SERVICE, this.$store.state.configuration.DEFAULT_BASE_MAP_OPTIONS, this.$store.state.configuration.DEFAULT_BASE_MAP_SCHEMA_TYPE, ); this.addFeatureToMap(); }, addFeatureToMap() { const featureGroup = mapService.addFeatures({ features: [this.currentFeature], featureTypes: this.feature_types, addToMap: true, }); mapService.fitExtent(buffer(featureGroup.getExtent(),200)); }, getFeatureEvents() { featureAPI .getFeatureEvents(this.slugSignal) .then((data) => (this.events = data)); }, getFeatureAttachments() { featureAPI .getFeatureAttachments(this.slugSignal) .then((data) => (this.attachments = data)); }, getLinkedFeatures() { featureAPI .getFeatureLinks(this.slugSignal) .then((data) => this.$store.commit('feature/SET_LINKED_FEATURES', data) ); }, checkAddedForm() { let isValid = true; //* fallback if all customForms returned true if (this.$refs.featureTable && this.$refs.featureTable.$refs.extraForm) { for (const extraForm of this.$refs.featureTable.$refs.extraForm) { if (extraForm.checkForm() === false) { isValid = false; } } } return isValid; }, validateFastEdition() { let is_valid = true; is_valid = this.checkAddedForm(); if (is_valid) { this.isSavingChanges = true; // change the value to avoid confirmation popup after redirection with new query this.$store.dispatch( 'feature/SEND_FEATURE', { routeName: this.$route.name, query: this.$route.query } ).then((response) => { if (response === 'reloadPage') { this.reloadPage(); } }); } } }, }; </script> <style scoped> .map-container { height: 100%; position: relative; overflow: hidden; z-index: 1; background-color: #fff; } #map { width: 100%; height: 100%; min-height: 250px; max-height: 70vh; border: 1px solid grey; } .prewrap { white-space: pre-wrap; } .ui.active.dimmer { position: fixed; } .ui.modal > .content { text-align: center; } .ui.modal > .actions { display: flex; justify-content: space-evenly; } </style>