diff --git a/src/components/feature/FeatureListTable.vue b/src/components/feature/FeatureListTable.vue index d19aa0160cc080fc976e5a8adb21190885a1acef..2787cb177a7096722f862dfe21b194c5bcf96216 100644 --- a/src/components/feature/FeatureListTable.vue +++ b/src/components/feature/FeatureListTable.vue @@ -75,14 +75,16 @@ </tr> </thead> <tbody> - <tr v-for="(feature, index) in paginatedFeatures" :key="index"> + <tr v-for="(feature, index) in sortedFeatures" :key="index"> <td class="center"> <div class="ui checkbox" :class=" - feature.properties.creator.username !== user.username && !user.is_superuser && !isUserProjectAdministrator ? - 'disabled' : - '' + feature.properties.creator.username !== user.username && + !user.is_superuser && + !isUserProjectAdministrator + ? 'disabled' + : '' " > <input @@ -90,7 +92,11 @@ :id="feature.id" :value="feature.id" v-model="checked" - :disabled="feature.properties.creator.username !== user.username && !user.is_superuser && !isUserProjectAdministrator" + :disabled=" + feature.properties.creator.username !== user.username && + !user.is_superuser && + !isUserProjectAdministrator + " /> <label></label> </div> @@ -157,7 +163,7 @@ {{ feature.properties.display_last_editor }} </td> </tr> - <tr v-if="filteredFeatures.length === 0" class="odd"> + <tr v-if="featuresCount === 0" class="odd"> <td colspan="5" class="dataTables_empty" valign="top"> Aucune donnée disponible </td> @@ -165,7 +171,7 @@ </tbody> </table> <div - v-if="nbPages.length > 1" + v-if="pageNumbers.length > 1" id="table-features_info" class="dataTables_info" role="status" @@ -173,15 +179,15 @@ > Affichage de l'élément {{ pagination.start + 1 }} à {{ displayedPageEnd }} - sur {{ filteredFeatures.length }} éléments + sur {{ featuresCount }} éléments </div> <div - v-if="nbPages.length > 1" + v-if="pageNumbers.length > 1" id="table-features_paginate" class="dataTables_paginate paging_simple_numbers" > <a - @click="toPreviousPage" + @click="$emit('update:page', 'previous');" id="table-features_previous" :class="[ 'paginate_button previous', @@ -194,9 +200,9 @@ > <span> <a - v-for="pageNumber in nbPages" + v-for="pageNumber in pageNumbers" :key="'page' + pageNumber" - @click="toPage(pageNumber)" + @click="$emit('update:page', pageNumber)" :class="[ 'paginate_button', { current: pageNumber === pagination.currentPage }, @@ -207,17 +213,17 @@ >{{ pageNumber }}</a > </span> - <!-- // TODO : <span v-if="nbPages > 4" class="ellipsis">...</span> --> + <a id="table-features_next" :class="[ 'paginate_button next', - { disabled: pagination.currentPage === nbPages.length }, + { disabled: pagination.currentPage === pageNumbers.length }, ]" aria-controls="table-features" data-dt-idx="7" tabindex="0" - @click="toNextPage" + @click="$emit('update:page', 'next');" >Suivant</a > </div> @@ -225,21 +231,21 @@ </template> <script> -import { mapState, mapGetters } from 'vuex'; +import { mapState, mapGetters } from "vuex"; export default { name: "FeatureListTable", - props: ["filteredFeatures", "checkedFeatures"], + props: [ + "geojsonFeatures", + "checkedFeatures", + "featuresCount", + "pagination", + "pageNumbers", + ], data() { return { - pagination: { - currentPage: 1, - pagesize: 15, - start: 0, - end: 15, - }, sort: { column: "", ascending: true, @@ -248,23 +254,18 @@ export default { }, computed: { - ...mapState([ - 'user', - ]), - ...mapGetters([ - 'project', - 'permissions' - ]), + ...mapState(["user"]), + ...mapGetters(["project", "permissions"]), isUserProjectAdministrator() { return this.permissions.is_project_administrator; }, - paginatedFeatures() { - let filterdFeatures = [...this.filteredFeatures]; + sortedFeatures() { + let sortedFeatures = [...this.geojsonFeatures]; // Ajout du tri if (this.sort.column !== "") { - filterdFeatures = filterdFeatures.sort((a, b) => { + sortedFeatures = sortedFeatures.sort((a, b) => { let aProp = this.getFeatureDisplayName(a); let bProp = this.getFeatureDisplayName(b); if (this.sort.column === "statut") { @@ -301,18 +302,7 @@ export default { } }); } - return filterdFeatures.slice(this.pagination.start, this.pagination.end); - }, - - nbPages() { - let N = Math.ceil( - this.filteredFeatures.length / this.pagination.pagesize - ); - const arr = [...Array(N).keys()].map(function (x) { - ++x; - return x; - }); - return arr; + return sortedFeatures; }, checked: { @@ -325,8 +315,8 @@ export default { }, displayedPageEnd() { - return this.filteredFeatures.length <= this.pagination.end - ? this.filteredFeatures.length + return this.featuresCount <= this.pagination.end + ? this.featuresCount : this.pagination.end; }, }, @@ -358,30 +348,6 @@ export default { this.sort.ascending = true; } }, - - toPage(pageNumber) { - const toAddOrRemove = - (pageNumber - this.pagination.currentPage) * this.pagination.pagesize; - this.pagination.start += toAddOrRemove; - this.pagination.end += toAddOrRemove; - this.pagination.currentPage = pageNumber; - }, - - toPreviousPage() { - if (this.pagination.start > 0) { - this.pagination.start -= this.pagination.pagesize; - this.pagination.end -= this.pagination.pagesize; - this.pagination.currentPage -= 1; - } - }, - - toNextPage() { - if (this.pagination.end < this.filteredFeatures.length) { - this.pagination.start += this.pagination.pagesize; - this.pagination.end += this.pagination.pagesize; - this.pagination.currentPage += 1; - } - }, }, }; </script> diff --git a/src/views/feature/Feature_list.vue b/src/views/feature/Feature_list.vue index 854579a247e998a267cd859506f62c53704a9ff6..1bb24d0fbc731c81422a92f8ed244ce03e499798 100644 --- a/src/views/feature/Feature_list.vue +++ b/src/views/feature/Feature_list.vue @@ -16,7 +16,7 @@ ><i class="map fitted icon"></i ></a> <a - @click="showMap = false" + @click="showTable" :class="['item no-margin', { active: !showMap }]" data-tab="list" data-tooltip="Liste" @@ -24,6 +24,7 @@ ></a> <div class="item"> <h4> + <!-- {{ featuresCount }} signalement{{ featuresCount > 1 ? "s" : "" }} --> {{ filteredFeatures.length }} signalement{{ filteredFeatures.length > 1 ? "s" : "" }} @@ -86,6 +87,7 @@ <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" @@ -97,6 +99,7 @@ <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" @@ -113,7 +116,7 @@ type="text" name="title" v-model="form.title" - @input="onFilterChange" + @input="fetchPagedFeatures" /> <button type="button" @@ -133,12 +136,17 @@ <div v-show="showMap" class="ui tab active map-container" data-tab="map"> <div id="map" ref="map"></div> - <SidebarLayers v-if="baseMaps && map" /> + <SidebarLayers v-if="basemaps && map" /> </div> + <!-- | --> <FeatureListTable v-show="!showMap" - :filteredFeatures="filteredFeatures" + v-on:update:page="handlePageChange" + :geojsonFeatures="geojsonFeaturesPaginated" :checkedFeatures.sync="checkedFeatures" + :featuresCount="featuresCount" + :pagination="pagination" + :pageNumbers="pageNumbers" /> <!-- MODAL ALL DELETE FEATURE TYPE --> @@ -180,7 +188,7 @@ 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'; +import axios from "@/axios-client.js"; // axios.defaults.headers.common['X-CSRFToken'] = (name => { // var re = new RegExp(name + "=([^;]+)"); @@ -233,33 +241,56 @@ export default { }, geojsonFeatures: [], - filterStatus: null, - filterType: null, + 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", [ - 'features', - 'checkedFeatures' - ]), - ...mapState('feature_type', [ - 'feature_types' - ]), - - baseMaps() { - return this.$store.state.map.basemaps; + ...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() { @@ -287,13 +318,6 @@ export default { } return results; }, - - 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" - ); - }, }, watch: { @@ -305,23 +329,30 @@ export default { }, methods: { + showTable() { + if (this.paginatedFeaturesDone) { + this.fetchPagedFeatures(); + this.paginatedFeaturesDone = false; + } + this.showMap = false; + }, + modalAllDelete() { this.modalAllDeleteOpen = !this.modalAllDeleteOpen; }, deleteFeature(feature_id) { - const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}features/${feature_id}/?project__slug=${this.project.slug}`; + 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 - } - ) + .dispatch("feature/GET_PROJECT_FEATURES", { + project_slug: this.project.slug, + }) .then(() => { + this.fetchPagedFeatures(); this.getNloadGeojsonFeatures(); this.checkedFeatures.splice(feature_id); }); @@ -349,7 +380,7 @@ export default { features, {}, true, - this.$store.state.feature_type.feature_types + this.feature_types ); mapUtil.getMap().invalidateSize(); mapUtil.getMap()._onResize(); // force refresh for vector tiles @@ -368,17 +399,15 @@ export default { }, initMap() { - console.log(this); 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, @@ -394,22 +423,20 @@ export default { }); // --------- End sidebar events ---------- + //this.fetchPagedFeatures(); this.getNloadGeojsonFeatures(); - setTimeout( - function () { - let project_id = this.$route.params.slug.split("-")[0]; - const mvtUrl = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}features.mvt/?tile={z}/{x}/{y}&project_id=${project_id}`; - mapUtil.addVectorTileLayer( - mvtUrl, - this.$route.params.slug, - this.$store.state.feature_type.feature_types, - this.form - ); - mapUtil.addGeocoders(this.$store.state.configuration); - }.bind(this), - 1000 - ); + 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() { @@ -422,7 +449,8 @@ export default { .get(url) .then((response) => { if (response.status === 200 && response.data.features.length > 0) { - this.loadFeatures(response.data.features); + this.geojsonFeatures = response.data.features; + this.loadFeatures(); } this.$store.commit("DISCARD_LOADER"); }) @@ -432,8 +460,7 @@ export default { }); }, - loadFeatures(features) { - this.geojsonFeatures = features; + loadFeatures() { const urlParams = new URLSearchParams(window.location.search); const featureType = urlParams.get("feature_type"); const featureStatus = urlParams.get("status"); @@ -446,7 +473,7 @@ export default { featureTitle, }, true, - this.$store.state.feature_type.feature_types + this.feature_types ); // Fit the map to bound only if no initial zoom and center are defined if ( @@ -464,25 +491,177 @@ export default { ), //* 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); + } + }, + + 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 - axios.all([ - this.$store.dispatch("GET_PROJECT_INFO", this.$route.params.slug), - this.$store.dispatch('feature/GET_PROJECT_FEATURES', { - project_slug: this.$route.params.slug - })]) + this.$store + .dispatch("GET_PROJECT_INFO", this.$route.params.slug) .then(() => { this.initMap(); }); - } - else { + } else { this.initMap(); } - }, destroyed() { diff --git a/src/views/project/Project_edit.vue b/src/views/project/Project_edit.vue index 3b44bcc8034ce968788560746b0833f8ea9aecb3..f9784c6a4e5a064f2201ae0b7cc50c30a14cb556 100644 --- a/src/views/project/Project_edit.vue +++ b/src/views/project/Project_edit.vue @@ -458,14 +458,7 @@ export default { }); } }, - }, - - created() { - this.definePageType(); - console.log(this.action); - if (this.action === "create") { - this.thumbnailFileSrc = require("@/assets/img/default.png"); - } else if (this.action === "edit" || this.action === "create_from") { + fillProjectForm() { if (!this.project) { this.$store.dispatch("GET_PROJECT_INFO", this.$route.params.slug); } @@ -480,6 +473,7 @@ export default { this.form.is_project_type = false; } //* transform string values to objects for dropdowns display (could be in a computed) + this.form.access_level_pub_feature = { name: this.project.access_level_pub_feature, value: this.levelPermissions.find( @@ -492,6 +486,15 @@ export default { (el) => el.name === this.project.access_level_arch_feature ).value, }; + }, + }, + + created() { + this.definePageType(); + if (this.action === "create") { + this.thumbnailFileSrc = require("@/assets/img/default.png"); + } else if (this.action === "edit" || this.action === "create_from") { + this.fillProjectForm(); } }, };