diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index ec2a5d07f5d4fa8812cd2b2d8da1dedcca000a10..5b6fe4777530ebbe7898f2d45712b614b841f0a2 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -172,7 +172,7 @@ data-position="bottom right" > <div class="crossed-out"> - <i class="wifi icon"/> + <i class="wifi icon" /> </div> </span> </div> @@ -228,21 +228,21 @@ </div> </div> </div> - <MessageInfo /> + <MessageInfoList /> </div> </div> </template> <script> import { mapState } from 'vuex'; -import MessageInfo from '@/components/MessageInfo'; +import MessageInfoList from '@/components/MessageInfoList'; export default { name: 'AppHeader', components: { - MessageInfo + MessageInfoList }, data() { diff --git a/src/components/Feature/Edit/FeatureExtraForm.vue b/src/components/Feature/Edit/FeatureExtraForm.vue index 36f4130b18a0fb6a3503c35eb256e1ef209d3bd7..6e671d42c2baa854b1651e76c63b6e01587cca0c 100644 --- a/src/components/Feature/Edit/FeatureExtraForm.vue +++ b/src/components/Feature/Edit/FeatureExtraForm.vue @@ -140,7 +140,7 @@ export default { }, displayLabels() { - return this.$route.name === 'editer-signalement' || this.$route.name === 'ajouter-signalement'; + return this.$route.name === 'editer-signalement' || this.$route.name === 'ajouter-signalement' || this.$route.name === 'editer-attribut-signalement'; } }, diff --git a/src/components/Feature/FeatureListMassToggle.vue b/src/components/Feature/FeatureListMassToggle.vue deleted file mode 100644 index ff4b1da6ff6616933727a93e6595b10238a7bfd8..0000000000000000000000000000000000000000 --- a/src/components/Feature/FeatureListMassToggle.vue +++ /dev/null @@ -1,59 +0,0 @@ -<template> - <div - class="switch-buttons pointer" - :data-tooltip="`Passer en mode ${massMode === 'modify' ? 'suppression':'édition'}`" - @click="switchMode" - > - <div> - <i - :class="['icon pencil', {disabled: massMode !== 'modify'}]" - aria-hidden="true" - /> - </div> - <span class="grey">| </span> - <div> - <i - :class="['icon trash', {disabled: massMode !== 'delete'}]" - aria-hidden="true" - /> - </div> - </div> -</template> - -<script> -import { mapMutations, mapState } from 'vuex'; - -export default { - name: 'FeatureListMassToggle', - - computed: { - ...mapState('feature', ['massMode']) - }, - - methods: { - ...mapMutations('feature', [ - 'TOGGLE_MASS_MODE', - 'UPDATE_CHECKED_FEATURES', - 'UPDATE_CLICKED_FEATURES']), - - switchMode() { - this.TOGGLE_MASS_MODE(this.massMode === 'modify' ? 'delete' : 'modify'); - this.UPDATE_CLICKED_FEATURES([]); - this.UPDATE_CHECKED_FEATURES([]); - } - }, -}; -</script> - -<style scoped> -.switch-buttons { - display: flex; - justify-content: center; - align-items: baseline; -} - -.grey { - color: #bbbbbb; -} - -</style> diff --git a/src/components/MessageInfo.vue b/src/components/MessageInfo.vue index 3996dc9de95d967afa14deee2d063ddf3dfeddf0..4fe49097eaeefdfd6129215601c6921ce2e80111 100644 --- a/src/components/MessageInfo.vue +++ b/src/components/MessageInfo.vue @@ -1,36 +1,30 @@ <template> - <transition name="fadeDownUp"> - <div - v-if="messages && messages.length > 0" - class="row over-content" - > - <div class="fourteen wide column"> - <div - v-for="(message, index) in messages" - :key="'message-' + index" - :class="['ui', message.level ? message.level : 'info', 'message']" - > + <li + :ref="'message-' + message.counter" + :class="['list-container', { show }]" + > + <div :class="['list-item', { show}]"> + <div :class="['ui', message.level ? message.level : 'info', 'message']"> + <i + class="close icon" + aria-hidden="true" + @click="removeListItem" + /> + <div class="header"> <i - class="close icon" + class="info circle icon" aria-hidden="true" - @click="DISCARD_MESSAGE(message)" /> - <div class="header"> - <i - class="info circle icon" - aria-hidden="true" - /> - Informations - </div> - <ul class="list"> - {{ - message.comment - }} - </ul> + Informations </div> + <ul class="list"> + {{ + message.comment + }} + </ul> </div> </div> - </transition> + </li> </template> <script> @@ -39,54 +33,100 @@ import { mapState, mapMutations } from 'vuex'; export default { name: 'MessageInfo', + props: { + message: { + type: Object, + default: () => {}, + }, + }, + + data() { + return { + listMessages: [], + show: false, + }; + }, + computed: { ...mapState(['messages']), }, + mounted() { + setTimeout(() => { + this.show = true; + }, 15); + }, + methods: { ...mapMutations(['DISCARD_MESSAGE']), + + removeListItem(){ + const container = this.$refs['message-' + this.message.counter]; + container.ontransitionend = () => { + this.DISCARD_MESSAGE(this.message.counter); + }; + this.show = false; + }, }, }; </script> -<style> -.row.over-content { - position: absolute; /* to display message info over page content */ - z-index: 99; - opacity: 0.95; - width: calc(100% - 4em); /* 4em is #content left + right paddings */ - top: calc(61px + 1em); /* 61px is #app-header height */ - right: 2em; /* 2em is #content left paddings */ +<style scoped> +.list-container{ + list-style: none; + width: 100%; + height: 0; + position: relative; + cursor: pointer; + overflow: hidden; + transition: all 0.6s ease-out; } - -.fadeDownUp-enter-active { - animation: fadeInDown .5s; +.list-container.show{ + height: 6em; } -.fadeDownUp-leave-active { - animation: fadeOutUp .5s; +.list-container.show:not(:first-child){ + margin-top: 10px; } -@keyframes fadeOutUp { - 0% { - opacity: 1; - } - - 100% { +.list-container .list-item{ + padding: .5rem 0; + width: 100%; + position: absolute; opacity: 0; - transform: translate3d(0, -100%, 0); - } + top: 0; + left: 0; + transition: all 0.6s ease-out; +} +.list-container .list-item.show{ + opacity: 1; } -@keyframes fadeInDown { - from { - opacity: 0; - transform: translate3d(0, -100%, 0); - } +ul.list{ + overflow: scroll; + height: 2.2em; + margin-bottom: .5em !important; +} - to { - opacity: 1; - transform: translate3d(0, 0, 0); - } +.ui.message { + overflow: hidden; + padding-bottom: 0 !important; +} +.ui.message::after { + content: ""; + position: absolute; + bottom: 0; + left: 1em; + right: 0; + width: calc(100% - 2em); +} +.ui.info.message::after { + box-shadow: 0px -8px 5px 3px rgb(248, 255, 255); +} +.ui.positive.message::after { + box-shadow: 0px -8px 5px 3px rgb(248, 255, 255); +} +.ui.negative.message::after { + box-shadow: 0px -8px 5px 3px rgb(248, 255, 255); } .ui.message > .close.icon { diff --git a/src/components/MessageInfoList.vue b/src/components/MessageInfoList.vue new file mode 100644 index 0000000000000000000000000000000000000000..5974a28d54314d12975ceb58e0cf6eee917aa603 --- /dev/null +++ b/src/components/MessageInfoList.vue @@ -0,0 +1,50 @@ +<template> + <div + v-if="messages && messages.length > 0" + class="row over-content" + > + <div class="fourteen wide column"> + <ul + class="message-list" + aria-live="assertive" + > + <MessageInfo + v-for="message in messages" + :key="'message-' + message.counter" + :message="message" + /> + </ul> + </div> + </div> +</template> + +<script> +import { mapState } from 'vuex'; +import MessageInfo from '@/components/MessageInfo'; + +export default { + name: 'MessageInfoList', + + components: { + MessageInfo, + }, + computed: { + ...mapState(['messages']), + }, +}; +</script> + +<style scoped> + +.row.over-content { + position: absolute; /* to display message info over page content */ + z-index: 99; + opacity: 0.95; + width: calc(100% - 4em); /* 4em is #content left + right paddings */ + top: calc(61px + 1em); /* 61px is #app-header height */ + right: 2em; /* 2em is #content left paddings */ +} +.message-list{ + list-style: none; +} +</style> \ No newline at end of file diff --git a/src/components/Project/FeaturesListAndMap/FeatureListTable.vue b/src/components/Project/FeaturesListAndMap/FeatureListTable.vue index cc4b12e12e55c74c05befd20f97c08c816682d51..a522fc895075e010c2c9e902d4cc6e395bf1c636 100644 --- a/src/components/Project/FeaturesListAndMap/FeatureListTable.vue +++ b/src/components/Project/FeaturesListAndMap/FeatureListTable.vue @@ -1,8 +1,53 @@ <template> <div> - <div class="table-mobile-buttons left-align"> - <FeatureListMassToggle v-if="isOnline" /> + <div class="ui form"> + <div + v-if="isOnline" + class="inline fields" + > + <label + data-tooltip="Choisir un type de sélection de signalements pour effectuer une action" + data-position="bottom left" + >Mode de sélection :</label> + <div class="field"> + <div class="ui radio checkbox"> + <input + id="edit-status" + v-model="mode" + type="radio" + name="mode" + value="edit-status" + > + <label for="edit-status">Édition de statut</label> + </div> + </div> + <div class="field"> + <div class="ui radio checkbox"> + <input + id="edit-attributes" + v-model="mode" + type="radio" + name="mode" + value="edit-attributes" + > + <label for="edit-attributes">Édition d'attribut</label> + </div> + </div> + <div class="field"> + <div class="ui radio checkbox"> + <input + id="delete-features" + v-model="mode" + type="radio" + name="mode" + value="delete-features" + > + <label for="delete-features">Suppression de signalement</label> + </div> + </div> + </div> </div> + <div data-tab="list" class="dataTables_wrapper no-footer" @@ -19,7 +64,7 @@ scope="col" class="dt-center" > - <FeatureListMassToggle /> + Sélection </th> <th @@ -356,7 +401,6 @@ <script> import { mapState, mapGetters, mapMutations } from 'vuex'; -import FeatureListMassToggle from '@/components/Feature/FeatureListMassToggle'; import { formatStringDate } from '@/utils'; export default { @@ -368,10 +412,13 @@ export default { }, }, - components: { - FeatureListMassToggle, + beforeRouteLeave (to, from, next) { + if (to.name !== 'editer-attribut-signalement') { + this.UPDATE_CHECKED_FEATURES([]); // empty if not needed anymore + } + next(); // continue navigation }, - + props: { paginatedFeatures: { type: Array, @@ -400,7 +447,11 @@ export default { queryparams: { type: Object, default: null, - } + }, + editAttributesFeatureType: { + type: String, + default: null, + }, }, computed: { @@ -413,6 +464,17 @@ export default { ...mapState('projects', ['project']), ...mapState('feature', ['clickedFeatures', 'massMode']), + mode: { + get() { + return this.massMode; + }, + set(newMode) { + this.TOGGLE_MASS_MODE(newMode); + this.UPDATE_CLICKED_FEATURES([]); + this.UPDATE_CHECKED_FEATURES([]); + }, + }, + userStatus() { return this.USER_LEVEL_PROJECTS[this.$route.params.slug]; }, @@ -457,17 +519,21 @@ export default { }, }, - destroyed() { - this.UPDATE_CHECKED_FEATURES([]); - }, - methods: { ...mapMutations('feature', [ 'UPDATE_CLICKED_FEATURES', 'UPDATE_CHECKED_FEATURES', + 'TOGGLE_MASS_MODE', ]), storeClickedFeature(feature) { + if (this.massMode === 'edit-attributes') { // if modifying attributes + if (this.checkedFeatures.length === 0) { // store feature type slug to restrict selection for next selected features + this.$emit('update:editAttributesFeatureType', feature.feature_type.slug); + } else if (this.checkedFeatures.length === 1 && this.checkedFeatures[0] === feature.feature_id) { + this.$emit('update:editAttributesFeatureType', null); // delete feature type slug if last checkedFeatures is unselected, to allow other types selection + } + } this.UPDATE_CLICKED_FEATURES([ ...this.clickedFeatures, { feature_id: feature.feature_id, feature_type: feature.feature_type.slug } @@ -490,7 +556,11 @@ export default { Contributeur : ['draft', 'pending', 'published'], }; - if (this.user.is_superuser) { + if (this.checkedFeatures.length > 0 && // check if selection should be restricted to a specific feature type, for attributes modification + feature.feature_type.slug !== this.editAttributesFeatureType && + this.massMode === 'edit-attributes') { + return false; + } else if (this.user.is_superuser) { return true; } else if (this.userStatus === 'Contributeur' && feature.display_creator !== `${this.user.first_name} ${this.user.last_name}`) { return false; @@ -502,20 +572,13 @@ export default { }, checkRights(feature) { - switch (this.massMode) { - case 'modify': + if (this.massMode.includes('edit')) { return this.canEditFeature(feature); - case 'delete': + } else if (this.massMode === 'delete-features') { return this.canDeleteFeature(feature); } }, - switchMode() { - this.$emit('update:mode', this.mode === 'modify' ? 'delete' : 'modify'); - this.UPDATE_CLICKED_FEATURES([]); - this.$store.commit('feature/UPDATE_CHECKED_FEATURES', []); - }, - isSortedAsc(column) { return this.sort.column === column && this.sort.ascending; }, @@ -641,8 +704,9 @@ i.icon.sort:not(.down):not(.up) { margin-bottom: 1em; } -#table-features .disabled { - opacity: .5; +/* increase contrast between available checkboxes and disabled ones */ +#table-features .ui.disabled.checkbox label::before { + background-color: #fbf5f5;; } @media only screen and (min-width: 761px) { diff --git a/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue b/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue index 2c85be503f2d7d6eddc3857c87ebe5a6f50254b1..5d2c39aac1ef890d6bbb17b0d2e572ff9b2a0493 100644 --- a/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue +++ b/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue @@ -86,11 +86,11 @@ </div> </div> <div - v-if="checkedFeatures.length > 0 && massMode === 'modify' && isOnline" + v-if="checkedFeatures.length > 0 && massMode.includes('edit') && isOnline" class="ui dropdown button compact button-hover-green tiny-margin-left" - data-tooltip="Modifier le statut des Signalements" + :data-tooltip="`Modifier le${massMode.includes('status') ? ' statut' : 's attributs'} des signalements`" data-position="bottom right" - @click="toggleModifyStatus" + @click="editFeatures" > <i class="pencil fitted icon" @@ -109,7 +109,7 @@ v-for="status in availableStatus" :key="status.value" class="item" - @click="$emit('modify-status', status.value)" + @click="$emit('edit-status', status.value)" > {{ status.name }} </span> @@ -117,7 +117,7 @@ </div> </div> <div - v-if="checkedFeatures.length > 0 && massMode === 'delete' && isOnline" + v-if="checkedFeatures.length > 0 && massMode === 'delete-features' && isOnline" class="ui button compact button-hover-red tiny-margin-left" data-tooltip="Supprimer tous les signalements sélectionnés" data-position="bottom right" @@ -235,7 +235,12 @@ export default { ...initialPagination }; } - } + }, + editAttributesFeatureType: { + type: String, + default: null, + }, + }, data() { @@ -334,11 +339,37 @@ export default { this.showModifyStatus = false; }, + editFeatures() { + switch (this.massMode) { + case 'edit-status': + this.toggleModifyStatus(); + break; + case 'edit-attributes': + this.displayAttributesForm(); + break; + } + }, + toggleModifyStatus() { this.showModifyStatus = !this.showModifyStatus; this.showAddFeature = false; }, + displayAttributesForm() { + if (this.checkedFeatures.length > 1) { + this.$router.push({ + name: 'editer-attribut-signalement', + params: { + slug_type_signal: this.editAttributesFeatureType, + }, + }); + } else { + this.$store.commit('DISPLAY_MESSAGE', { + comment: 'Veuillez sélectionner au moins 2 signalements pour l\'édition multiple d\'attributs' + }); + } + }, + clickOutsideDropdown(e) { if (!e.target.closest('#button-dropdown')) { this.showModifyStatus = false; diff --git a/src/router/index.js b/src/router/index.js index 8fe8e9d76322ca63af83c24d4c95a744059aec64..56953714fbab4848193e56d9fab1c6dcd82e8f04 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -130,6 +130,11 @@ const routes = [ name: 'editer-signalement', component: () => import('../views/Feature/FeatureEdit.vue') }, + { + path: `/${projectBase}/:slug/type-signalement/:slug_type_signal/editer-signalements-attributs/`, + name: 'editer-attribut-signalement', + component: () => import('../views/Feature/FeatureEdit.vue') + }, { path: '/projet/:slug/catalog/:feature_type_slug', diff --git a/src/store/index.js b/src/store/index.js index f5ed15a226fbf68d621784197be62e48aa7c1968..2be432d43f1c6c21dc1de190ecb8cc843cac176b 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -34,6 +34,7 @@ export default new Vuex.Store({ message: 'En cours de chargement' }, logged: false, + messageCount: 0, messages: [], reloadIntervalId: null, staticPages: null, @@ -75,16 +76,18 @@ export default new Vuex.Store({ state.levelsPermissions = levelsPermissions; }, DISPLAY_MESSAGE(state, message) { - state.messages = [message, ...state.messages]; + message['counter'] = state.messageCount; + state.messageCount += 1; + state.messages = [message, ...state.messages]; // add new message at the beginning of the list if (document.getElementById('scroll-top-anchor')) { document.getElementById('scroll-top-anchor').scrollIntoView({ block: 'start', inline: 'nearest' }); } setTimeout(() => { - state.messages = []; + state.messages = state.messages.slice(0, -1); // remove one message from the end of the list }, 3000); }, - DISCARD_MESSAGE(state, message) { - state.messages = state.messages.filter((el) => el.comment !== message.comment); + DISCARD_MESSAGE(state, messageCount) { + state.messages = state.messages.filter((mess) => mess.counter !== messageCount); }, CLEAR_MESSAGES(state) { state.messages = []; diff --git a/src/store/modules/feature.store.js b/src/store/modules/feature.store.js index 2466c7c6ad6e4b2c81188aadf9fe694b73d96e6f..7f82a9c848fcf6fa8e385472d0cdc7c5b40b073f 100644 --- a/src/store/modules/feature.store.js +++ b/src/store/modules/feature.store.js @@ -15,7 +15,7 @@ const feature = { form: null, linkedFormset: [], //* used to edit in feature_edit linked_features: [], //* used to display in feature_detail - massMode: 'modify', + massMode: 'edit-status', }, mutations: { SET_FEATURES(state, features) { @@ -174,7 +174,6 @@ const feature = { const cancelToken = axios.CancelToken.source(); commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true }); - //commit('SET_CURRENT_FEATURE', null); //? Est-ce que c'est nécessaire ? -> fait sauter l'affichage au clic sur un signalement lié (feature_detail) const url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}projects/${project_slug}/feature/?id=${feature_id}`; return axios .get(url, { cancelToken: cancelToken.token }) @@ -186,18 +185,20 @@ const feature = { return response; }) .catch((error) => { + console.error('Error while getting feature for id = ', feature_id, error); throw error; }); }, SEND_FEATURE({ state, rootState, commit, dispatch }, routeName) { - function redirect(featureId) { + function redirect(featureId, featureName, response) { + if (routeName === 'editer-attribut-signalement') return response; // exit function to avoid conflict with next feature call to GET_PROJECT_FEATURE when modifying more than 2 features commit( 'DISPLAY_MESSAGE', { comment: routeName === 'ajouter-signalement' ? 'Le signalement a été crée' : - 'Le signalement a été mis à jour', + `Le signalement ${featureName} a été mis à jour`, level: 'positive' }, { root: true }, @@ -209,7 +210,7 @@ const feature = { feature_id: featureId }) .then(() => { - if (routeName.includes('details-signalement')) return; + if (routeName === 'details-signalement') return response; router.push({ name: 'details-signalement', params: { @@ -218,24 +219,25 @@ const feature = { }, }); }); + return response; } - async function handleOtherForms(featureId) { + async function handleOtherForms(featureId, featureName, response) { await dispatch('SEND_ATTACHMENTS', featureId); await dispatch('PUT_LINKED_FEATURES', featureId); - redirect(featureId); + return redirect(featureId, featureName, response); } function createGeojson() { //* prepare feature data to send const extraFormObject = {}; //* prepare an object to be flatten in properties of geojson for (const field of state.extra_forms) { - extraFormObject[field.name] = field.value; + if (field.value !== null) extraFormObject[field.name] = field.value; } return { id: state.form.feature_id || state.currentFeature.feature_id, type: 'Feature', geometry: state.form.geometry || state.form.geom || - state.currentFeature.geometry || state.currentFeature.geom, + state.currentFeature.geometry || state.currentFeature.geom, properties: { title: state.form.title, description: state.form.description.value, @@ -264,12 +266,14 @@ const feature = { data: geojson }).then((response) => { if ((response.status === 200 || response.status === 201) && response.data) { + const featureId = response.data.id; + const featureName = response.data.properties.title; if (state.attachmentFormset.length > 0 || state.linkedFormset.length > 0 || state.attachmentsToDelete.length > 0) { - handleOtherForms(response.data.id); + return handleOtherForms(featureId, featureName, response); } else { - redirect(response.data.id); + return redirect(featureId, featureName, response); } } }) @@ -296,7 +300,7 @@ const feature = { }); } else { - console.error(error); + console.error('Error while sending feature', error); throw error; } throw error; diff --git a/src/views/Feature/FeatureEdit.vue b/src/views/Feature/FeatureEdit.vue index 35eaa28cc17c4b6e0c15fdfb9ed527e20c95f8b0..824c440563985231552addb3ec5c9f1f585280f8 100644 --- a/src/views/Feature/FeatureEdit.vue +++ b/src/views/Feature/FeatureEdit.vue @@ -1,12 +1,15 @@ <template> <div id="feature-edit"> - <h1 v-if="feature && currentRouteName === 'editer-signalement'"> - Mise à jour du signalement "{{ feature.title || feature.feature_id }}" - </h1> - <h1 - v-else-if="feature_type && currentRouteName === 'ajouter-signalement'" - > - Création d'un signalement <small>[{{ feature_type.title }}]</small> + <h1> + <span v-if="feature_type && currentRouteName === 'ajouter-signalement'"> + Création d'un signalement <small>[{{ feature_type.title }}]</small> + </span> + <span v-else-if="feature && currentRouteName === 'editer-signalement'"> + Mise à jour du signalement "{{ feature.title || feature.feature_id }}" + </span> + <span v-else-if="feature_type && currentRouteName === 'editer-attribut-signalement'"> + Mise à jour des attributs de {{ checkedFeatures.length }} signalements + </span> </h1> <form @@ -17,7 +20,10 @@ class="ui form" > <!-- Feature Fields --> - <div class="two fields"> + <div + v-if="currentRouteName !== 'editer-attribut-signalement'" + class="two fields" + > <div :class="field_title"> <label :for="form.title.id_for_label">{{ form.title.label }}</label> <input @@ -64,7 +70,10 @@ </div> </div> - <div class="field"> + <div + v-if="currentRouteName !== 'editer-attribut-signalement'" + class="field" + > <label :for="form.description.id_for_label">{{ form.description.label }}</label> @@ -77,7 +86,10 @@ </div> <!-- Geom Field --> - <div class="field"> + <div + v-if="currentRouteName !== 'editer-attribut-signalement'" + class="field" + > <label :for="form.geom.id_for_label">{{ form.geom.label }}</label> <!-- Import GeoImage --> <div @@ -274,7 +286,7 @@ </div> <!-- Pièces jointes --> - <div v-if="isOnline"> + <div v-if="isOnline && currentRouteName !== 'editer-attribut-signalement'"> <div class="ui horizontal divider"> PIÈCES JOINTES </div> @@ -304,7 +316,7 @@ </div> <!-- Signalements liés --> - <div v-if="isOnline"> + <div v-if="isOnline && currentRouteName !== 'editer-attribut-signalement'"> <div class="ui horizontal divider"> SIGNALEMENTS LIÉS </div> @@ -333,7 +345,7 @@ <button type="button" :class="['ui teal icon button', { loading: sendingFeature }]" - @click="postForm" + @click="onSave" > <i class="white save icon" @@ -439,9 +451,11 @@ export default { ]), ...mapState('feature', [ 'attachmentFormset', - 'linkedFormset', - 'features', + 'checkedFeatures', + 'currentFeature', 'extra_forms', + 'features', + 'linkedFormset', ]), ...mapState('feature-type', [ 'feature_types' @@ -520,7 +534,7 @@ export default { this.$route.params.slug_type_signal ); //* empty previous feature data, not emptying by itself since it doesn't update by itself anymore - if (this.currentRouteName === 'ajouter-signalement') { + if (this.currentRouteName === 'ajouter-signalement' || this.currentRouteName === 'editer-attribut-signalement') { this.$store.commit('feature/SET_CURRENT_FEATURE', []); } @@ -531,10 +545,13 @@ export default { }, mounted() { - const promises = [ - this.$store.dispatch('projects/GET_PROJECT', this.$route.params.slug), - this.$store.dispatch('projects/GET_PROJECT_INFO', this.$route.params.slug), - ]; + const promises = []; + if (!this.project) { + promises.push( + this.$store.dispatch('projects/GET_PROJECT', this.$route.params.slug), + this.$store.dispatch('projects/GET_PROJECT_INFO', this.$route.params.slug), + ); + } if (this.$route.params.slug_signal) { promises.push( this.$store.dispatch('feature/GET_PROJECT_FEATURE', { @@ -545,9 +562,11 @@ export default { } Promise.all(promises).then(() => { - this.initForm(); - this.initMap(); - this.onFeatureTypeLoaded(); + if (this.currentRouteName !== 'editer-attribut-signalement') { + this.initForm(); + this.initMap(); + this.onFeatureTypeLoaded(); // init map tools + } this.$store.dispatch('feature/INIT_EXTRA_FORMS'); }); }, @@ -562,7 +581,7 @@ export default { methods: { initForm() { - if (this.currentRouteName === 'editer-signalement') { + if (this.currentRouteName.includes('editer')) { for (const key in this.feature) { if (key && this.form[key]) { if (key === 'status') { @@ -770,22 +789,27 @@ export default { return isValid; }, - postForm() { - let is_valid = true; - if (!this.feature_type.title_optional) { - is_valid = - this.checkFormTitle() && - this.checkFormGeom() && - this.checkAddedForm(); + onSave() { + if (this.currentRouteName === 'editer-attribut-signalement') { + this.postMultipleFeatures(); } else { - is_valid = this.checkFormGeom() && this.checkAddedForm(); + this.postForm(); } + }, + + async postForm() { + let is_valid = true; + let response; + is_valid = + this.checkFormGeom() && + this.checkAddedForm(); + if (!this.feature_type.title_optional) is_valid = this.checkFormTitle() && is_valid; if (is_valid) { //* in a moderate project, at edition of a published feature by someone else than admin or moderator, switch published status to draft. if ( this.project.moderation && - this.currentRouteName === 'editer-signalement' && + this.currentRouteName.includes('editer') && this.form.status.value.value === 'published' && !this.permissions.is_project_administrator && !this.permissions.is_project_moderator @@ -794,9 +818,58 @@ export default { this.updateStore(); } this.sendingFeature = true; - this.$store.dispatch('feature/SEND_FEATURE', this.currentRouteName) - .then(() => this.sendingFeature = false); + response = await this.$store.dispatch('feature/SEND_FEATURE', this.currentRouteName); + this.sendingFeature = false; + return response; + } + }, + + async postMultipleFeatures() { + this.$store.commit('DISPLAY_LOADER', 'Envoi des signalements en cours...'); + const extraForms = [...this.extra_forms];// store extra forms for multiple features to not be overide by current feature + let results = []; + for (const featureId of this.checkedFeatures) { + const response = await this.$store.dispatch('feature/GET_PROJECT_FEATURE', { + project_slug: this.$route.params.slug, + feature_id: featureId, + }); + if (response.status === 200) { + this.initForm(); // fill title, status, description needed to send request + for (let xtraForm of extraForms) { // fill extra forms with features values, only if the value of the extra form for multiple features is null + if (xtraForm.value === null) { // if no value to overide in feature, keep the feature value + xtraForm['value'] = this.feature.feature_data.find((feat) => feat.label === xtraForm.label).value; + await this.$store.commit('feature/UPDATE_EXTRA_FORM', xtraForm); + } + } + const result = await this.postForm(); + results.push(result); + } } + this.$store.commit('DISCARD_LOADER'); + const errors = results.filter((res) => res === undefined || res.status !== 200); + if (errors.length > 0) { + this.$store.commit( + 'DISPLAY_MESSAGE', + { + comment: 'Des signalements n\'ont pas pu être mis à jour', + level: 'negative' + }, + ); + } else { + this.$store.commit( + 'DISPLAY_MESSAGE', + { + comment: 'Les signalements ont été mis à jour', + level: 'positive' + }, + ); + } + this.$router.push({ + name: 'liste-signalements', + params: { + slug: this.$route.params.slug, + }, + }); }, //* ************* MAP *************** *// @@ -847,9 +920,9 @@ export default { } }, - updateGeomField(newGeom) { + async updateGeomField(newGeom) { this.form.geom.value = newGeom; - this.updateStore(); + await this.updateStore(); }, initMap() { diff --git a/src/views/Project/FeaturesListAndMap.vue b/src/views/Project/FeaturesListAndMap.vue index ebebfcb555d6d666a481aa73b4746ac7cf3ec9f1..fd67094f18b6eb966acf80b64f9fd4dc60007f3b 100644 --- a/src/views/Project/FeaturesListAndMap.vue +++ b/src/views/Project/FeaturesListAndMap.vue @@ -5,11 +5,12 @@ :show-map="showMap" :features-count="featuresCount" :pagination="pagination" + :edit-attributes-feature-type="editAttributesFeatureType" @set-filter="setFilters" @reset-pagination="resetPagination" @fetch-features="fetchPagedFeatures" @show-map="setShowMap" - @modify-status="modifyStatus" + @edit-status="modifyStatus" @toggle-delete-modal="toggleDeleteModal" /> @@ -48,6 +49,7 @@ :features-count="featuresCount" :pagination="pagination" :sort="sort" + :edit-attributes-feature-type.sync="editAttributesFeatureType" :queryparams="queryparams" @update:page="handlePageChange" @update:sort="handleSortChange" @@ -125,6 +127,7 @@ export default { data() { return { + editAttributesFeatureType: null, currentLayer: null, featuresCount: 0, form: { @@ -192,6 +195,7 @@ export default { }, mounted() { + this.UPDATE_CHECKED_FEATURES([]); // empty for when turning back from edit attributes page if (!this.project) { // Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh Promise.all([ @@ -224,7 +228,7 @@ export default { setShowMap(newValue) { this.showMap = newValue; //* expanded sidebar is visible under the list, even when the map is closed (position:absolute), solved by closing it whin switching to list - if (newValue === false) this.$refs.sidebar.toggleSidebar(false); + if (newValue === false && this.$refs.sidebar) this.$refs.sidebar.toggleSidebar(false); }, resetPagination() { this.pagination = { ...initialPagination }; @@ -299,6 +303,10 @@ export default { this.toggleDeleteModal(); }, + modifyFeaturesAttributes() { + console.log('modifyFeaturesAttributes'); + }, + onFilterChange() { if (mapService.getMap() && mapService.mvtLayer) { mapService.mvtLayer.changed();