Something went wrong on our end
-
Timothee P authoredTimothee P authored
Feature_list.vue 23.45 KiB
<template>
<div class="fourteen wide column">
<div
id="feature-list-container"
class="ui grid mobile-column"
>
<div class="four wide column mobile-fullwidth">
<h1>Signalements</h1>
</div>
<div class="twelve-wide column no-padding-mobile mobile-fullwidth">
<div class="ui large text loader">
Chargement
</div>
<div class="ui secondary menu no-margin">
<a
:class="['item no-margin', { active: showMap }]"
data-tab="map"
data-tooltip="Carte"
@click="showMap = true"
><i class="map fitted icon" /></a>
<a
:class="['item no-margin', { active: !showMap }]"
data-tab="list"
data-tooltip="Liste"
@click="showMap = false"
><i class="list fitted icon" /></a>
<div class="item">
<h4>
{{ featuresCount }} signalement{{ featuresCount > 1 ? "s" : "" }}
</h4>
</div>
<div
v-if="
project &&
feature_types.length > 0 &&
permissions.can_create_feature
"
id="button-dropdown"
class="item right"
>
<div
class="ui dropdown button compact button-hover-green"
data-tooltip="Ajouter un signalement"
data-position="bottom right"
@click="toggleAddFeature"
>
<i class="plus fitted icon" />
<div
v-if="showAddFeature"
class="menu left transition visible"
style="z-index: 9999"
>
<div class="header">
Ajouter un signalement du type
</div>
<div class="scrolling menu text-wrap">
<router-link
v-for="(type, index) in feature_types"
:key="type.slug + index"
:to="{
name: 'ajouter-signalement',
params: { slug_type_signal: type.slug },
}"
class="item"
>
{{ type.title }}
</router-link>
</div>
</div>
</div>
<div
v-if="checkedFeatures.length > 0 && mode === 'modify'"
class="ui dropdown button compact button-hover-green margin-left-25"
data-tooltip="Modifier le statut des Signalements"
data-position="bottom right"
@click="toggleModifyStatus"
>
<i class="pencil fitted icon" />
<div
v-if="showModifyStatus"
class="menu left transition visible"
style="z-index: 9999"
>
<div class="header">
Modifier le statut des Signalements
</div>
<div class="scrolling menu text-wrap">
<span
v-for="status in availableStatus"
:key="status.value"
class="item"
@click="modifyStatus(status.value)"
>
{{ status.name }}
</span>
</div>
</div>
</div>
<div
v-if="checkedFeatures.length > 0 && mode === 'delete'"
class="ui button compact button-hover-red margin-left-25"
data-tooltip="Effacer tous les types de signalements sélectionnés"
data-position="bottom right"
@click="modalAllDelete"
>
<i class="grey trash fitted icon" />
</div>
</div>
</div>
</div>
</div>
<section
id="form-filters"
class="ui form grid"
>
<div class="field wide four column no-margin-mobile">
<label>Type</label>
<Dropdown
:options="featureTypeChoices"
:selected="form.type.selected"
:selection.sync="form.type.selected"
:search="true"
:clearable="true"
/>
</div>
<div class="field wide four column no-padding-mobile no-margin-mobile">
<label>Statut</label>
<!-- //* giving an object mapped on key name -->
<Dropdown
:options="statusChoices"
:selected="form.status.selected.name"
:selection.sync="form.status.selected"
:search="true"
:clearable="true"
/>
</div>
<div class="field wide four column">
<label>Nom</label>
<div class="ui icon input">
<i class="search icon" />
<div class="ui action input">
<input
v-model="form.title"
type="text"
name="title"
@keyup.enter="resetPaginationNfetchFeatures"
>
<button
id="submit-search"
class="ui teal icon button"
@click="resetPaginationNfetchFeatures"
>
<i class="search icon" />
</button>
</div>
</div>
</div>
<!-- map params, updated on map move -->
<input
v-model="zoom"
type="hidden"
name="zoom"
>
<input
v-model="lat"
type="hidden"
name="lat"
>
<input
v-model="lng"
type="hidden"
name="lng"
>
</section>
<div
v-show="showMap"
class="ui tab active map-container"
data-tab="map"
>
<div
id="map"
ref="map"
/>
<SidebarLayers v-if="basemaps && map" />
</div>
<FeatureListTable
v-show="!showMap"
:paginated-features="paginatedFeatures"
:checked-features.sync="checkedFeatures"
:features-count="featuresCount"
:clicked-features.sync="clickedFeatures"
:mode.sync="mode"
:pagination="pagination"
:sort="sort"
@update:page="handlePageChange"
@update:sort="handleSortChange"
/>
<!-- MODAL ALL DELETE FEATURE TYPE -->
<div
v-if="modalAllDeleteOpen"
class="ui dimmer modals page transition visible active"
style="display: flex !important"
>
<div
:class="[
'ui mini modal subscription',
{ 'active visible': modalAllDeleteOpen },
]"
>
<i
class="close icon"
@click="modalAllDeleteOpen = false"
/>
<div class="ui icon header">
<i class="trash alternate icon" />
Êtes-vous sûr de vouloir effacer
<span v-if="checkedFeatures.length === 1"> un signalement ? </span>
<span v-else> ces {{ checkedFeatures.length }} signalements ? </span>
</div>
<div class="actions">
<button
type="button"
class="ui red compact fluid button"
@click="deleteAllFeatureSelection"
>
Confirmer la suppression
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
import { mapUtil } from '@/assets/js/map-util.js';
import featureAPI from '@/services/feature-api';
import SidebarLayers from '@/components/map-layers/SidebarLayers';
import FeatureListTable from '@/components/feature/FeatureListTable';
import Dropdown from '@/components/Dropdown.vue';
import axios from '@/axios-client.js';
export default {
name: 'FeatureList',
components: {
SidebarLayers,
Dropdown,
FeatureListTable,
},
data() {
return {
form: {
type: {
selected: '',
},
status: {
selected: '',
choices: [
{
name: 'Brouillon',
value: 'draft',
},
{
name: 'En attente de publication',
value: 'pending',
},
{
name: 'Publié',
value: 'published',
},
{
name: 'Archivé',
value: 'archived',
},
],
},
title: null,
},
baseUrl: this.$store.state.configuration.BASE_URL,
clickedFeatures: [],
modalAllDeleteOpen: false,
mode: 'modify',
map: null,
zoom: null,
lat: null,
lng: null,
featuresCount: 0,
paginatedFeatures: [],
currentLayer: null,
pagination: {
currentPage: 1,
pagesize: 15,
start: 0,
end: 15,
},
previous: null,
next: null,
sort: {
column: '',
ascending: true,
},
showMap: true,
showAddFeature: false,
showModifyStatus: false,
};
},
computed: {
...mapState(['user', 'USER_LEVEL_PROJECTS']),
...mapGetters([
'permissions',
]),
...mapState('projects', [
'project',
]),
...mapState('feature', [
'checkedFeatures'
]),
...mapState('feature_type', [
'feature_types'
]),
...mapState('map', [
'basemaps'
]),
API_BASE_URL() {
return this.$store.state.configuration.VUE_APP_DJANGO_API_BASE;
},
statusChoices() {
//* if project is not moderate, remove pending status
return this.form.status.choices.filter((el) =>
this.project && this.project.moderation ? true : el.value !== 'pending'
);
},
availableStatus() {
if (this.project) {
const isModerate = this.project.moderation;
const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug];
const isOwnFeature = this.feature
? this.feature.creator === this.user.id //* prevent undefined feature
: false; //* si le contributeur est l'auteur du signalement
if (
//* si admin, modérateur ou super contributeur, statuts toujours disponible: Brouillon, Publié, Archivé
userStatus === 'Administrateur projet' ||
(userStatus === 'Super Contributeur' && !isModerate)
) {
return this.statusChoices.filter((el) => el.value !== 'pending');
} else if (userStatus === 'Super Contributeur' && isModerate) {
return this.statusChoices.filter(
(el) => el.value === 'draft' || el.value === 'pending'
);
} else if (userStatus === 'Modérateur') {
return this.statusChoices.filter(
(el) => el.value === 'draft' || el.value === 'published'
);
} else if (userStatus === 'Contributeur') {
//* cas particuliers du contributeur
if (
this.currentRouteName === 'ajouter-signalement' ||
!isOwnFeature
) {
//* même cas à l'ajout d'une feature ou si feature n'a pas été créé par le contributeur
return isModerate
? this.statusChoices.filter(
(el) => el.value === 'draft' || el.value === 'pending'
)
: this.statusChoices.filter(
(el) => el.value === 'draft' || el.value === 'published'
);
} else {
//* à l'édition d'une feature et si le contributeur est l'auteur de la feature
return isModerate
? this.statusChoices.filter(
(el) => el.value !== 'published' //* toutes sauf "Publié"
)
: this.statusChoices.filter(
(el) => el.value !== 'pending' //* toutes sauf "En cours de publication"
);
}
}
}
return [];
},
featureTypeChoices() {
return this.feature_types.map((el) => el.title);
},
},
watch: {
'form.type.selected'() {
this.resetPaginationNfetchFeatures();
},
'form.status.selected.value'() {
this.resetPaginationNfetchFeatures();
},
map(newValue) {
if (newValue && this.paginatedFeatures && this.paginatedFeatures.length) {
if (this.currentLayer) {
this.map.removeLayer(this.currentLayer);
}
this.currentLayer = mapUtil.addFeatures(
this.paginatedFeatures,
{},
true,
this.feature_types
);
}
},
paginatedFeatures: {
deep: true,
handler(newValue, oldValue) {
if (newValue && newValue.length && newValue !== oldValue && this.map) {
if (this.currentLayer) {
this.map.removeLayer(this.currentLayer);
this.currentLayer = null;
}
this.currentLayer = mapUtil.addFeatures(
newValue,
{},
true,
this.feature_types
);
} else if (newValue && newValue.length === 0) {
if (this.currentLayer) {
this.map.removeLayer(this.currentLayer);
this.currentLayer = null;
}
}
}
}
},
mounted() {
if (!this.project) {
// Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh
this.$store.dispatch('projects/GET_PROJECT', this.$route.params.slug);
this.$store
.dispatch('projects/GET_PROJECT_INFO', this.$route.params.slug)
.then(() => this.initMap());
} else {
this.initMap();
}
this.fetchPagedFeatures();
window.addEventListener('mousedown', this.clickOutsideDropdown);
},
destroyed() {
window.removeEventListener('mousedown', this.clickOutsideDropdown);
//* allow user to change page if ever stuck on loader
this.$store.commit('DISCARD_LOADER');
},
methods: {
...mapActions('feature', [
'GET_PROJECT_FEATURES',
'SEND_FEATURE'
]),
toggleAddFeature() {
this.showAddFeature = !this.showAddFeature;
this.showModifyStatus = false;
},
toggleModifyStatus() {
this.showModifyStatus = !this.showModifyStatus;
this.showAddFeature = false;
},
modalAllDelete() {
this.modalAllDeleteOpen = !this.modalAllDeleteOpen;
},
clickOutsideDropdown(e) {
if (!e.target.closest('#button-dropdown')) {
this.showModifyStatus = false;
setTimeout(() => { //* timout necessary to give time to click on link to add feature
this.showAddFeature = false;
}, 500);
}
},
async modifyStatus(newStatus) {
let errorCount = 0;
const promises = this.checkedFeatures.map((feature_id) => {
let feature = this.clickedFeatures.find((el) => el.feature_id === feature_id);
if (feature) {
return featureAPI.updateFeature({
feature_id,
feature_type__slug: feature.feature_type,
project__slug: this.$route.params.slug, newStatus
});
} else {
errorCount += 1;
}
});
const promisesResult = await Promise.all(promises);
promisesResult.forEach((response) => {
if (response && response.data && response.status === 200) {
this.checkedFeatures.splice(this.checkedFeatures.indexOf(response.data.id), 2);
} else {
errorCount += 1;
}
});
let message = {
comment: 'Tous les signalements ont été modifié avec succès.',
level: 'positive'
};
if (errorCount) {
//* display error message
if(errorCount === 1) {
message.comment = "Un signalement n'a pas pu être modifié. (Il reste sélectionné)";
} else {
message.comment = `${errorCount} signalements n'ont pas pu être modifiés. (Ils restent sélectionnés)`;
}
message.level = 'negative';
}
this.fetchPagedFeatures();
this.$store.commit('DISPLAY_MESSAGE', message);
},
deleteFeature(feature_id) {
const url = `${this.API_BASE_URL}features/${feature_id}/?project__slug=${this.project.slug}`;
axios //TODO: REFACTO -> Delete function already exist in store
.delete(url, {})
.then(() => {
if (!this.modalAllDeleteOpen) {
this.GET_PROJECT_FEATURES({
project_slug: this.$route.params.slug,
})
.then(() => {
this.fetchPagedFeatures();
this.checkedFeatures.splice(feature_id);
});
}
})
.catch(() => {
return false;
});
},
deleteAllFeatureSelection() {
let feature = {};
this.checkedFeatures.forEach((feature_id) => {
feature = { feature_id: feature_id }; // ? Is this usefull ?
this.deleteFeature(feature.feature_id); //? since property feature_id is directly used after...
});
this.modalAllDelete();
},
onFilterChange() {
if (mapUtil.getMap()) {
mapUtil.getMap().invalidateSize();
mapUtil.getMap()._onResize(); // force refresh for vector tiles
if (window.layerMVT) {
window.layerMVT.redraw();
}
}
},
initMap() {
this.zoom = this.$route.query.zoom || '';
this.lat = this.$route.query.lat || '';
this.lng = this.$route.query.lng || '';
var mapDefaultViewCenter =
this.$store.state.configuration.DEFAULT_MAP_VIEW.center;
var mapDefaultViewZoom =
this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom;
this.map = mapUtil.createMap(this.$refs.map, {
zoom: this.zoom,
lat: this.lat,
lng: this.lng,
mapDefaultViewCenter,
mapDefaultViewZoom,
});
this.fetchBboxNfit();
document.addEventListener('change-layers-order', (event) => {
// Reverse is done because the first layer in order has to be added in the map in last.
// Slice is done because reverse() changes the original array, so we make a copy first
mapUtil.updateOrder(event.detail.layers.slice().reverse());
});
// --------- End sidebar events ----------
setTimeout(() => {
const project_id = this.$route.params.slug.split('-')[0];
const mvtUrl = `${this.API_BASE_URL}features.mvt/?tile={z}/{x}/{y}&project_id=${project_id}`;
mapUtil.addVectorTileLayer(
mvtUrl,
this.$route.params.slug,
this.feature_types,
this.form
);
mapUtil.addGeocoders(this.$store.state.configuration);
}, 1000);
},
fetchBboxNfit(queryParams) {
featureAPI
.getFeaturesBbox(this.project.slug, queryParams)
.then((bbox) => {
if (bbox) {
mapUtil.getMap().fitBounds(bbox, { padding: [25, 25] });
}
});
},
//* Paginated Features for table *//
getFeatureTypeSlug(title) {
const featureType = this.feature_types.find((el) => el.title === title);
return featureType ? featureType.slug : null;
},
getAvalaibleField(orderField) {
let result = orderField;
if (orderField === 'display_creator') {
result = 'creator';
} else if (orderField === 'display_last_editor') {
result = 'last_editor';
}
return result;
},
buildQueryString() {
let urlParams = '';
let typeFilter = this.getFeatureTypeSlug(this.form.type.selected);
let statusFilter = this.form.status.selected.value;
if (typeFilter) urlParams += `&feature_type_slug=${typeFilter}`;
if (statusFilter) urlParams += `&status__value=${statusFilter}`;
if (this.form.title) urlParams += `&title=${this.form.title}`;
if (this.sort.column) {
urlParams += `&ordering=${
this.sort.ascending ? '-' : ''
}${this.getAvalaibleField(this.sort.column)}`;
}
return urlParams;
},
fetchPagedFeatures(newUrl) {
this.onFilterChange(); //* use paginated event to watch change in filters and modify features on map
let url = `${this.API_BASE_URL}projects/${this.$route.params.slug}/feature-paginated/?output=geojson&limit=${this.pagination.pagesize}&offset=${this.pagination.start}`;
//* if receiving next & previous url
if (newUrl && typeof newUrl === 'string') {
//newUrl = newUrl.replace("8000", "8010"); //* for dev uncomment to use proxy link
url = newUrl;
}
const queryString = this.buildQueryString();
url += queryString;
this.$store.commit(
'DISPLAY_LOADER',
'Récupération des signalements en cours...'
);
featureAPI.getPaginatedFeatures(url)
.then((data) => {
if (data) {
this.featuresCount = data.count;
this.previous = data.previous;
this.next = data.next;
this.paginatedFeatures = data.results.features;
}
//* bbox needs to be updated with the same filters
if (this.paginatedFeatures.length) this.fetchBboxNfit(queryString);
this.$store.commit('DISCARD_LOADER');
});
},
resetPaginationNfetchFeatures() {
this.pagination = {
currentPage: 1,
pagesize: 15,
start: 0,
end: 15,
},
this.fetchPagedFeatures();
},
//* Pagination for table *//
handlePageChange(page) {
if (page === 'next') {
this.toNextPage();
} else if (page === 'previous') {
this.toPreviousPage();
} else if (typeof page === 'number') {
//* update limit and offset
this.toPage(page);
}
},
handleSortChange(sort) {
this.sort = sort;
this.fetchPagedFeatures({
filterType: undefined,
filterValue: undefined,
});
},
toPage(pageNumber) {
const toAddOrRemove =
(pageNumber - this.pagination.currentPage) * this.pagination.pagesize;
this.pagination.start += toAddOrRemove;
this.pagination.end += toAddOrRemove;
this.pagination.currentPage = pageNumber;
this.fetchPagedFeatures();
},
toPreviousPage() {
if (this.pagination.currentPage !== 1) {
if (this.pagination.start > 0) {
this.pagination.start -= this.pagination.pagesize;
this.pagination.end -= this.pagination.pagesize;
this.pagination.currentPage -= 1;
}
this.fetchPagedFeatures(this.previous);
}
},
toNextPage() {
if (this.pagination.currentPage !== this.pageNumbers.length) {
if (this.pagination.end < this.featuresCount) {
this.pagination.start += this.pagination.pagesize;
this.pagination.end += this.pagination.pagesize;
this.pagination.currentPage += 1;
}
this.fetchPagedFeatures(this.next);
}
},
},
};
</script>
<style scoped>
#map {
width: 100%;
min-height: 300px;
height: calc(100vh - 300px);
border: 1px solid grey;
/* To not hide the filters */
z-index: 1;
}
#feature-list-container {
justify-content: flex-start;
}
#feature-list-container .ui.menu:not(.vertical) .right.item {
padding-right: 0;
}
.map-container {
width: 80vw;
transform: translateX(-50%);
margin-left: 50%;
}
.margin-left-25 {
margin-left: 0.25em !important;
}
.no-padding {
padding: 0 !important;
}
.ui.dropdown .menu .left.menu, .ui.dropdown > .left.menu .menu {
margin-right: 0 !important;
}
@media screen and (min-width: 767px) {
.twelve-wide {
width: 75% !important;
}
}
@media screen and (max-width: 767px) {
#feature-list-container > .mobile-fullwidth {
width: 100% !important;
}
.no-margin-mobile {
margin: 0 !important;
}
.no-padding-mobile {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.mobile-column {
flex-direction: column !important;
}
#button-dropdown {
transform: translate(-50px, -60px);
}
#form-filters > .field.column {
width: 100% !important;
}
.map-container {
width: 100%;
}
}
</style>