diff --git a/package-lock.json b/package-lock.json index aac4365c145df2276e626c9152f58a9009190219..5c65244599ec38ec18636b5cfef2ee74c1468351 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11592,6 +11592,11 @@ } } }, + "vue-multiselect": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-2.1.6.tgz", + "integrity": "sha512-s7jmZPlm9FeueJg1RwJtnE9KNPtME/7C8uRWSfp9/yEN4M8XcS/d+bddoyVwVnvFyRh9msFo0HWeW0vTL8Qv+w==" + }, "vue-router": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.2.tgz", diff --git a/package.json b/package.json index 6288ae767b382e24b599d969739e7d3d9c545a0e..6349c1907fa5f9956b842e129dd23e2b90e5c30f 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "sortablejs": "^1.14.0", "vue": "^2.6.11", "vue-frag": "^1.1.5", + "vue-multiselect": "~2.1.6", "vue-router": "^3.2.0", "vuex": "^3.6.2" }, diff --git a/src/assets/styles/base.css b/src/assets/styles/base.css index d49a7d4750077875a1d26d66f025feef2eba7b9d..0777e8e3742e08d03179550ddfff9e40778efb21 100644 --- a/src/assets/styles/base.css +++ b/src/assets/styles/base.css @@ -158,4 +158,48 @@ footer .ui.text.menu .item:not(:first-child) { border-radius: 3px; background-color: rgb(250, 241, 242); padding: 1rem; +} + +/* ---------------------------------- */ + /* ERROR LIST */ +/* ---------------------------------- */ +.multiselect__tags { + border: 2px solid #ced4da; +} +.multiselect__tags > .multiselect__input { + border: none !important; + font-size: 1rem !important; +} + +.multiselect__placeholder { + color: #838383; + margin-bottom: 0px; + padding-top: 0; +} + +.multiselect__single, .multiselect__tags, .multiselect__content, .multiselect__option { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + max-width: 100%; +} + +.multiselect__select { + z-index: 1 !important; +} +.multiselect__clear { + position: absolute; + right: 1px; + top: 8px; + width: 40px; + display: block; + cursor: pointer; + z-index: 9; + background-color: #fff; +} +.multiselect__spinner { + z-index: 2 !important; + background-color: #fff; + opacity: 1; + top: 2px; } \ No newline at end of file diff --git a/src/components/SearchFeature.vue b/src/components/SearchFeature.vue new file mode 100644 index 0000000000000000000000000000000000000000..6ea7c0d5747542584fedcddacede3988782670cc --- /dev/null +++ b/src/components/SearchFeature.vue @@ -0,0 +1,114 @@ +<template> + <div> + <Multiselect + v-model="selection" + :options="results" + :options-limit="10" + :allow-empty="true" + track-by="feature_id" + label="title" + :reset-after="false" + select-label="" + selected-label="" + deselect-label="" + :searchable="true" + :placeholder="'Rechercher un signalement ...'" + :show-no-results="true" + :loading="loading" + :clear-on-select="false" + :preserve-search="true" + @search-change="search" + @select="select" + @close="close" + > + <template slot="clear"> + <div + v-if="selection" + class="multiselect__clear" + @click.prevent.stop="selection = null" + > + <i class="close icon"></i> + </div> + </template> + <span slot="noResult"> + Aucun résultat. + </span> + <span slot="noOptions"> + Saisissez les premiers caractères ... + </span> + </Multiselect> + </div> +</template> + +<script> +import { mapState, mapMutations, mapActions } from 'vuex'; + +import Multiselect from 'vue-multiselect'; + +export default { + name: 'SearchFeature', + + components: { + Multiselect + }, + + data() { + return { + loading: false, + selection: null, + text: null, + results: [] + } + }, + + computed: { + ...mapState('feature', [ + 'features' + ]) + }, + + watch: { + text: function(newValue) { + this.loading = true; + this.GET_PROJECT_FEATURES({ + project_slug: this.$route.params.slug, + feature_type__slug: this.$route.params.slug_type_signal, + search: newValue, + limit: '10' + }) + .then(() => { + if (newValue) { + this.results = this.features; + } else { + this.results.splice(0); + } + this.loading = false; + }); + } + }, + + created() { + this.RESET_CANCELLABLE_SEARCH_REQUEST(); + }, + + methods: { + ...mapMutations(['RESET_CANCELLABLE_SEARCH_REQUEST']), + ...mapActions('feature', [ + 'GET_PROJECT_FEATURES' + ]), + search(text) { + this.text = text; + }, + select(e) { + this.$emit('select', e); + }, + close() { + this.$emit('close', this.selection); + } + } +} +</script> + +<style scoped> + +</style> diff --git a/src/components/feature/FeatureLinkedForm.vue b/src/components/feature/FeatureLinkedForm.vue index 02cd0bfe25a382e920681ea126841bc5ac80f971..27c60fbd5eaa879ae80d94c40df07bb04de5cc56 100644 --- a/src/components/feature/FeatureLinkedForm.vue +++ b/src/components/feature/FeatureLinkedForm.vue @@ -30,10 +30,9 @@ <label for="form.feature_to.id_for_label">{{ form.feature_to.label }}</label> - <Dropdown - :options="featureOptions" - :selected="selected_feature_to" - :selection.sync="selected_feature_to" + <SearchFeature + @select="selectFeatureTo" + @close="selectFeatureTo" /> {{ form.feature_to.errors }} </div> @@ -44,6 +43,7 @@ <script> import Dropdown from "@/components/Dropdown.vue"; +import SearchFeature from '@/components/SearchFeature.vue'; export default { name: "FeatureLinkedForm", @@ -52,6 +52,7 @@ export default { components: { Dropdown, + SearchFeature }, data() { @@ -92,22 +93,6 @@ export default { }, computed: { - featureOptions: function () { - return this.features - .filter( - (el) => - el.feature_type.slug === this.$route.params.slug_type_signal && //* filter only for the same feature - el.feature_id !== this.$route.params.slug_signal //* filter out current feature - ) - .map((el) => { - return { - name: `${el.title} (${el.display_creator} - ${this.formatDate( - el.created_on - )})`, - value: el.feature_id, - }; - }); - }, selected_relation_type: { // getter @@ -119,27 +104,7 @@ export default { this.form.relation_type.value = newValue; this.updateStore(); }, - }, - - selected_feature_to: { - // getter - get() { - return this.form.feature_to.value.name; - }, - // setter - set(newValue) { - this.form.feature_to.value = newValue; - this.updateStore(); - }, - }, - }, - - watch: { - featureOptions(newValue) { - if (newValue) { - this.getExistingFeature_to(newValue); - } - }, + } }, methods: { @@ -153,6 +118,10 @@ export default { this.$store.commit("feature/REMOVE_LINKED_FORM", this.linkedForm.dataKey); }, + selectFeatureTo(e) { + this.form.feature_to.value = e; + }, + updateStore() { this.$store.commit("feature/UPDATE_LINKED_FORM", { dataKey: this.linkedForm.dataKey, diff --git a/src/main.js b/src/main.js index 3e5f6956593ac0d53834bdcd46cf80970188613a..d986b104faeb379d2520a42994c0d6dfea5788fd 100644 --- a/src/main.js +++ b/src/main.js @@ -13,6 +13,8 @@ import '@fortawesome/fontawesome-free/js/all.js' import { library } from '@fortawesome/fontawesome-svg-core'; import { fas } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; +// Multiselect installation +import 'vue-multiselect/dist/vue-multiselect.min.css'; library.add(fas) diff --git a/src/store/index.js b/src/store/index.js index 326af9583fae47c8d7d016c4f011a1b2c26b83b2..8ca73167c4c0afe944e3b551f99d243c47b79ab4 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -52,6 +52,7 @@ export default new Vuex.Store({ isLoading: false, message: "En cours de chargement" }, + cancellableSearchRequest: [] }, mutations: { @@ -116,6 +117,13 @@ export default new Vuex.Store({ message: "En cours de chargement" }; }, + SET_CANCELLABLE_SEARCH_REQUEST(state, payload) { + state.cancellableSearchRequest.push(payload); + }, + + RESET_CANCELLABLE_SEARCH_REQUEST(state) { + state.cancellableSearchRequest = []; + }, }, getters: { @@ -278,7 +286,7 @@ export default new Vuex.Store({ let promises = [ dispatch("GET_PROJECT_LAST_MESSAGES", slug).then(response => response), dispatch("feature_type/GET_PROJECT_FEATURE_TYPES", slug).then(response => response), - dispatch("feature/GET_PROJECT_FEATURES", slug).then(response => response), + // dispatch("feature/GET_PROJECT_FEATURES", slug).then(response => response), ] if (state.user) promises.push(dispatch("map/GET_BASEMAPS", slug).then(response => response)) diff --git a/src/store/modules/feature.js b/src/store/modules/feature.js index e72e524d4d9d0614df74203326a67f9e841c7040..6db7f1afc510a15f2a09dc63199f8d57773e528f 100644 --- a/src/store/modules/feature.js +++ b/src/store/modules/feature.js @@ -99,9 +99,28 @@ const feature = { getters: { }, actions: { - GET_PROJECT_FEATURES({ commit, rootState }, project_slug) { + GET_PROJECT_FEATURES({ commit, rootState }, { project_slug, feature_type__slug, search, limit }) { + if (rootState.cancellableSearchRequest.length > 0) { + const currentRequestCancelToken = + rootState.cancellableSearchRequest[rootState.cancellableSearchRequest.length - 1]; + currentRequestCancelToken.cancel(); + } + + const cancelToken = axios.CancelToken.source(); + commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true }); + + let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}projects/${project_slug}/feature/`; + if (feature_type__slug) { + url = url.concat('', `${url.includes('?') ? '&' : '?'}feature_type__slug=${feature_type__slug}`); + } + if (search) { + url = url.concat('', `${url.includes('?') ? '&' : '?'}title__contains=${search}`); + } + if (limit) { + url =url.concat('', `${url.includes('?') ? '&' : '?'}limit=${limit}`); + } return axios - .get(`${rootState.configuration.VUE_APP_DJANGO_API_BASE}projects/${project_slug}/feature/`) + .get(url , { cancelToken: cancelToken.token }) .then((response) => { if (response.status === 200 && response.data) { const features = response.data.features; @@ -120,18 +139,24 @@ const feature = { const message = routeName === "editer-signalement" ? "Le signalement a été mis à jour" : "Le signalement a été crée"; function redirect(featureId) { - dispatch("GET_PROJECT_FEATURES", rootState.project_slug).then(() => { - console.log(state.feature); - commit("DISCARD_LOADER", null, { root: true }) - router.push({ - name: "details-signalement", - params: { - slug_type_signal: rootState.feature_type.current_feature_type_slug, - slug_signal: featureId, - message, - }, + dispatch( + 'GET_PROJECT_FEATURES', + { + project_slug: rootState.project_slug, + feature_type__slug: rootState.feature_type.current_feature_type_slug + } + ) + .then(() => { + commit("DISCARD_LOADER", null, { root: true }) + router.push({ + name: "details-signalement", + params: { + slug_type_signal: rootState.feature_type.current_feature_type_slug, + slug_signal: featureId, + message, + }, + }); }); - }) } async function handleOtherForms(featureId) { diff --git a/src/views/feature/Feature_detail.vue b/src/views/feature/Feature_detail.vue index 6bb76f5b71a1701e79257ee2149ec3b328f5a537..62c585a0ecf9abd67bb2251ed13a33c881bd381c 100644 --- a/src/views/feature/Feature_detail.vue +++ b/src/views/feature/Feature_detail.vue @@ -567,7 +567,9 @@ export default { if (response.status === 204) { this.$store.dispatch( "feature/GET_PROJECT_FEATURES", - this.$route.params.slug + { + project_slug: this.$route.params.slug + } ); this.goBackToProject(); } diff --git a/src/views/feature/Feature_edit.vue b/src/views/feature/Feature_edit.vue index b0944d45e800edee947a5a4716d2b6a72d3d14c2..30ceab1a5636a89b695fe2f8c4991fa3b40ec77e 100644 --- a/src/views/feature/Feature_edit.vue +++ b/src/views/feature/Feature_edit.vue @@ -1018,17 +1018,19 @@ export default { mounted() { this.$store .dispatch("GET_PROJECT_INFO", this.$route.params.slug) - .then((data) => { - console.log(data); - this.initForm(); - this.initMap(); - this.onFeatureTypeLoaded(); - this.initExtraForms(this.feature); - - setTimeout(() => { - mapUtil.addGeocoders(this.$store.state.configuration); - }, 1000); - }); + .then(() => { + this.initForm(); + this.initMap(); + this.onFeatureTypeLoaded(); + this.initExtraForms(this.feature); + + setTimeout( + function () { + mapUtil.addGeocoders(this.$store.state.configuration); + }.bind(this), + 1000 + ); + }); }, destroyed() { diff --git a/src/views/feature/Feature_list.vue b/src/views/feature/Feature_list.vue index d3b0e0c39f31db659070b6a9464f475d430cef86..37a7eda3f61ef00a0f33bd9ca992e697ca116a44 100644 --- a/src/views/feature/Feature_list.vue +++ b/src/views/feature/Feature_list.vue @@ -322,7 +322,11 @@ export default { .then(() => { if (!this.modalAllDeleteOpen) { this.$store - .dispatch("feature/GET_PROJECT_FEATURES", this.project.slug) + .dispatch("feature/GET_PROJECT_FEATURES", + { + project_slug: this.project.slug + } + ) .then(() => { this.getNloadGeojsonFeatures(); this.checkedFeatures.splice(feature_id); diff --git a/src/views/project/Project_detail.vue b/src/views/project/Project_detail.vue index 523bf1e4978ac7e3caf35ad9dfc722463eea6293..a25fec89a57c9d557b45dd77fb7fbe59e9b8b7a3 100644 --- a/src/views/project/Project_detail.vue +++ b/src/views/project/Project_detail.vue @@ -337,6 +337,14 @@ <div class="content"> <div class="center aligned header">Derniers signalements</div> <div class="center aligned description"> + <div + :class="{ active: featuresLoading }" + class="ui inverted dimmer" + > + <div class="ui text loader"> + Récupération des signalements en cours... + </div> + </div> <div class="ui relaxed list"> <div v-for="(item, index) in last_features" @@ -572,6 +580,7 @@ export default { is_suscriber: false, tempMessage: null, featureTypeLoading: true, + featuresLoading: true }; }, @@ -771,10 +780,17 @@ export default { }, mounted() { - this.$store.dispatch("GET_PROJECT_INFO", this.slug).then(() => { - this.featureTypeLoading = false; - setTimeout(this.initMap, 1000); - }); + this.$store.dispatch('GET_PROJECT_INFO', this.slug) + .then(() => { + this.featureTypeLoading = false; + setTimeout(this.initMap, 1000); + }); + this.$store.dispatch('feature/GET_PROJECT_FEATURES', { + project_slug: this.slug + }) + .then(() => { + this.featuresLoading = false; + }); if (this.message) { this.tempMessage = this.message;