Something went wrong on our end
-
Timothee P authoredTimothee P authored
Feature_list.vue 21.65 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
@click="showMap = true"
:class="['item no-margin', { active: showMap }]"
data-tab="map"
data-tooltip="Carte"
><i class="map fitted icon"></i
></a>
<a
@click="showTable"
:class="['item no-margin', { active: !showMap }]"
data-tab="list"
data-tooltip="Liste"
><i class="list fitted icon"></i
></a>
<div class="item">
<h4>
<!-- {{ featuresCount }} signalement{{ featuresCount > 1 ? "s" : "" }} -->
{{ filteredFeatures.length }} signalement{{
filteredFeatures.length > 1 ? "s" : ""
}}
</h4>
</div>
<div
v-if="
project &&
feature_types.length > 0 &&
permissions.can_create_feature
"
class="item right"
>
<div
@click="showAddFeature = !showAddFeature"
class="ui dropdown button compact button-hover-green"
data-tooltip="Ajouter un signalement"
data-position="bottom left"
>
<i class="plus fitted icon"></i>
<div
v-if="showAddFeature"
class="menu transition visible"
style="z-index: 9999"
>
<div class="header">Ajouter un signalement du type</div>
<div class="scrolling menu text-wrap">
<router-link
:to="{
name: 'ajouter-signalement',
params: { slug_type_signal: type.slug },
}"
v-for="(type, index) in feature_types"
:key="type.slug + index"
class="item"
>
{{ type.title }}
</router-link>
</div>
</div>
</div>
<div
v-if="checkedFeatures.length"
@click="modalAllDelete"
class="ui button compact button-hover-red margin-left-25"
data-tooltip="Effacer tous les types de signalements sélectionnés"
data-position="left center"
data-variation="mini"
>
<i class="grey trash fitted icon"></i>
</div>
</div>
</div>
</div>
</div>
<form id="form-filters" class="ui form grid" action="" method="get">
<div class="field wide four column no-margin-mobile">
<label>Type</label>
<Dropdown
v-on:update:selection="updateTypeFeatures"
:options="form.type.choices"
: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
v-on:update:selection="updateStatusFeatures"
: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"></i>
<div class="ui action input">
<input
type="text"
name="title"
v-model="form.title"
@input="fetchPagedFeatures"
/>
<button
type="button"
class="ui teal icon button"
id="submit-search"
>
<i class="search icon"></i>
</button>
</div>
</div>
</div>
<!-- map params, updated on map move -->
<input type="hidden" name="zoom" v-model="zoom" />
<input type="hidden" name="lat" v-model="lat" />
<input type="hidden" name="lng" v-model="lng" />
</form>
<div v-show="showMap" class="ui tab active map-container" data-tab="map">
<div id="map" ref="map"></div>
<SidebarLayers v-if="basemaps && map" />
</div>
<!-- | -->
<FeatureListTable
v-show="!showMap"
v-on:update:page="handlePageChange"
v-on:update:sort="handleSortChange"
:geojsonFeatures="geojsonFeaturesPaginated"
:checkedFeatures.sync="checkedFeatures"
:featuresCount="featuresCount"
:pagination="pagination"
:pageNumbers="pageNumbers"
/>
<!-- 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 @click="modalAllDeleteOpen = false" class="close icon"></i>
<div class="ui icon header">
<i class="trash alternate icon"></i>
Ê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
@click="deleteAllFeatureSelection"
type="button"
class="ui red compact fluid button no-margin"
>
Confirmer la suppression
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapState } from "vuex";
import { mapUtil } from "@/assets/js/map-util.js";
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";
// axios.defaults.headers.common['X-CSRFToken'] = (name => {
// var re = new RegExp(name + "=([^;]+)");
// var value = re.exec(document.cookie);
// return (value !== null) ? unescape(value[1]) : null;
// })('csrftoken');
export default {
name: "Feature_list",
components: {
SidebarLayers,
Dropdown,
FeatureListTable,
},
data() {
return {
modalAllDeleteOpen: false,
form: {
type: {
selected: null,
choices: [],
},
status: {
selected: {
name: null,
value: null,
},
choices: [
{
name: "Brouillon",
value: "draft",
},
{
name: "En attente de publication",
value: "pending",
},
{
name: "Publié",
value: "published",
},
{
name: "Archivé",
value: "archived",
},
],
},
title: null,
},
geojsonFeatures: [],
geojsonFeaturesPaginated: [],
baseUrl: this.$store.state.configuration.BASE_URL,
map: null,
zoom: null,
lat: null,
lng: null,
//limit: 15,
//offset: 0,
featuresCount: 0,
filterType: null,
filterStatus: null,
pagination: {
currentPage: 1,
pagesize: 15,
start: 0,
end: 15,
},
previous: null,
next: null,
showMap: true,
showAddFeature: false,
paginatedFeaturesDone: true,
};
},
computed: {
...mapGetters(["project", "permissions"]),
...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"
);
},
pageNumbers() {
const totalPages = Math.ceil(
this.featuresCount / this.pagination.pagesize
);
return [...Array(totalPages).keys()].map((pageNumb) => {
++pageNumb;
return pageNumb;
});
},
filteredFeatures() {
let results = this.geojsonFeatures;
if (this.form.type.selected) {
results = results.filter(
(el) => el.properties.feature_type.title === this.form.type.selected
);
}
if (this.form.status.selected.value) {
console.log("filter by" + this.form.status.selected.value);
results = results.filter(
(el) => el.properties.status.value === this.form.status.selected.value
);
}
if (this.form.title) {
results = results.filter((el) => {
if (el.properties.title) {
return el.properties.title
.toLowerCase()
.includes(this.form.title.toLowerCase());
} else
return el.id.toLowerCase().includes(this.form.title.toLowerCase());
});
}
return results;
},
},
watch: {
filteredFeatures(newValue, oldValue) {
if (newValue && newValue !== oldValue) {
this.onFilterChange();
}
},
},
methods: {
showTable() {
if (this.paginatedFeaturesDone) {
this.fetchPagedFeatures();
this.paginatedFeaturesDone = false;
}
this.showMap = false;
},
modalAllDelete() {
this.modalAllDeleteOpen = !this.modalAllDeleteOpen;
},
deleteFeature(feature_id) {
const url = `${this.API_BASE_URL}features/${feature_id}/?project__slug=${this.project.slug}`;
axios
.delete(url, {})
.then(() => {
if (!this.modalAllDeleteOpen) {
this.$store
.dispatch("feature/GET_PROJECT_FEATURES", {
project_slug: this.project.slug,
})
.then(() => {
this.fetchPagedFeatures();
this.getNloadGeojsonFeatures();
this.checkedFeatures.splice(feature_id);
});
}
})
.catch(() => {
return false;
});
},
deleteAllFeatureSelection() {
let feature = {};
this.checkedFeatures.forEach((feature_id) => {
feature = { feature_id: feature_id };
this.deleteFeature(feature.feature_id);
});
this.modalAllDelete();
},
onFilterChange() {
if (this.featureGroup) {
const features = this.filteredFeatures;
this.featureGroup.clearLayers();
this.featureGroup = mapUtil.addFeatures(
features,
{},
true,
this.feature_types
);
mapUtil.getMap().invalidateSize();
mapUtil.getMap()._onResize(); // force refresh for vector tiles
if(window.layerMVT) {
window.layerMVT.redraw();
}
if (this.featureGroup.getLayers().length > 0) {
mapUtil
.getMap()
.fitBounds(this.featureGroup.getBounds(), { padding: [25, 25] });
} else {
mapUtil.getMap().zoomOut(1);
}
}
},
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,
});
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 ----------
//this.fetchPagedFeatures();
this.getNloadGeojsonFeatures();
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);
},
getNloadGeojsonFeatures() {
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/?output=geojson`;
this.$store.commit(
"DISPLAY_LOADER",
"Récupération des signalements en cours..."
);
axios
.get(url)
.then((response) => {
if (response.status === 200 && response.data.features.length > 0) {
this.geojsonFeatures = response.data.features;
this.loadFeatures();
}
this.$store.commit("DISCARD_LOADER");
})
.catch((error) => {
this.$store.commit("DISCARD_LOADER");
throw error;
});
},
loadFeatures() {
const urlParams = new URLSearchParams(window.location.search);
const featureType = urlParams.get("feature_type");
const featureStatus = urlParams.get("status");
const featureTitle = urlParams.get("title");
this.featureGroup = mapUtil.addFeatures(
this.geojsonFeatures,
{
featureType,
featureStatus,
featureTitle,
},
true,
this.feature_types
);
// Fit the map to bound only if no initial zoom and center are defined
if (
(this.lat === "" || this.lng === "" || this.zoom === "") &&
this.geojsonFeatures.length > 0
) {
mapUtil
.getMap()
.fitBounds(this.featureGroup.getBounds(), { padding: [25, 25] });
}
this.form.type.choices = [
//* converting Set to an Array with spread "..."
...new Set(
this.geojsonFeatures.map((el) => el.properties.feature_type.title)
), //* use Set to eliminate duplicate values
];
},
//* Paginated Features for table *//
getFeatureTypeSlug(title) {
const featureType = this.feature_types.find((el) => el.title === title);
return featureType ? featureType.slug : null;
},
buildFilterParams({ filterType, filterValue }) {
let params = "";
let typeFilter, statusFilter;
//*** feature type ***//
if (filterType === "featureType") {
if (filterValue === "" && !this.form.type.selected) {
//* s'il y n'avait pas de filtre et qu'il a été supprimé --> ne pas mettre à jour les features
return "abort";
} else if (filterValue !== undefined && filterValue !== null) {
//* s'il y a un nouveau filtre --> ajouter une params
typeFilter = this.getFeatureTypeSlug(filterValue);
} //* sinon il n'y a pas de param ajouté, ce qui supprime la query
//*** status ***//
} else if (filterType === "status") {
if (filterValue === "" && !this.form.status.selected.value) {
return "abort";
} else if (filterValue !== undefined && filterValue !== null) {
statusFilter = filterValue.value;
}
}
//* after possibilities of aborting features fetch, empty geojson to make sure even no result would update
if (
(filterType === undefined || filterType === "status") &&
this.form.type.selected
) {
//* s'il y a déjà un filtre sélectionné, maintenir le params
typeFilter = this.getFeatureTypeSlug(this.form.type.selected);
}
if (
(filterType === undefined || filterType === "featureType") &&
this.form.status.selected.value
) {
statusFilter = this.form.status.selected.value;
}
if (typeFilter) {
let typeParams = `&feature_type_slug=${typeFilter}`;
params += typeParams;
}
if (statusFilter) {
let statusParams = `&status__value=${statusFilter}`;
params += statusParams;
}
//*** title ***//
if (this.form.title) {
params += `&title=${this.form.title}`;
}
return params;
},
updateTypeFeatures(filterValue) {
//* only update:selection custom event can trigger the filter update,
//* but it happens before the value is updated, thus using selected value from event to update query
this.fetchPagedFeatures({ filterType: "featureType", filterValue });
},
updateStatusFeatures(filterValue) {
this.fetchPagedFeatures({ filterType: "status", filterValue });
},
fetchPagedFeatures(params) {
// this.onFilterChange(); //* temporary, use paginated event to watch change in filters, to modify geojson on map
//* replace function calls in watcher and on input
let url = `${this.API_BASE_URL}projects/${this.$route.params.slug}/feature-paginated/?output=geojson&limit=${this.pagination.pagesize}&offset=${this.pagination.start}`;
if (params) {
if (typeof params === "object") {
const filterParams = this.buildFilterParams(params);
if (filterParams === "abort") return;
url += filterParams;
} else {
//console.error("ONLY FOR DEV !!!!!!!!!!!!!");
//params = params.replace("8000", "8010");
//console.log(url);
url = params;
}
}
this.$store.commit(
"DISPLAY_LOADER",
"Récupération des signalements en cours..."
);
axios
.get(url)
.then((response) => {
if (response.status === 200) {
this.featuresCount = response.data.count;
this.previous = response.data.previous;
this.next = response.data.next;
this.geojsonFeaturesPaginated = response.data.results.features;
//if (response.data.results.features.length > 0) {
//this.loadFeatures();
//this.onFilterChange();
//}
}
this.$store.commit("DISCARD_LOADER");
})
.catch((error) => {
this.$store.commit("DISCARD_LOADER");
throw error;
});
},
//* 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(ordering) {
console.log("ORDERING", ordering)
},
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);
}
},
},
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("GET_PROJECT_INFO", this.$route.params.slug)
.then(() => {
this.initMap();
});
} else {
this.initMap();
}
},
destroyed() {
//* allow user to change page if ever stuck on loader
this.$store.commit("DISCARD_LOADER");
},
};
</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;
}
.center {
text-align: center !important;
}
#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-margin {
margin: 0 !important;
}
.no-padding {
padding: 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;
}
#form-filters > .field.column {
width: 100% !important;
}
.map-container {
width: 100%;
}
}
</style>