Skip to content
Snippets Groups Projects
FeatureDetail.vue 18.3 KiB
Newer Older
Florent Lavelle's avatar
Florent Lavelle committed
      v-if="currentFeature"
Florent Lavelle's avatar
Florent Lavelle committed
      class="ui grid stackable"
Florent Lavelle's avatar
Florent Lavelle committed
        <div class="sixteen wide column">
            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"
Florent Lavelle's avatar
Florent Lavelle committed
        <div class="eight wide column">
          <FeatureTable
            v-if="project"
            ref="featureTable"
            :feature-type="feature_type"
            :fast-edition-mode="project.fast_edition_mode"
Florent Lavelle's avatar
Florent Lavelle committed
          />
        <div
          v-if="feature_type && feature_type.geom_type !== 'none'"
          class="eight wide column"
        >
          <div class="map-container">
              id="map"
              ref="map"
            >
              <SidebarLayers
                v-if="basemaps && map"
                ref="sidebar"
              />
              <Geolocation />
            <div
              id="popup"
              class="ol-popup"
            >
              <a
                id="popup-closer"
                href="#"
                class="ol-popup-closer"
              />
              <div
                id="popup-content"
              />
            </div>
DESPRES Damien's avatar
DESPRES Damien committed
          </div>
Florent Lavelle's avatar
Florent Lavelle committed
        <div class="eight wide column">
          <FeatureAttachements
            :attachments="attachments"
          />
Florent Lavelle's avatar
Florent Lavelle committed
        <div class="eight wide column">
          <FeatureComments
            :events="events"
            :enable-key-doc-notif="feature_type.enable_key_doc_notif"
Florent Lavelle's avatar
Florent Lavelle committed
          />
        v-if="isDeleting"
        class="ui dimmer modals visible active"
            'ui mini modal',
            { 'active visible': isDeleting },
Florent Lavelle's avatar
Florent Lavelle committed
          <i
            class="close icon"
Florent Lavelle's avatar
Florent Lavelle committed
            aria-hidden="true"
            @click="isDeleting = false"
          <div
            v-if="isDeleting"
            class="ui icon header"
          >
Florent Lavelle's avatar
Florent Lavelle committed
            <i
              class="trash alternate icon"
              aria-hidden="true"
            />
Timothee P's avatar
Timothee P committed
            <button
              type="button"
              class="ui red compact fluid button"
              @click="deleteFeature"
Timothee P's avatar
Timothee P committed
              Confirmer la suppression
            </button>

      <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
              aria-hidden="true"
            />
          </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"
            >
              Annuler
            </button>
            <button
              type="button"
              class="ui red compact button"
              @click="leavePage"
            >
              Continuer
            </button>
          </div>
        </div>
      </div>
Florent Lavelle's avatar
Florent Lavelle committed
    <div v-else>
      Pas de signalement correspondant trouvé
    </div>
import { mapState, mapActions, mapMutations, mapGetters } from 'vuex';
DESPRES Damien's avatar
DESPRES Damien committed
import mapService from '@/services/map-service';
Florent Lavelle's avatar
Florent Lavelle committed

import axios from '@/axios-client.js';
Florent Lavelle's avatar
Florent Lavelle committed
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 Geolocation from '@/components/Map/Geolocation';
DESPRES Damien's avatar
DESPRES Damien committed
import { buffer } from 'ol/extent';
  name: 'FeatureDetail',
Florent Lavelle's avatar
Florent Lavelle committed
  components: {
    FeatureHeader,
    FeatureTable,
    FeatureAttachements,
    FeatureComments,
    SidebarLayers,
    Geolocation,
Timothee P's avatar
Timothee P committed
  },

  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
    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
      attachments: [],
          title: '',
          file: null,
          id_for_label: 'add-comment',
          html_name: 'add-comment',
          errors: '',
      isDeleting: false,
      isLeaving: false,
      isSavingChanges: false,
Timothee P's avatar
Timothee P committed
      slug: this.$route.params.slug,
      'user'
Timothee P's avatar
Timothee P committed
    ...mapState('projects', [
      'project'
    ]),
Florent Lavelle's avatar
Florent Lavelle committed
    ...mapState('feature-type', [
    ]),
    ...mapState('feature', [
      'currentFeature',
    ...mapGetters('feature-type', [
      'feature_type',
    ]),
    ...mapState('map', [
      'basemaps',
    ]),
    /**
     * Checks if there are any unsaved changes in the form compared to the current feature's properties.
     * This function is useful for prompting the user before they navigate away from a page with unsaved changes.
     * 
     * @returns {boolean} - Returns true if there are unsaved changes; otherwise, returns false.
     */
    hasUnsavedChange() {
      // Ensure we are in edition mode and all required objects are present.
      if (this.project && this.project.fast_edition_mode &&
        this.form && this.currentFeature && this.currentFeature.properties) {
        // Check for changes in title, description, and status.
        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;
        if (this.form.assigned_member.value !== this.currentFeature.properties.assigned_member) return true;

        // Iterate over extra forms to check for any changes.
        for (const xForm of this.$store.state.feature.extra_forms) {
          const originalValue = this.currentFeature.properties[xForm.name];
          // Check if the form value has changed, considering edge cases for undefined, null, or empty values.
            !isEqual(xForm.value, originalValue) && // Check if values have changed.
            !(!xForm.value && !originalValue) // Ensure both aren't undefined/null/empty, treating null as equivalent to false for unactivated conditionals or unset booleans.
            // Log the difference for debugging purposes.
            console.log(`In custom form [${xForm.name}], the current form value [${xForm.value}] differs from original value [${originalValue}]`);
      // If none of the above conditions are met, return false indicating no unsaved changes.
      if (this.currentFeature && this.currentFeature.properties && this.user) {
        return this.currentFeature.properties.creator === this.user.id ||
          this.currentFeature.properties.creator.username === this.user.username;
Timothee P's avatar
Timothee P committed
      return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.slug] === 'Modérateur';
    isAdministrator() {
Timothee P's avatar
Timothee P committed
      return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.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;
Timothee P's avatar
Timothee P committed

    /** 
     * To navigate back or forward to the previous or next URL, the query params in url are updated
     * since the route doesn't change, mounted is not called, then the page isn't updated
     * To reload page infos we need to call initPage() when query changes
    */ 
    '$route.query'(newValue, oldValue) {
      if (newValue !== oldValue) {
        this.initPage();
Timothee P's avatar
Timothee P committed
  mounted() {
Timothee P's avatar
Timothee P committed
  },

  beforeDestroy() {
    this.$store.commit('CLEAR_MESSAGES');
    this.$store.dispatch('CANCEL_CURRENT_SEARCH_REQUEST');
Timothee P's avatar
Timothee P committed
  },

Florent Lavelle's avatar
Florent Lavelle committed
    ...mapMutations([
      'DISPLAY_LOADER',
      'DISCARD_LOADER'
    ]),
    ...mapMutations('feature', [
      'SET_CURRENT_FEATURE'
    ]),
Florent Lavelle's avatar
Florent Lavelle committed
    ...mapMutations('feature-type', [
      'SET_CURRENT_FEATURE_TYPE_SLUG'
    ]),
    ...mapActions('projects', [
      'GET_PROJECT',
      'GET_PROJECT_INFO'
    ]),
    ...mapActions('feature', [
Florent Lavelle's avatar
Florent Lavelle committed
      'GET_PROJECT_FEATURE',
      'GET_PROJECT_FEATURES'
    ]),
    async initPage() {
      await this.getPageInfo();
      if(this.feature_type && this.feature_type.geom_type === 'none') {
        // setting map to null to ensure map would be created when navigating next to a geographical feature
        this.map = null;
      } else if (this.currentFeature) {
        this.initMap();
      } 
      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 = [];
      //* Récupération du projet, en cas d'arrivée directe sur la page ou de refresh
        promises.push(this.GET_PROJECT(this.slug));
      }
      //* Récupération des types de signalement, en cas de redirection page détails signalement avec id (projet déjà récupéré) ou cas précédent
      if (!this.featureType || !this.basemaps) {
Timothee P's avatar
Timothee P committed
          this.GET_PROJECT_INFO(this.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) {
Timothee P's avatar
Timothee P committed
            project_slug: this.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() {
      //* update the params or queries in the route/url
      this.$router.push(newEntry)
        //* catch error if navigation get aborted (in beforeRouteUpdate)
        .catch(() => true);
leandro's avatar
leandro committed
    goBackToProject(message) {
      this.$router.push({
        name: 'project_detail',
leandro's avatar
leandro committed
        params: {
Timothee P's avatar
Timothee P committed
          slug: this.slug,
leandro's avatar
leandro committed
          message,
        },
      });
    },
      this.isDeleting = false;
      this.DISPLAY_LOADER('Suppression du signalement en cours...');
        .dispatch('feature/DELETE_FEATURE', { feature_id: this.currentFeature.id })
        .then(async (response) => {
            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 });
Timothee P's avatar
Timothee P committed
      const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.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 };
    initMap() {
      var mapDefaultViewCenter =
        this.$store.state.configuration.DEFAULT_MAP_VIEW.center;
      var mapDefaultViewZoom =
        this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom;
      if (this.map) {
        mapService.removeFeatures();
      } else {
        this.map = mapService.createMap(this.$refs.map, {
          mapDefaultViewCenter,
          mapDefaultViewZoom,
          maxZoom: this.project.map_max_zoom_level,
          interactions : {
            doubleClickZoom :false,
            mouseWheelZoom: true,
            dragPan: true
          },
          fullScreenControl: true,
          geolocationControl: true,
      this.addFeatureToMap();
    },

    addFeatureToMap() {
      const featureGroup = mapService.addFeatures({
Timothee P's avatar
Timothee P committed
        project_slug: this.slug,
        features: [this.currentFeature],
        featureTypes: this.feature_types,
        addToMap: true,
      });
      mapService.fitExtent(buffer(featureGroup.getExtent(),200));
    getFeatureEvents() {
      featureAPI
        .then((data) => (this.events = data));
    },

    getFeatureAttachments() {
      featureAPI
        .then((data) => (this.attachments = data));
    },

    getLinkedFeatures() {
      featureAPI
          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') {
            // when query doesn't change we need to reload the page infos with initPage(),
            // since it would not be called from the watcher'$route.query' when the query does change
            this.initPage();
Timothee P's avatar
Timothee P committed
<style scoped>
.map-container {
  height: 100%;
  position: relative;
  overflow: hidden;
  background-color: #fff;
}
  border: 1px solid grey;
div.geolocation-container {
  /* each button have (more or less depends on borders) .5em space between */
  /* zoom buttons are 60px high, geolocation and full screen button is 34px high with borders */
  top: calc(1.3em + 60px + 34px);
}


.ui.active.dimmer {
  position: fixed;
}
.ui.modal > .content {
  text-align: center;
}

.ui.modal > .actions {
  display: flex;
  justify-content: space-evenly;
}