<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="featureType" :fast-edition-mode="project.fast_edition_mode" :display-to-list-button="displayToListButton" :is-feature-creator="isFeatureCreator" :can-edit-feature="canEditFeature" :can-delete-feature="canDeleteFeature" @setIsDeleting="isDeleting = true" @tofeature="pushNgo" /> </div> </div> <div class="row"> <div class="eight wide column"> <FeatureTable v-if="project" :feature-type="featureType" :fast-edition-mode="project.fast_edition_mode" :can-edit-feature="canEditFeature" @tofeature="pushNgo" /> </div> <div class="eight wide column"> <div id="map" ref="map" /> <div id="popup" class="ol-popup" > <a id="popup-closer" href="#" class="ol-popup-closer" /> <div id="popup-content" /> </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="[project.fast_edition_mode && hasUnsavedChange ? 'sign-out' : 'random', 'icon']" aria-hidden="true" /> Abandonner {{ project.fast_edition_mode && hasUnsavedChange ? 'les modifications' : 'la vue signalement filtré' }} </div> <div class="content"> {{ project.fast_edition_mode && hasUnsavedChange ? 'Les modifications apportées au signalement ne seront pas sauvegardées, continuer ?': `Vous allez quittez la vue signalement filtré, l\'ordre des signalements pourrait changer après édition d\'un signalement.` }} </div> <div class="actions"> <button type="button" class="ui green compact button" @click="stayOnPage" > Annuler </button> <button type="button" class="ui red compact button" @click="leavePage" > Continuer </button> </div> </div> </div> </div> <div v-else> Pas de signalement correspondant trouvé </div> </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 { buffer } from 'ol/extent'; export default { name: 'FeatureDetail', components: { FeatureHeader, FeatureTable, FeatureAttachements, FeatureComments }, beforeRouteEnter (to, from, next) { // if the user edited the feature, coming from filtered features details browsing, // display a button to turn back to the list view, in order to start again from the list // because order changes after edition, depending the sort criteria // in beforeRouteEnter, the component is not mounted and this doesn't exist yet, thus we store the value in window object window.displayToListButton = false; // reinitialisation of the value if (from.query.offset !== undefined) { // if a queryset for filtered features is stored in the route from window.displayToListButton = true; // toggle the value to display the button } next(); // continue page loading }, beforeRouteUpdate (to, from, next) { if (this.hasUnsavedChange) { 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 || (from.query.offset >= 0 && to.name === 'editer-signalement')) { 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: [], featureType: {}, featuresCount: null, isDeleting: false, isLeaving: false, slugSignal: '', displayToListButton: false, }; }, computed: { ...mapState([ 'USER_LEVEL_PROJECTS', 'user' ]), ...mapState('projects', [ 'project' ]), ...mapState('feature-type', [ 'feature_types', 'feature_type', ]), ...mapState('feature', [ 'currentFeature', 'form', ]), ...mapGetters([ 'permissions', ]), hasUnsavedChange() { if (this.project.fast_edition_mode && this.form) { if (this.form.title !== this.currentFeature.title) return true; if (this.form.description.value !== this.currentFeature.description) return true; if (this.form.status.value !== this.currentFeature.status) return true; for (const xForm of this.$store.state.feature.extra_forms) { const originalField = this.currentFeature.feature_data.find(el => el.label === xForm.label); if (originalField && xForm.value !== originalField.value) return true; } } return false; }, isFeatureCreator() { if (this.currentFeature && this.user) { return this.currentFeature.creator === this.user.id; } return false; }, isModerator() { return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Modérateur'; }, isAdministrator() { return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Administrateur projet'; }, canEditFeature() { return (this.permissions && this.permissions.can_update_feature) || this.isFeatureCreator || this.isModerator || this.user.is_superuser; }, canDeleteFeature() { return (this.permissions && this.permissions.can_delete_feature && this.isFeatureCreator) || this.isFeatureCreator || this.isModerator || this.isAdministrator || this.user.is_superuser; } }, 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(); // when this is available, set the value with previously stored value in windows to pass it as a prop this.displayToListButton = window.displayToListButton; }, beforeDestroy() { this.$store.commit('CLEAR_MESSAGES'); }, 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(); 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; this.featureType = this.feature_type; } //* 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.feature_id !== this.slugSignal) { promises.push( this.GET_PROJECT_FEATURE({ project_slug: this.$route.params.slug, feature_id: this.slugSignal, }) ); } await axios.all(promises); this.getFeatureEvents(); this.getFeatureAttachments(); this.getLinkedFeatures(); this.DISCARD_LOADER(); 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.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.$store .dispatch('feature/DELETE_FEATURE', { feature_id: this.currentFeature.feature_id }) .then(async (response) => { if (response.status === 204) { await this.GET_PROJECT_FEATURES({ project_slug: this.$route.params.slug }); this.goBackToProject(); } }); }, fetchFilteredFeature() { 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}`; return featureAPI.getPaginatedFeatures(url) .then((data) => { if (data && data.results && data.results[0]) { this.featuresCount = data.count; this.previous = data.previous; this.next = data.next; this.SET_CURRENT_FEATURE(data.results[0]); const { feature_id, feature_type } = data.results[0]; this.slugSignal = feature_id; this.featureType = feature_type; this.SET_CURRENT_FEATURE_TYPE_SLUG(feature_type.slug); return { feature_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: false, dragPan: false } }); // 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; var baseMaps = this.$store.state.map.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 url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/` + `?feature_id=${this.slugSignal}&output=geojson`; axios .get(url) .then((response) => { if (response.data.features.length > 0) { const featureGroup = mapService.addFeatures( response.data.features, {}, this.feature_types, true ); mapService.fitExtent(buffer(featureGroup.getExtent(),200)); } }) .catch((error) => { throw error; }); }, 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) ); }, }, }; </script> <style scoped> #map { width: 100%; height: 100%; min-height: 250px; max-height: 70vh; } .prewrap { white-space: pre-wrap; } </style>