Newer
Older
Sébastien DA ROCHA
committed
<template>

Timothee P
committed
<div id="feature-detail">
<div class="row">

Timothee P
committed
:features-count="featuresCount"
:slug-signal="slugSignal"
:feature-type="feature_type"
:fast-edition-mode="project.fast_edition_mode"

Timothee P
committed
:is-feature-creator="isFeatureCreator"
:can-edit-feature="canEditFeature"
@fastEditFeature="validateFastEdition"
@setIsDeleting="isDeleting = true"

Timothee P
committed
@tofeature="pushNgo"
</div>
Sébastien DA ROCHA
committed
</div>
<div class="row">
:feature-type="feature_type"
:fast-edition-mode="project.fast_edition_mode"

Timothee P
committed
:can-edit-feature="canEditFeature"
</div>
<div
v-if="feature_type && feature_type.geom_type !== 'none'"
class="eight wide column"
>
>
<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>
Sébastien DA ROCHA
committed
</div>
<div class="row">
<div class="eight wide column">
<FeatureAttachements
:attachments="attachments"
/>
Sébastien DA ROCHA
committed
</div>
<div class="eight wide column">
<FeatureComments
:events="events"
:enable-key-doc-notif="feature_type.enable_key_doc_notif"
@fetchEvents="getFeatureEvents"
Sébastien DA ROCHA
committed
</div>
</div>
Sébastien DA ROCHA
committed
<div

Timothee P
committed
class="ui dimmer modals visible active"
Sébastien DA ROCHA
committed
>
<div
:class="[
'ui mini modal',
{ 'active visible': isDeleting },
]"
>
<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"
Sébastien DA ROCHA
committed
>
</div>
Sébastien DA ROCHA
committed
</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

Timothee P
committed
class="sign-out icon"

Timothee P
committed
Abandonner les modifications
</div>
<div class="content">

Timothee P
committed
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"
>

Timothee P
committed
<i
class="close icon"
aria-hidden="true"
/>
Annuler
</button>
<button
type="button"
class="ui red compact button"
@click="leavePage"
>
Continuer

Timothee P
committed
<i
class="arrow right icon"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
Sébastien DA ROCHA
committed
</div>
Pas de signalement correspondant trouvé
</div>
Sébastien DA ROCHA
committed
</div>
</template>
<script>

Timothee P
committed
import { isEqual } from 'lodash';

Timothee P
committed
import { mapState, mapActions, mapMutations, mapGetters } from 'vuex';
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';
Sébastien DA ROCHA
committed
export default {
Sébastien DA ROCHA
committed
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

Timothee P
committed
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

Timothee P
committed
}
},
Sébastien DA ROCHA
committed
data() {
return {
Sébastien DA ROCHA
committed
comment_form: {
attachment_file: {
errors: null,
Sébastien DA ROCHA
committed
},
comment: {
id_for_label: 'add-comment',
html_name: 'add-comment',
errors: '',
Sébastien DA ROCHA
committed
value: null,
},
},
events: [],

Timothee P
committed
featuresCount: null,
isDeleting: false,
isLeaving: false,
isSavingChanges: false,

Timothee P
committed
slugSignal: '',
Sébastien DA ROCHA
committed
};
},
computed: {

Timothee P
committed
...mapState([
'USER_LEVEL_PROJECTS',

Timothee P
committed
]),
'feature_types',

Timothee P
committed
...mapGetters('feature-type', [
'feature_type',
]),

Timothee P
committed
...mapGetters([
'permissions',
]),
...mapState('map', [
'basemaps',
]),

Timothee P
committed
/**
* 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.
*/

Timothee P
committed
// 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;

Timothee P
committed
if (this.form.assigned_member.value !== this.currentFeature.properties.assigned_member) return true;

Timothee P
committed
// 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];

Timothee P
committed
// Check if the form value has changed, considering edge cases for undefined, null, or empty values.

Timothee P
committed
if (

Timothee P
committed
!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.

Timothee P
committed
) {

Timothee P
committed
// Log the difference for debugging purposes.
console.log(`In custom form [${xForm.name}], the current form value [${xForm.value}] differs from original value [${originalValue}]`);

Timothee P
committed
// If none of the above conditions are met, return false indicating no unsaved changes.

Timothee P
committed
},
isFeatureCreator() {
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
committed
}
return false;
},
isModerator() {
return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.slug] === 'Modérateur';

Timothee P
committed
},
return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.slug] === 'Administrateur projet';

Timothee P
committed
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;
},
Sébastien DA ROCHA
committed
},
watch: {
/**
* 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
committed
this.initPage();
},
beforeDestroy() {
this.$store.commit('CLEAR_MESSAGES');
this.$store.dispatch('CANCEL_CURRENT_SEARCH_REQUEST');
Sébastien DA ROCHA
committed
methods: {
...mapMutations([
'DISPLAY_LOADER',
'DISCARD_LOADER'
]),

Timothee P
committed
...mapMutations('feature', [
'SET_CURRENT_FEATURE'
]),
...mapMutations('feature-type', [
'SET_CURRENT_FEATURE_TYPE_SLUG'
]),
...mapActions('projects', [
'GET_PROJECT',
'GET_PROJECT_INFO'
]),

Timothee P
committed

Timothee P
committed
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();
}

Timothee P
committed
},
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);

Timothee P
committed
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

Timothee P
committed
if (!this.project) {

Timothee P
committed
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
committed
promises.push(

Timothee P
committed
);
}
//* changement de requête selon s'il y a un id ou un offset(dans le cas du parcours des signalements filtrés)

Timothee P
committed
if (this.$route.query.offset >= 0) {
promises.push(this.fetchFilteredFeature());
} else if (!this.currentFeature || this.currentFeature.id !== this.slugSignal) {

Timothee P
committed
promises.push(
this.GET_PROJECT_FEATURE({

Timothee P
committed
feature_id: this.slugSignal,
})
);
}
await axios.all(promises);
this.DISCARD_LOADER();

Timothee P
committed
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');
}
}

Timothee P
committed
},
confirmLeave(next) {
this.next = next;
this.isLeaving = true;
},
stayOnPage() {
this.isLeaving = false;
},
leavePage() {

Timothee P
committed
this.isLeaving = false;

Timothee P
committed
},
//* update the params or queries in the route/url
this.$router.push(newEntry)
//* catch error if navigation get aborted (in beforeRouteUpdate)
.catch(() => true);
Sébastien DA ROCHA
committed
deleteFeature() {

Timothee P
committed
this.isDeleting = false;
this.DISPLAY_LOADER('Suppression du signalement en cours...');
this.$store
.dispatch('feature/DELETE_FEATURE', { feature_id: this.currentFeature.id })

Timothee P
committed
this.DISCARD_LOADER();
if (response.status === 200) {
this.goBackToProject({ comment: 'Le signalement a bien été supprimé', level: 'positive' });

Timothee P
committed
} else {
this.$store.commit('DISPLAY_MESSAGE', { comment: 'Une erreur est survenue pendant la suppression du signalement', level: 'negative' });
}
});
Sébastien DA ROCHA
committed
},
fetchFilteredFeature() { // TODO : if no query for sort, use project default ones

Timothee P
committed
const queryString = new URLSearchParams({ ...this.$route.query });
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.slug}/feature-paginated/?limit=1&${queryString}&output=geojson`;

Timothee P
committed
return featureAPI.getPaginatedFeatures(url)
.then((data) => {
if (data && data.results && data.results.features && data.results.features[0]) {

Timothee P
committed
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 };

Timothee P
committed
}
return;
});
},
Sébastien DA ROCHA
committed
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,
Sébastien DA ROCHA
committed
}
this.addFeatureToMap();
},
addFeatureToMap() {
const featureGroup = mapService.addFeatures({
features: [this.currentFeature],
featureTypes: this.feature_types,
addToMap: true,
});
mapService.fitExtent(buffer(featureGroup.getExtent(),200));
getFeatureEvents() {
featureAPI

Timothee P
committed
.getFeatureEvents(this.slugSignal)
.then((data) => (this.events = data));
},
getFeatureAttachments() {
featureAPI

Timothee P
committed
.getFeatureAttachments(this.slugSignal)
.then((data) => (this.attachments = data));
},
getLinkedFeatures() {
featureAPI

Timothee P
committed
.getFeatureLinks(this.slugSignal)
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();
Sébastien DA ROCHA
committed
},
};
</script>
.map-container {
height: 100%;
position: relative;
overflow: hidden;
background-color: #fff;
}
Sébastien DA ROCHA
committed
#map {
width: 100%;
height: 100%;
min-height: 250px;
Sébastien DA ROCHA
committed
}
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);
}

Timothee P
committed
.prewrap {
white-space: pre-wrap;
}
.ui.active.dimmer {
position: fixed;
}

Timothee P
committed
.ui.modal > .content {
text-align: center;
}
.ui.modal > .actions {
display: flex;
justify-content: space-evenly;
}