diff --git a/package.json b/package.json index abb7e0b31723d7495246cdd21575cbf8367c9bcf..0670fac7cdeb2eb7f04b5313a693450f2bee9eda 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "geocontrib-frontend", - "version": "3.0.1", + "version": "3.0.2", "private": true, "scripts": { "serve": "npm run init-proxy & npm run init-serve", - "init-proxy": "lcp --proxyUrl http://localhost:8000 --origin http://localhost:8080 --proxyPartial ''", + "init-proxy": "lcp --proxyUrl http://127.0.0.1:8000 --origin http://localhost:8080 --proxyPartial ''", "init-serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" diff --git a/src/App.vue b/src/App.vue index af053b2d87e67b5a9deebc44bcec4e30cd73fdf9..1407a94c71a83f1ea2eee96d9dab62ba4854296a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -65,7 +65,7 @@ <router-link v-if=" - project && + project && isOnline && (user.is_administrator || user.is_superuser || isAdmin) " :to="{ @@ -78,7 +78,7 @@ </router-link> <router-link v-if=" - project && + project && isOnline && (user.is_administrator || user.is_superuser || isAdmin) " :to="{ @@ -92,8 +92,12 @@ <div class="mobile"> <router-link + :is="isOnline ? 'router-link' : 'span'" v-if="user" - :to="{name: 'my_account', params: { slug: $route.params.slug ? $route.params.slug : '-' }}" + :to="{ + name: 'my_account', + params: { slug: isSharedProject && $route.params.slug ? $route.params.slug : null } + }" class="item" > {{ userFullname || user.username || "Utilisateur inconnu" }} @@ -137,7 +141,7 @@ </div> </div> </div> - <div class="desktop flex push-right-desktop item title"> + <div class="desktop flex push-right-desktop item title abstract"> <span> {{ APPLICATION_ABSTRACT }} </span> @@ -145,8 +149,12 @@ <div class="desktop flex push-right-desktop"> <router-link + :is="isOnline ? 'router-link' : 'span'" v-if="user" - :to="{name: 'my_account', params: { slug: $route.params.slug ? $route.params.slug : '-' }}" + :to="{ + name: 'my_account', + params: { slug: isSharedProject && $route.params.slug ? $route.params.slug : null } + }" class="item" > {{ userFullname || user.username || "Utilisateur inconnu" }} @@ -291,6 +299,9 @@ export default { 'projects', 'project', ]), + ...mapState([ + 'isOnline', + ]), APPLICATION_NAME() { return this.configuration.VUE_APP_APPLICATION_NAME; }, @@ -394,7 +405,13 @@ footer { z-index: 1001; } -@media screen and (min-width: 560px) { +@media screen and (max-width: 985px) { + .abstract{ + display: none !important; + } +} + +@media screen and (min-width: 590px) { .mobile { display: none !important; } @@ -409,7 +426,7 @@ footer { } } -@media screen and (max-width: 560px) { +@media screen and (max-width: 590px) { .desktop { display: none !important; } diff --git a/src/assets/styles/base.css b/src/assets/styles/base.css index 5e5a50703d8e64157be1f60c5f476588c9a67753..f77d223bc0e28657f22ab8017e15c4ed3f490920 100644 --- a/src/assets/styles/base.css +++ b/src/assets/styles/base.css @@ -32,6 +32,12 @@ main { .important-flex { display: flex !important; } +.pointer:hover { + cursor: pointer; +} +.dimmer-anchor { + position: relative; +} /* ---------------------------------- */ /* MAIN */ /* ---------------------------------- */ diff --git a/src/components/Dropdown.vue b/src/components/Dropdown.vue index d658cb72f7df541516c86220855f4c620e50d6bc..0fa9ba3d802b323bae63aa1286874cd830e7a27e 100644 --- a/src/components/Dropdown.vue +++ b/src/components/Dropdown.vue @@ -125,11 +125,10 @@ export default { methods: { toggleDropdown(val) { - if (this.isOpen) { - this.input = ''; // * clear input field when closing dropdown - } else if (this.search) { - //* focus on input if is a search dropdown - this.$refs.input.focus({ + if (this.isOpen) { //* if dropdown is open : + this.input = ''; // * -> clear input field when closing dropdown + } else if (this.search) { //* if dropdown is closed is a search dropdown: + this.$refs.input.focus({ //* -> focus on input field preventScroll: true, }); } else if (this.clearable && val.target && this.selected) { @@ -172,8 +171,9 @@ export default { }, clickOutsideDropdown(e) { - if (!e.target.closest(`#custom-dropdown${this.identifier}`)) + if (!e.target.closest(`#custom-dropdown${this.identifier}`) && this.isOpen) { this.toggleDropdown(false); + } }, }, }; diff --git a/src/components/Pagination.vue b/src/components/Pagination.vue index d0f730bab725b56ccaab46e4c02835e7c197da65..2afc85dfff08547813bd8d477c290efe1632ddd3 100644 --- a/src/components/Pagination.vue +++ b/src/components/Pagination.vue @@ -8,7 +8,7 @@ > <a class="page-link" - href="#" + :href="currentLocation" @click="page -= 1" > <i class="ui icon big angle left" /> @@ -26,7 +26,7 @@ > <a class="page-link" - href="#" + :href="currentLocation" @click="changePage(index)" > {{ index }} @@ -45,7 +45,7 @@ > <a class="page-link" - href="#" + :href="currentLocation" @click="page = index" > {{ index }} @@ -58,7 +58,7 @@ > <a class="page-link" - href="#" + :href="currentLocation" @click="page += 1" > <i class="ui icon big angle right" /> @@ -89,7 +89,8 @@ export default { data() { return { - page: 1 + page: 1, + currentLocation: window.location.origin + window.location.pathname + '#', }; }, diff --git a/src/components/SearchFeature.vue b/src/components/SearchFeature.vue index b0a661a24ab82eee7bc16852fbd6c671473c65cc..c1692eba7482fd18b6cc82ed8203bd4400e07278 100644 --- a/src/components/SearchFeature.vue +++ b/src/components/SearchFeature.vue @@ -12,7 +12,7 @@ selected-label="" deselect-label="" :searchable="true" - :placeholder="'Rechercher un signalement ...'" + :placeholder="placeholder" :show-no-results="true" :loading="loading" :clear-on-select="false" @@ -52,12 +52,20 @@ export default { Multiselect }, + props: { + currentSelection : { + type: Object, + default: null, + } + }, + data() { return { loading: false, selection: null, text: null, - results: [] + results: [], + placeholder: 'Rechercher un signalement ...' }; }, @@ -91,6 +99,12 @@ export default { this.RESET_CANCELLABLE_SEARCH_REQUEST(); }, + mounted() { + if (this.currentSelection && this.currentSelection.feature_to) { + this.placeholder = this.currentSelection.feature_to.title; + } + }, + methods: { ...mapMutations(['RESET_CANCELLABLE_SEARCH_REQUEST']), ...mapActions('feature', [ @@ -109,6 +123,19 @@ export default { }; </script> -<style scoped> +<style> +.multiselect input { + line-height: 1em !important; + padding: 0 !important; +} + +.multiselect__placeholder { + margin: 0; + padding: 0; +} +.multiselect__input { + min-height: auto !important; + line-height: 1em !important; +} </style> diff --git a/src/components/feature/FeatureLinkedForm.vue b/src/components/feature/FeatureLinkedForm.vue index 09f5686322a21c53c5d2ddf82fd8e7911add0eb2..c19199eb7fa08e26a1be83a5c51271754912a736 100644 --- a/src/components/feature/FeatureLinkedForm.vue +++ b/src/components/feature/FeatureLinkedForm.vue @@ -24,7 +24,7 @@ <div class="visible-fields"> <div class="two fields"> <div class="required field"> - <label for="form.relation_type.id_for_label">{{ + <label :for="form.relation_type.id_for_label">{{ form.relation_type.label }}</label> <Dropdown @@ -35,10 +35,11 @@ {{ form.relation_type.errors }} </div> <div class="required field"> - <label for="form.feature_to.id_for_label">{{ + <label :for="form.feature_to.id_for_label">{{ form.feature_to.label }}</label> <SearchFeature + :current-selection="linkedForm" @select="selectFeatureTo" @close="selectFeatureTo" /> @@ -90,10 +91,7 @@ export default { }, html_name: 'feature_to', label: 'Signalement lié', - value: { - name: '', - value: '', - }, + value: '', }, }, relationTypeChoices: [ @@ -106,7 +104,6 @@ export default { }, computed: { - selected_relation_type: { // getter get() { @@ -122,7 +119,11 @@ export default { mounted() { if (this.linkedForm.relation_type) { - this.getExistingRelation_type(); + this.form.relation_type.value.name = this.linkedForm.relation_type_display; + this.form.relation_type.value.value = this.linkedForm.relation_type; + } + if (this.linkedForm.feature_to) { + this.form.feature_to.value = this.linkedForm.feature_to.feature_id; } }, @@ -137,8 +138,11 @@ export default { this.$store.commit('feature/REMOVE_LINKED_FORM', this.linkedForm.dataKey); }, - selectFeatureTo(e) { - this.form.feature_to.value = e; + selectFeatureTo(feature) { + if (feature && feature.feature_id) { + this.form.feature_to.value = feature.feature_id; + this.updateStore(); + } }, updateStore() { @@ -146,7 +150,7 @@ export default { dataKey: this.linkedForm.dataKey, relation_type: this.form.relation_type.value.value, feature_to: { - feature_id: this.form.feature_to.value.value, + feature_id: this.form.feature_to.value, }, }); }, @@ -154,7 +158,7 @@ export default { checkForm() { if (this.form.feature_to.value === '') { this.form.errors = [ - '<strong>Choisir un signalement lié</strong><br/> Pourriez-vous choisir un signalement pour la nouvelle liaison ?', + 'Veuillez choisir un signalement pour la nouvelle liaison.', ]; document .getElementById('errorlist-links') @@ -164,24 +168,6 @@ export default { this.form.errors = []; return true; }, - - getExistingFeature_to(featureOptions) { - const feature_to = featureOptions.find( - (el) => el.value === this.linkedForm.feature_to.feature_id - ); - if (feature_to) { - this.form.feature_to.value = feature_to; - } - }, - - getExistingRelation_type() { - const relation_type = this.relationTypeChoices.find( - (el) => el.value === this.linkedForm.relation_type - ); - if (relation_type) { - this.form.relation_type.value = relation_type; - } - }, }, }; </script> \ No newline at end of file diff --git a/src/components/feature/FeatureListMassToggle.vue b/src/components/feature/FeatureListMassToggle.vue new file mode 100644 index 0000000000000000000000000000000000000000..02aaa775df28a1cb77c6dbbf47ff1111b31a5e16 --- /dev/null +++ b/src/components/feature/FeatureListMassToggle.vue @@ -0,0 +1,48 @@ +<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'}]" /></div> + <span class="grey">| </span> + <div><i :class="['icon trash', {disabled: massMode !== 'delete'}]" /></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> \ No newline at end of file diff --git a/src/components/feature/FeatureListTable.vue b/src/components/feature/FeatureListTable.vue index d0a0ad5a90ac960398790d161f4b7dae86dbae5b..e892519aba5b3f83ce0ea1a3a27770028f3cc58f 100644 --- a/src/components/feature/FeatureListTable.vue +++ b/src/components/feature/FeatureListTable.vue @@ -1,322 +1,320 @@ <template> - <div - data-tab="list" - class="dataTables_wrapper no-footer" - > - <table - id="table-features" - class="ui compact table dataTable" + <div> + <div class="table-mobile-buttons left-align"> + <FeatureListMassToggle /> + </div> + <div + data-tab="list" + class="dataTables_wrapper no-footer" > - <thead> - <tr> - <th class="dt-center"> - <div - class="switch-buttons pointer" - :data-tooltip="`Passer en mode ${mode === 'modify' ? 'suppression':'édition'}`" - @click="switchMode" - > - <div><i :class="['icon pencil', {disabled: mode !== 'modify'}]" /></div> - <span class="grey">| </span> - <div><i :class="['icon trash', {disabled: mode !== 'delete'}]" /></div> - </div> - </th> - - <th class="dt-center"> - <div - class="pointer" - @click="changeSort('status')" - > - Statut - <i - :class="{ - down: isSortedAsc('status'), - up: isSortedDesc('status'), - }" - class="icon sort" - /> - </div> - </th> - <th class="dt-center"> - <div - class="pointer" - @click="changeSort('feature_type')" - > - Type - <i - :class="{ - down: isSortedAsc('feature_type'), - up: isSortedDesc('feature_type'), - }" - class="icon sort" - /> - </div> - </th> - <th class="dt-center"> - <div - class="pointer" - @click="changeSort('title')" + <table + id="table-features" + class="ui compact table unstackable dataTable" + > + <thead> + <tr> + <th class="dt-center"> + <FeatureListMassToggle /> + </th> + + <th class="dt-center"> + <div + class="pointer" + @click="changeSort('status')" + > + Statut + <i + :class="{ + down: isSortedAsc('status'), + up: isSortedDesc('status'), + }" + class="icon sort" + /> + </div> + </th> + <th class="dt-center"> + <div + class="pointer" + @click="changeSort('feature_type')" + > + Type + <i + :class="{ + down: isSortedAsc('feature_type'), + up: isSortedDesc('feature_type'), + }" + class="icon sort" + /> + </div> + </th> + <th class="dt-center"> + <div + class="pointer" + @click="changeSort('title')" + > + Nom + <i + :class="{ + down: isSortedAsc('title'), + up: isSortedDesc('title'), + }" + class="icon sort" + /> + </div> + </th> + <th class="dt-center"> + <div + class="pointer" + @click="changeSort('updated_on')" + > + Dernière modification + <i + :class="{ + down: isSortedAsc('updated_on'), + up: isSortedDesc('updated_on'), + }" + class="icon sort" + /> + </div> + </th> + <th + v-if="user" + class="dt-center" > - Nom - <i - :class="{ - down: isSortedAsc('title'), - up: isSortedDesc('title'), - }" - class="icon sort" - /> - </div> - </th> - <th class="dt-center"> - <div - class="pointer" - @click="changeSort('updated_on')" + <div + class="pointer" + @click="changeSort('display_creator')" + > + Auteur + <i + :class="{ + down: isSortedAsc('display_creator'), + up: isSortedDesc('display_creator'), + }" + class="icon sort" + /> + </div> + </th> + <th + v-if="user" + class="dt-center" > - Dernière modification - <i - :class="{ - down: isSortedAsc('updated_on'), - up: isSortedDesc('updated_on'), - }" - class="icon sort" - /> - </div> - </th> - <th - v-if="user" - class="dt-center" + <div + class="pointer" + @click="changeSort('display_last_editor')" + > + Dernier éditeur + <i + :class="{ + down: isSortedAsc('display_last_editor'), + up: isSortedDesc('display_last_editor'), + }" + class="icon sort" + /> + </div> + </th> + </tr> + </thead> + <tbody> + <tr + v-for="(feature, index) in paginatedFeatures" + :key="index" > - <div - class="pointer" - @click="changeSort('display_creator')" - > - Auteur - <i - :class="{ - down: isSortedAsc('display_creator'), - up: isSortedDesc('display_creator'), + <td class="dt-center"> + <div + :class="['ui checkbox', {disabled: !checkRights(feature)}]" + > + <input + :id="feature.id" + v-model="checked" + type="checkbox" + :value="feature.id" + :disabled="!checkRights(feature)" + name="select" + @input="storeClickedFeature(feature)" + > + <label for="select" /> + </div> + </td> + + <td class="dt-center"> + <div v-if="feature.properties.status.value === 'archived'"> + <span data-tooltip="Archivé"> + <i class="grey archive icon" /> + </span> + </div> + <div v-else-if="feature.properties.status.value === 'pending'"> + <span data-tooltip="En attente de publication"> + <i class="teal hourglass outline icon" /> + </span> + </div> + <div v-else-if="feature.properties.status.value === 'published'"> + <span data-tooltip="Publié"> + <i class="olive check icon" /> + </span> + </div> + <div v-else-if="feature.properties.status.value === 'draft'"> + <span data-tooltip="Brouillon"> + <i class="orange pencil alternate icon" /> + </span> + </div> + </td> + <td class="dt-center"> + <router-link + :to="{ + name: 'details-type-signalement', + params: { + feature_type_slug: feature.properties.feature_type.slug, + }, }" - class="icon sort" - /> - </div> - </th> - <th - v-if="user" - class="dt-center" - > - <div - class="pointer" - @click="changeSort('display_last_editor')" - > - Dernier éditeur - <i - :class="{ - down: isSortedAsc('display_last_editor'), - up: isSortedDesc('display_last_editor'), + > + {{ feature.properties.feature_type.title }} + </router-link> + </td> + <td class="dt-center"> + <router-link + :to="{ + name: 'details-signalement', + params: { + slug_type_signal: feature.properties.feature_type.slug, + slug_signal: feature.properties.slug || feature.id, + }, }" - class="icon sort" - /> - </div> - </th> - </tr> - </thead> - <tbody> - <tr - v-for="(feature, index) in paginatedFeatures" - :key="index" - > - <td class="dt-center"> - <div - :class="['ui checkbox', {disabled: !checkRights(feature)}]" - > - <input - :id="feature.id" - v-model="checked" - type="checkbox" - :value="feature.id" - :disabled="!checkRights(feature)" - name="select" - @input="storeClickedFeature(feature)" > - <label for="select" /> - </div> - </td> - - <td class="dt-center"> - <div - v-if="feature.properties.status.value === 'archived'" - data-tooltip="Archivé" - > - <i class="grey archive icon" /> - </div> - <div - v-else-if="feature.properties.status.value === 'pending'" - data-tooltip="En attente de publication" - > - <i class="teal hourglass outline icon" /> - </div> - <div - v-else-if="feature.properties.status.value === 'published'" - data-tooltip="Publié" - > - <i class="olive check icon" /> - </div> - <div - v-else-if="feature.properties.status.value === 'draft'" - data-tooltip="Brouillon" - > - <i class="orange pencil alternate icon" /> - </div> - </td> - <td class="dt-center"> - <router-link - :to="{ - name: 'details-type-signalement', - params: { - feature_type_slug: feature.properties.feature_type.slug, - }, - }" + {{ getFeatureDisplayName(feature) }} + </router-link> + </td> + <td class="dt-center"> + {{ feature.properties.updated_on }} + </td> + <td + v-if="user" + class="dt-center" > - {{ feature.properties.feature_type.title }} - </router-link> - </td> - <td class="dt-center"> - <router-link - :to="{ - name: 'details-signalement', - params: { - slug_type_signal: feature.properties.feature_type.slug, - slug_signal: feature.properties.slug || feature.id, - }, - }" + {{ getUserName(feature) }} + </td> + <td + v-if="user" + class="dt-center" > - {{ getFeatureDisplayName(feature) }} - </router-link> - </td> - <td class="dt-center"> - {{ feature.properties.updated_on }} - </td> - <td - v-if="user" - class="dt-center" + {{ feature.properties.display_last_editor }} + </td> + </tr> + <tr + v-if="featuresCount === 0" + class="odd" > - {{ getUserName(feature) }} - </td> - <td - v-if="user" - class="dt-center" - > - {{ feature.properties.display_last_editor }} - </td> - </tr> - <tr - v-if="featuresCount === 0" - class="odd" - > - <td - colspan="5" - class="dataTables_empty" - valign="top" - > - Aucune donnée disponible - </td> - </tr> - </tbody> - </table> - <div - v-if="pageNumbers.length > 1" - id="table-features_info" - class="dataTables_info" - role="status" - aria-live="polite" - > - Affichage de l'élément {{ pagination.start + 1 }} à - {{ displayedPageEnd }} - sur {{ featuresCount }} éléments - </div> - <div - v-if="pageNumbers.length > 1" - id="table-features_paginate" - class="dataTables_paginate paging_simple_numbers" - > - <a - id="table-features_previous" - :class="[ - 'paginate_button previous', - { disabled: pagination.currentPage === 1 }, - ]" - aria-controls="table-features" - data-dt-idx="0" - tabindex="0" - @click="$emit('update:page', 'previous')" - >Précédent</a> - <span> - <span v-if="pagination.currentPage >= 5"> - <a - key="page1" - class="paginate_button" - aria-controls="table-features" - data-dt-idx="1" - tabindex="0" - @click="$emit('update:page', 1)" - >{{ 1 }}</a> - <span class="ellipsis">…</span> - </span> + <td + colspan="5" + class="dataTables_empty" + valign="top" + > + Aucune donnée disponible + </td> + </tr> + </tbody> + </table> + <div + v-if="pageNumbers.length > 1" + id="table-features_info" + class="dataTables_info" + role="status" + aria-live="polite" + > + Affichage de l'élément {{ pagination.start + 1 }} à + {{ displayedPageEnd }} + sur {{ featuresCount }} éléments + </div> + <div + v-if="pageNumbers.length > 1" + id="table-features_paginate" + class="dataTables_paginate paging_simple_numbers" + > <a - v-for="pageNumber in displayedPageNumbers" - :key="'page' + pageNumber" + id="table-features_previous" :class="[ - 'paginate_button', - { current: pageNumber === pagination.currentPage }, + 'paginate_button previous', + { disabled: pagination.currentPage === 1 }, ]" aria-controls="table-features" - data-dt-idx="1" + data-dt-idx="0" tabindex="0" - @click="$emit('update:page', pageNumber)" - >{{ pageNumber }}</a> - <span v-if="(lastPageNumber - pagination.currentPage) >= 4"> - <span class="ellipsis">…</span> + @click="$emit('update:page', 'previous')" + >Précédent</a> + <span> + <span v-if="pagination.currentPage >= 5"> + <a + key="page1" + class="paginate_button" + aria-controls="table-features" + data-dt-idx="1" + tabindex="0" + @click="$emit('update:page', 1)" + >{{ 1 }}</a> + <span class="ellipsis">…</span> + </span> <a - :key="'page' + lastPageNumber" - class="paginate_button" + v-for="pageNumber in displayedPageNumbers" + :key="'page' + pageNumber" + :class="[ + 'paginate_button', + { current: pageNumber === pagination.currentPage }, + ]" aria-controls="table-features" data-dt-idx="1" tabindex="0" - @click="$emit('update:page', lastPageNumber)" - >{{ lastPageNumber }}</a> + @click="$emit('update:page', pageNumber)" + >{{ pageNumber }}</a> + <span v-if="(lastPageNumber - pagination.currentPage) >= 4"> + <span class="ellipsis">…</span> + <a + :key="'page' + lastPageNumber" + class="paginate_button" + aria-controls="table-features" + data-dt-idx="1" + tabindex="0" + @click="$emit('update:page', lastPageNumber)" + >{{ lastPageNumber }}</a> + </span> </span> - </span> - <a - id="table-features_next" - :class="[ - 'paginate_button next', - { disabled: pagination.currentPage === pageNumbers.length }, - ]" - aria-controls="table-features" - data-dt-idx="7" - tabindex="0" - @click="$emit('update:page', 'next')" - >Suivant</a> + <a + id="table-features_next" + :class="[ + 'paginate_button next', + { disabled: pagination.currentPage === pageNumbers.length }, + ]" + aria-controls="table-features" + data-dt-idx="7" + tabindex="0" + @click="$emit('update:page', 'next')" + >Suivant</a> + </div> </div> </div> </template> <script> +import { mapState, mapGetters, mapMutations } from 'vuex'; +import FeatureListMassToggle from '@/components/feature/FeatureListMassToggle'; -import { mapState, mapGetters } from 'vuex'; export default { name: 'FeatureListTable', + components: { + FeatureListMassToggle, + }, + props: { paginatedFeatures: { type: Array, default: null, }, - checkedFeatures: { + pageNumbers: { type: Array, default: null, }, - clickedFeatures: { + checkedFeatures: { type: Array, default: null, }, @@ -332,16 +330,13 @@ export default { type: Object, default: null, }, - mode: { - type: String, - default: null, - } }, computed: { ...mapGetters(['permissions']), ...mapState(['user', 'USER_LEVEL_PROJECTS']), ...mapState('projects', ['project']), + ...mapState('feature', ['clickedFeatures', 'massMode']), userStatus() { return this.USER_LEVEL_PROJECTS[this.$route.params.slug]; @@ -362,16 +357,6 @@ export default { : this.pagination.end; }, - pageNumbers() { - const totalPages = Math.ceil( - this.featuresCount / this.pagination.pagesize - ); - return [...Array(totalPages).keys()].map((pageNumb) => { - ++pageNumb; - return pageNumb; - }); - }, - lastPageNumber() { return this.pageNumbers.slice(-1)[0]; }, @@ -396,12 +381,20 @@ export default { }, destroyed() { - this.$store.commit('feature/UPDATE_CHECKED_FEATURES', []); + this.UPDATE_CHECKED_FEATURES([]); }, methods: { + ...mapMutations('feature', [ + 'UPDATE_CLICKED_FEATURES', + 'UPDATE_CHECKED_FEATURES', + ]), + storeClickedFeature(feature) { - this.$emit('update:clickedFeatures', [...this.clickedFeatures, { feature_id: feature.id, feature_type: feature.properties.feature_type.slug }]); + this.UPDATE_CLICKED_FEATURES([ + ...this.clickedFeatures, + { feature_id: feature.id, feature_type: feature.properties.feature_type.slug } + ]); }, canDeleteFeature(feature) { @@ -428,7 +421,7 @@ export default { }, checkRights(feature) { - switch (this.mode) { + switch (this.massMode) { case 'modify': return this.canEditFeature(feature); case 'delete': @@ -436,12 +429,6 @@ export default { } }, - switchMode() { - this.$emit('update:mode', this.mode === 'modify' ? 'delete' : 'modify'); - this.$emit('update:clickedFeatures', []); - this.$store.commit('feature/UPDATE_CHECKED_FEATURES', []); - }, - getUserName(feature) { if (!feature.properties.creator) { return ' ---- '; @@ -565,15 +552,6 @@ table.dataTable th.dt-center, table.dataTable td.dt-center, table.dataTable td.d i.icon.sort:not(.down):not(.up) { color: rgb(220, 220, 220); } -.pointer:hover { - cursor: pointer; -} - -.switch-buttons { - display: flex; - justify-content: center; - align-items: baseline; -} .grey { color: #bbbbbb; @@ -582,13 +560,28 @@ i.icon.sort:not(.down):not(.up) { .ui.dropdown .menu .left.menu, .ui.dropdown > .left.menu .menu { margin-right: 0 !important; } + +.table-mobile-buttons { + margin-bottom: 1em; +} +@media only screen and (min-width: 761px) { + .table-mobile-buttons { + display: none !important; + } +} /* Max width before this PARTICULAR table gets nasty This query will take effect for any screen smaller than 760px and also iPads specifically. */ -@media only screen and (max-width: 760px), - (min-device-width: 768px) and (max-device-width: 1024px) { +@media only screen and (max-width: 760px) { + .table-mobile-buttons { + display: flex !important; + } + /* hide table border */ + .ui.table { + border: none !important; + } /* Force table to not be like tables anymore */ table, thead, @@ -606,8 +599,12 @@ and also iPads specifically. left: -9999px; } - tr { + tr { /* style as a card */ border: 1px solid #ccc; + border-radius: 7px; + margin-bottom: 3vh; + padding: 0 1vw .5em 1vw; + box-shadow: rgba(50, 50, 50, 0.1) 2px 5px 10px ; } td { @@ -617,7 +614,19 @@ and also iPads specifically. position: relative; padding-left: 50%; } - + .ui.table tr td { + border-top: none; + } + .ui.compact.table td { + padding: .2em; + } + td:nth-of-type(1) { + border: none !important; + padding: .25em !important; + } + td:nth-of-type(7) { + border-bottom: none !important; + } td:before { /* Now like a table header */ position: absolute; @@ -628,7 +637,6 @@ and also iPads specifically. padding-right: 10px; white-space: nowrap; } - /* Label the data */ @@ -650,20 +658,29 @@ and also iPads specifically. td:nth-of-type(6):before { content: "Auteur"; } + td:nth-of-type(7):before { + content: "Dernier éditeur"; + } table.dataTable th.dt-center, table.dataTable td.dt-center, table.dataTable td.dataTables_empty { text-align: right; } - #table-features { - margin-left: 1em; - width: calc(100% - 1em); - } - .ui.checkbox { position: absolute; - left: -1.75em; - top: 5em; + left: calc(-1vw - .75em); + top: -.75em; + } + .dataTables_wrapper .dataTables_info, + .dataTables_wrapper .dataTables_paginate { + width: 100%; + text-align: center; + margin: .5em 0; + } +} +@media only screen and (max-width: 410px) { + .ui.table tr td { + border: none; } } </style> \ No newline at end of file diff --git a/src/components/map-layers/SidebarLayers.vue b/src/components/map-layers/SidebarLayers.vue index 2e9cc9d64c89a85ea6897286b05225b44d926502..4ea8b410fad28f2ab7d7c007560ab60fda83e213 100644 --- a/src/components/map-layers/SidebarLayers.vue +++ b/src/components/map-layers/SidebarLayers.vue @@ -1,5 +1,8 @@ <template> - <div :class="['sidebar-container', { expanded }]"> + <div + v-if="isOnline" + :class="['sidebar-container', { expanded }]" + > <!-- <div class="sidebar-layers"></div> --> <div class="layers-icon" @@ -132,7 +135,12 @@ export default { }, computed: { - ...mapState('map', ['availableLayers']), + ...mapState([ + 'isOnline', + ]), + ...mapState('map', [ + 'availableLayers' + ]), }, mounted() { diff --git a/src/main.js b/src/main.js index 2aaa4ad7377d468b804c8deb1513ee05de3a0e6a..c8dd8f15eafac74b7b953c484289b8d7f401381b 100644 --- a/src/main.js +++ b/src/main.js @@ -38,6 +38,9 @@ if(navigator.serviceWorker){ let onConfigLoaded = function(config){ store.commit('SET_CONFIG', config); + setInterval(() => { //* check if navigator is online + store.commit('SET_IS_ONLINE', navigator.onLine); + }, 1000); // set title and favico document.title= config.VUE_APP_APPLICATION_NAME+' '+config.VUE_APP_APPLICATION_ABSTRACT; diff --git a/src/service-worker.js b/src/service-worker.js index 1b04819c7a0f9e3a915b944b3b5e3d53954d0d27..ae6b3aff91d2052812dc4111ff512321d3b33870 100644 --- a/src/service-worker.js +++ b/src/service-worker.js @@ -29,10 +29,15 @@ if (workbox) { new RegExp('.*/api/.*'), new workbox.strategies.NetworkFirst({ cacheName: 'api', + plugins: [ + new workbox.cacheableResponse.Plugin({ + statuses: [0, 200], + }), + ], }) ); workbox.routing.registerRoute( - /^https:\/\/c\.tile\.openstreetmap\.fr/, + /^https:\/\/[a-zA-Z]\.tile\.openstreetmap\.fr/, new workbox.strategies.CacheFirst({ cacheName: 'osm', plugins: [ @@ -46,6 +51,21 @@ if (workbox) { ], }) ); + workbox.routing.registerRoute( + new RegExp('.*/service=WMS&request=GetMap/.*'), + new workbox.strategies.CacheFirst({ + cacheName: 'wms', + plugins: [ + new workbox.cacheableResponse.Plugin({ + statuses: [0, 200], + }), + new workbox.expiration.Plugin({ + maxAgeSeconds: 60 * 60 * 24 * 365, + // maxEntries: 30, pour limiter le nombre d'entrée dans le cache + }), + ], + }) + ); } diff --git a/src/services/feature-api.js b/src/services/feature-api.js index a2278387d8e0f5c1f1bd980a3e133d899253d710..690df05e61decf02e2d0749458d73350a13f5d84 100644 --- a/src/services/feature-api.js +++ b/src/services/feature-api.js @@ -62,6 +62,55 @@ const featureAPI = { } }, + async getFeatureLinks(featureId) { + const response = await axios.get( + `${baseUrl}features/${featureId}/feature-links/` + ); + if ( + response.status === 200 && + response.data + ) { + return response.data; + } else { + return null; + } + }, + + async getFeaturesBlob(url) { + const response = await axios + .get(url, { responseType: 'blob' }); + if ( + response.status === 200 && + response.data + ) { + return response.data; + } else { + return null; + } + }, + // todo : fonction pour faire un post ou un put du signalement + + + async postOrPutFeature({ method, feature_id, feature_type__slug, project__slug, data }) { + let url = `${baseUrl}features/`; + if (method === 'PUT') { + url += `${feature_id}/? + feature_type__slug=${feature_type__slug} + &project__slug=${project__slug}`; + } + + const response = await axios({ + url, + method, + data, + }); + if ((response.status === 200 || response.status === 201) && response.data) { + return response; + } else { + return null; + } + }, + async updateFeature({ feature_id, feature_type__slug, project__slug, newStatus }) { let url = `${baseUrl}features/${feature_id}/?feature_type__slug=${feature_type__slug}&project__slug=${project__slug}`; @@ -108,33 +157,6 @@ const featureAPI = { return null; } }, - - async getFeatureLinks(featureId) { - const response = await axios.get( - `${baseUrl}features/${featureId}/feature-links/` - ); - if ( - response.status === 200 && - response.data - ) { - return response.data; - } else { - return null; - } - }, - - async getFeaturesBlob(url) { - const response = await axios - .get(url, { responseType: 'blob' }); - if ( - response.status === 200 && - response.data - ) { - return response.data; - } else { - return null; - } - }, }; export default featureAPI; diff --git a/src/services/project-api.js b/src/services/project-api.js index f150a81ed390fc9d128cb131201e1cc2fae1a135..8866ad935e563ed5f947be4f3b178760d825d937 100644 --- a/src/services/project-api.js +++ b/src/services/project-api.js @@ -45,21 +45,22 @@ const projectAPI = { } }, - async getProjects(baseUrl, filters, page) { + async getProjects({ baseUrl, filters, page, projectSlug, myaccount }) { + let url = `${baseUrl}projects/`; + if (projectSlug) url += `${projectSlug}/`; + url += `?page=${page}`; + if (myaccount) { + url += '&myaccount=true'; + } try { - const url = `${baseUrl}projects/?page=${page}`; - - let filteredUrl; if (Object.values(filters).some(el => el && el.length > 0)) { - filteredUrl = url; for (const filter in filters) { if (filters[filter]) { - filteredUrl = filteredUrl.concat('', `&${filter}=${filters[filter]}`); + url = url.concat('', `&${filter}=${filters[filter]}`); } } } - - const response = await axios.get(filteredUrl ? filteredUrl : url); + const response = await axios.get(url); if (response.status === 200 && response.data) { return response.data; } diff --git a/src/store/index.js b/src/store/index.js index 3aa9c61d5fa0359782e1ac3544647c40ab22ee7b..805c0fe99a5e12cc883303ce88bbe294b4c7fa25 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -23,25 +23,29 @@ const noPermissions = { export default new Vuex.Store({ modules, - + state: { - logged: false, - user: false, + cancellableSearchRequest: [], configuration: null, - staticPages: null, - USER_LEVEL_PROJECTS: null, - user_permissions: null, + isOnline: true, levelsPermissions: [], - messages: [], loader: { isLoading: false, message: 'En cours de chargement' }, - cancellableSearchRequest: [], - reloadIntervalId: null + logged: false, + messages: [], + reloadIntervalId: null, + staticPages: null, + user: false, + USER_LEVEL_PROJECTS: null, + user_permissions: null, }, mutations: { + SET_IS_ONLINE(state, payload) { + state.isOnline = payload; + }, SET_USER(state, payload) { state.user = payload; }, @@ -106,7 +110,7 @@ export default new Vuex.Store({ CLEAR_RELOAD_INTERVAL_ID(state) { clearInterval(state.reloadIntervalId); state.reloadIntervalId = null; - } + }, }, getters: { diff --git a/src/store/modules/feature.store.js b/src/store/modules/feature.store.js index 3039126bf92273a558eacab63830e5599198ed99..b9c90db26448e691b5777cad211d1e6b48458a36 100644 --- a/src/store/modules/feature.store.js +++ b/src/store/modules/feature.store.js @@ -1,20 +1,21 @@ import axios from '@/axios-client.js'; import router from '../../router'; - const feature = { namespaced: true, state: { attachmentFormset: [], attachmentsToDelete: [], checkedFeatures: [], + clickedFeatures: [], extra_form: [], features: [], features_count: 0, current_feature: [], form: null, - linkedFormset: [], - linked_features: [], + linkedFormset: [], //* used to edit in feature_edit + linked_features: [], //* used to display in feature_detail + massMode: 'modify', statusChoices: [ { name: 'Brouillon', @@ -100,7 +101,13 @@ const feature = { }, UPDATE_CHECKED_FEATURES(state, checkedFeatures) { state.checkedFeatures = checkedFeatures; - } + }, + UPDATE_CLICKED_FEATURES(state, clickedFeatures) { + state.clickedFeatures = clickedFeatures; + }, + TOGGLE_MASS_MODE(state, payload) { + state.massMode = payload; + }, }, getters: { }, @@ -161,7 +168,7 @@ const feature = { const cancelToken = axios.CancelToken.source(); commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true }); - commit('SET_CURRENT_FEATURE', null); + //commit('SET_CURRENT_FEATURE', null); //? Est-ce que c'est nécessaire ? -> fait sauter l'affichage au clic sur un signalement lié (feature_detail) let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}projects/${project_slug}/feature/?id=${feature_id}`; return axios .get(url, { cancelToken: cancelToken.token }) @@ -235,6 +242,9 @@ const feature = { &project__slug=${rootState.projects.project.slug}`; } + //* postOrPutFeature function from service featureAPI could be used here, but because configuration is in store, + //* projectBase would need to be sent with each function which imply to modify all function from this service, + //* which could create regression return axios({ url, method: routeName === 'editer-signalement' ? 'PUT' : 'POST', @@ -252,7 +262,7 @@ const feature = { }) .catch((error) => { commit('DISCARD_LOADER', null, { root: true }); - if (error.message === 'Network Error' || window.navigator.onLine === false) { + if (error.message === 'Network Error' || !rootState.isOnline) { let arraysOffline = []; let localStorageArray = localStorage.getItem('geocontrib_offline'); if (localStorageArray) { @@ -372,10 +382,11 @@ const feature = { }); }, - DELETE_FEATURE({ rootState }, feature_id) { - const url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/${feature_id}/?` + - `feature_type__slug=${rootState.feature_type.current_feature_type_slug}` + - `&project__slug=${rootState.projects.project.slug}`; + DELETE_FEATURE({ rootState }, payload) { + const { feature_id, noFeatureType } = payload; + let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/${feature_id}/?` + + `project__slug=${rootState.projects.project.slug}`; + if (!noFeatureType) url +=`&feature_type__slug=${rootState.feature_type.current_feature_type_slug}`; return axios .delete(url) .then((response) => response) diff --git a/src/store/modules/projects.store.js b/src/store/modules/projects.store.js index e6229cff4e35e2dcbae23a160ed0b9b517596226..ba104bc4c58579f814786065aa09ed3619e63028 100644 --- a/src/store/modules/projects.store.js +++ b/src/store/modules/projects.store.js @@ -1,6 +1,13 @@ import axios from '@/axios-client.js'; import projectAPI from '@/services/project-api'; +const initialFilters = { + moderation: null, + access_level: null, + user_access_level: null, + accessible: null +}; + const projects = { namespaced: true, @@ -8,12 +15,7 @@ const projects = { state: { count: 0, currentPage: 1, - filters: { - moderation: null, - access_level: null, - user_access_level: null, - accessible: null - }, + filters: { ...initialFilters }, isProjectsListSearched: null, last_comments: [], projects: [], @@ -22,11 +24,6 @@ const projects = { searchProjectsFilter: null, }, - getters: { - project_types: state => state.projects.filter(projet => projet.is_project_type), - project_user: state => state.projects.filter(projet => projet.creator === state.user.id), - }, - mutations: { SET_CURRENT_PAGE (state, payload) { state.currentPage = payload; @@ -54,6 +51,10 @@ const projects = { state.filters[payload.filter] = payload.value; }, + RESET_PROJECTS_FILTER(state) { + state.filters = { ...initialFilters }; + }, + SET_PROJECTS_SEARCH_STATE(state, payload) { state.isProjectsListSearched = payload.isSearched; state.searchProjectsFilter = payload.text; @@ -78,13 +79,21 @@ const projects = { } }, - async GET_PROJECTS({ state, rootState, commit }, page) { + async GET_PROJECTS({ state, rootState, commit }, payload) { + let { page, myaccount, projectSlug } = payload || {}; if (!page) { page = state.currentPage; } const baseUrl = rootState.configuration.VUE_APP_DJANGO_API_BASE; - const projects = await projectAPI.getProjects(baseUrl, state.filters, page); + const projects = await projectAPI.getProjects({ + baseUrl, + filters : state.filters, + page, + projectSlug, + myaccount, + }); commit('SET_PROJECTS', projects); + return; }, async SEARCH_PROJECTS({ commit, dispatch }, text) { @@ -99,7 +108,7 @@ const projects = { } }, - async GET_PROJECT({ rootState, commit }, slug) { + async GET_PROJECT({ rootState, commit }, slug) { // todo : use GET_PROJECTS instead, with slug const baseUrl = rootState.configuration.VUE_APP_DJANGO_API_BASE; const project = await projectAPI.getProject(baseUrl, slug); commit('SET_PROJECT', project); diff --git a/src/views/My_account.vue b/src/views/My_account.vue index 1959ce4516be28b94ffd585199fddc44879a4685..b5e72e7401d1e77391177642695784f78bcd4c32 100644 --- a/src/views/My_account.vue +++ b/src/views/My_account.vue @@ -58,9 +58,9 @@ MES PROJETS </h4> - <div class="ui divided items"> + <div class="ui divided items dimmer-anchor"> <div - v-for="project in availableProjects" + v-for="project in projectsArray" :key="project.slug" class="item" > @@ -133,6 +133,21 @@ </div> </div> </div> + + <div + :class="['ui inverted dimmer', { active: projectsLoading }]" + > + <div class="ui text loader"> + Récupération des projets en cours... + </div> + </div> + <!-- PAGINATION --> + <Pagination + v-if="count" + :nb-pages="Math.ceil(count/10)" + :on-page-change="SET_CURRENT_PAGE" + @change-page="changePage" + /> </div> </div> </div> @@ -308,8 +323,10 @@ <script> import frag from 'vue-frag'; -import { mapState } from 'vuex'; +import { mapActions, mapMutations, mapState } from 'vuex'; import miscAPI from '@/services/misc-api'; +import Pagination from '@/components/Pagination.vue'; + export default { name: 'MyAccount', @@ -318,11 +335,16 @@ export default { frag, }, + components: { + Pagination, + }, + data() { return { events: [], features: [], comments: [], + projectsLoading: true, }; }, @@ -333,9 +355,9 @@ export default { 'user_permissions', ]), - // todo : filter projects to user ...mapState('projects', [ - 'projects' + 'projects', + 'count', ]), DJANGO_BASE_URL() { @@ -352,19 +374,30 @@ export default { return this.$route.path.includes('projet-partage'); }, - availableProjects() { - if (this.isSharedProject) { - return this.projects.filter((el) => el.slug === this.$route.params.slug); - } - return this.projects; + projectsArray() { //* if only one project, only project object is returned + return Array.isArray(this.projects) ? this.projects : [this.projects]; } }, created(){ + this.RESET_PROJECTS_FILTER(); //* empty remaining filters in store + this.SET_PROJECTS([]); //* empty previous project to avoid undefined user_permissions[project.slug] + this.getData(); this.getEvents(); }, methods: { + ...mapActions('projects', [ + 'GET_PROJECTS', + ]), + ...mapMutations('projects', [ + 'SET_PROJECTS', + 'RESET_PROJECTS_FILTER', + ]), + ...mapMutations('projects', [ + 'SET_CURRENT_PAGE', + ]), + refreshId() { return '?ver=' + Math.random(); }, @@ -383,7 +416,18 @@ export default { return url.replace('projet', 'projet-partage'); } return url; - } + }, + + getData(page) { + this.projectsLoading = true; + this.GET_PROJECTS({ myaccount: true, projectSlug: this.$route.params.slug, page }) + .then(() => this.projectsLoading = false) + .catch(() => this.projectsLoading = false); + }, + + changePage(e) { + this.getData(e); + }, } }; </script> \ No newline at end of file diff --git a/src/views/Projects.vue b/src/views/Projects.vue index 42640515ef9d34aa72360921eabb32370d568721..5945f646d6fdbaf7fa10839e426ee6ce795bd33b 100644 --- a/src/views/Projects.vue +++ b/src/views/Projects.vue @@ -6,14 +6,14 @@ <div class="flex"> <router-link - v-if="user && user.can_create_project && isOffline() != true" + v-if="user && user.can_create_project && isOnline" :to="{ name: 'project_create', params: { action: 'create' } }" class="ui green basic button" > <i class="plus icon" /> Créer un nouveau projet </router-link> <router-link - v-if="user && user.can_create_project && isOffline() != true" + v-if="user && user.can_create_project && isOnline" :to="{ name: 'project_type_list', }" @@ -48,12 +48,6 @@ v-if="projects" class="ui divided items dimmable dimmed" > - <div - :class="{ active: loading }" - class="ui inverted dimmer" - > - <div class="ui loader" /> - </div> <div v-for="project in projects" :key="project.slug" @@ -124,16 +118,21 @@ v-if="!projects || projects.length === 0" >Vous n'avez accès à aucun projet.</span> - <div class="item" /> - </div> + <div + :class="{ active: loading }" + class="ui inverted dimmer" + > + <div class="ui loader" /> + </div> - <!-- PAGINATION --> - <pagination - v-if="count" - :nb-pages="Math.ceil(count/10)" - :on-page-change="SET_CURRENT_PAGE" - @change-page="changePage" - /> + <!-- PAGINATION --> + <Pagination + v-if="count" + :nb-pages="Math.ceil(count/10)" + :on-page-change="SET_CURRENT_PAGE" + @change-page="changePage" + /> + </div> </div> </template> @@ -162,12 +161,13 @@ export default { ...mapState([ 'configuration', 'user', - 'USER_LEVEL_PROJECTS' + 'USER_LEVEL_PROJECTS', + 'isOnline', ]), ...mapState('projects', [ 'projects', 'count', - 'filters' + 'filters', ]), APPLICATION_NAME() { return this.$store.state.configuration.VUE_APP_APPLICATION_NAME; @@ -226,9 +226,6 @@ export default { 'GET_PROJECTS' ]), - isOffline() { - return navigator.onLine == false; - }, refreshId() { //* change path of thumbnail to update image return '?ver=' + Math.random(); @@ -236,7 +233,7 @@ export default { getData(page) { this.loading = true; - this.GET_PROJECTS(page) + this.GET_PROJECTS({ page }) .then(() => { this.loading = false; }) diff --git a/src/views/feature/Feature_detail.vue b/src/views/feature/Feature_detail.vue index 66c148a4f66da1a651d6f6f7854fe31ed2c7273d..b880240f6307b20e27805f216832a8ede308572b 100644 --- a/src/views/feature/Feature_detail.vue +++ b/src/views/feature/Feature_detail.vue @@ -1,288 +1,245 @@ <template> <div v-frag> - <div - v-if="feature" - v-frag - > - <div class="row"> - <div class="fourteen wide column"> - <h1 class="ui header"> - <div class="content"> - {{ feature.title || feature.feature_id }} - <div class="ui icon right floated compact buttons"> - <router-link - v-if="permissions && permissions.can_create_feature" - :to="{ - name: 'ajouter-signalement', - params: { - slug_type_signal: $route.params.slug_type_signal, - }, - }" - class="ui button button-hover-orange" - data-tooltip="Ajouter un signalement" - data-position="bottom left" - > - <i class="plus fitted icon" /> - </router-link> - <router-link - v-if=" - (permissions && permissions.can_update_feature) || - isFeatureCreator || - isModerator - " - :to="{ - name: 'editer-signalement', - params: { - slug_signal: $route.params.slug_signal, - slug_type_signal: $route.params.slug_type_signal, - }, - }" - class="ui button button-hover-orange" - > - <i class="inverted grey pencil alternate icon" /> - </router-link> - <a - v-if="isFeatureCreator" - id="feature-delete" - class="ui button button-hover-red" - @click="isCanceling = true" - > - <i class="inverted grey trash alternate icon" /> - </a> - </div> - <div class="ui hidden divider" /> - <div class="sub header prewrap"> - {{ feature.description }} - </div> + <div class="row"> + <div class="fourteen wide column"> + <h1 class="ui header"> + <div + v-if="feature" + class="content" + > + {{ feature.title || feature.feature_id }} + <div class="ui icon right floated compact buttons"> + <router-link + v-if="permissions && permissions.can_create_feature" + :to="{ + name: 'ajouter-signalement', + params: { + slug_type_signal: $route.params.slug_type_signal, + }, + }" + class="ui button button-hover-orange" + data-tooltip="Ajouter un signalement" + data-position="bottom left" + > + <i class="plus fitted icon" /> + </router-link> + <router-link + v-if=" + (permissions && permissions.can_update_feature) || + isFeatureCreator || + isModerator + " + :to="{ + name: 'editer-signalement', + params: { + slug_signal: $route.params.slug_signal, + slug_type_signal: $route.params.slug_type_signal, + }, + }" + class="ui button button-hover-orange" + > + <i class="inverted grey pencil alternate icon" /> + </router-link> + <a + v-if="((permissions && permissions.can_update_feature) || isFeatureCreator) && isOnline" + id="feature-delete" + class="ui button button-hover-red" + @click="isCanceling = true" + > + <i class="inverted grey trash alternate icon" /> + </a> </div> - </h1> - </div> + <div class="ui hidden divider" /> + <div class="sub header prewrap"> + {{ feature.description }} + </div> + </div> + </h1> </div> + </div> - <div class="row"> - <div class="seven wide column"> - <table class="ui very basic table"> - <tbody> - <tr v-if="feature_type"> + <div v-if="!feature && !loader.isLoading"> + Pas de signalement correspondant trouvé + </div> + + <div class="row"> + <div class="seven wide column"> + <table + v-if="feature" + class="ui very basic table" + > + <tbody> + <div + v-for="(field, index) in feature.feature_data" + :key="'field' + index" + v-frag + > + <tr v-if="field"> <td> - <b> Type de signalement </b> + <b>{{ field.label }}</b> </td> <td> - <router-link - :to="{ - name: 'details-type-signalement', - params: { feature_type_slug: feature_type.slug }, - }" - class="feature-type-title" - > - <img - v-if="feature_type.geom_type === 'point'" - class="list-image-type" - src="@/assets/img/marker.png" - > - <img - v-if="feature_type.geom_type === 'linestring'" - class="list-image-type" - src="@/assets/img/line.png" - > - <img - v-if="feature_type.geom_type === 'polygon'" - class="list-image-type" - src="@/assets/img/polygon.png" - > - {{ feature_type.title }} - </router-link> - </td> - </tr> - <div - v-for="(field, index) in feature.feature_data" - :key="'field' + index" - v-frag - > - <tr v-if="field"> - <td> - <b>{{ field.label }}</b> - </td> - <td> - <b> - <i - v-if="field.field_type === 'boolean'" - :class="[ - 'icon', - field.value ? 'olive check' : 'grey times', - ]" - /> - <span v-else> - {{ field.value }} - </span> - </b> - </td> - </tr> - </div> - <tr> - <td>Auteur</td> - <td>{{ feature.display_creator }}</td> - </tr> - <tr> - <td>Statut</td> - <td> - <i - v-if="feature.status" - :class="['icon', statusIcon]" - /> - {{ statusLabel }} - </td> - </tr> - <tr> - <td>Date de création</td> - <td v-if="feature.created_on"> - {{ feature.created_on | formatDate }} - </td> - </tr> - <tr> - <td>Date de dernière modification</td> - <td v-if="feature.updated_on"> - {{ feature.updated_on | formatDate }} - </td> - </tr> - </tbody> - </table> - - <h3>Liaison entre signalements</h3> - <table class="ui very basic table"> - <tbody> - <tr - v-for="(link, index) in linked_features" - :key="link.feature_to.title + index" - > - <td v-if="link.feature_to.feature_type_slug"> - {{ link.relation_type_display }} - <a @click="pushNgo(link)">{{ link.feature_to.title }} </a> - ({{ link.feature_to.display_creator }} - - {{ link.feature_to.created_on }}) + <b> + <i + v-if="field.field_type === 'boolean'" + :class="[ + 'icon', + field.value ? 'olive check' : 'grey times', + ]" + /> + <span v-else> + {{ field.value }} + </span> + </b> </td> </tr> - </tbody> - </table> - </div> + </div> + <tr> + <td>Auteur</td> + <td>{{ feature.display_creator }}</td> + </tr> + <tr> + <td>Statut</td> + <td> + <i + v-if="feature.status" + :class="['icon', statusIcon]" + /> + {{ statusLabel }} + </td> + </tr> + <tr> + <td>Date de création</td> + <td v-if="feature.created_on"> + {{ feature.created_on | formatDate }} + </td> + </tr> + <tr> + <td>Date de dernière modification</td> + <td v-if="feature.updated_on"> + {{ feature.updated_on | formatDate }} + </td> + </tr> + </tbody> + </table> + + <h3 v-if="feature"> + Liaison entre signalements + </h3> + <table + v-if="feature" + class="ui very basic table" + > + <tbody> + <tr + v-for="(link, index) in linked_features" + :key="link.feature_to.title + index" + > + <td v-if="link.feature_to.feature_type_slug"> + {{ link.relation_type_display }} + <a + class="pointer" + @click="pushNgo(link)" + >{{ link.feature_to.title }} </a> + ({{ link.feature_to.display_creator }} - + {{ link.feature_to.created_on }}) + </td> + </tr> + </tbody> + </table> + </div> - <div class="seven wide column"> - <div - id="map" - ref="map" - /> - </div> + <div class="seven wide column"> + <div + id="map" + ref="map" + /> </div> + </div> - <div class="row"> - <div class="seven wide column"> - <h2 class="ui header"> - Pièces jointes - </h2> + <div + v-if="feature" + class="row" + > + <div + class="seven wide column" + > + <h2 class="ui header"> + Pièces jointes + </h2> - <div - v-for="pj in attachments" - :key="pj.id" - class="ui divided items" - > - <div class="item"> + <div + v-for="pj in attachments" + :key="pj.id" + class="ui divided items" + > + <div class="item"> + <a + class="ui tiny image" + target="_blank" + :href="pj.attachment_file" + > + <img + :src=" + pj.extension === '.pdf' + ? require('@/assets/img/pdf.png') + : pj.attachment_file + " + > + </a> + <div class="middle aligned content"> <a - class="ui tiny image" + class="header" target="_blank" :href="pj.attachment_file" - > - <img - :src=" - pj.extension === '.pdf' - ? require('@/assets/img/pdf.png') - : pj.attachment_file - " - > - </a> - <div class="middle aligned content"> - <a - class="header" - target="_blank" - :href="pj.attachment_file" - >{{ - pj.title - }}</a> - <div class="description"> - {{ pj.info }} - </div> + >{{ + pj.title + }}</a> + <div class="description"> + {{ pj.info }} </div> </div> </div> - <i - v-if="attachments.length === 0" - >Aucune pièce jointe associée au signalement.</i> </div> + <i + v-if="attachments.length === 0" + >Aucune pièce jointe associée au signalement.</i> + </div> - <div class="seven wide column"> - <h2 class="ui header"> - Activité et commentaires - </h2> + <div class="seven wide column"> + <h2 class="ui header"> + Activité et commentaires + </h2> + <div + id="feed-event" + class="ui feed" + > <div - id="feed-event" - class="ui feed" + v-for="(event, index) in events" + :key="'event' + index" + v-frag > <div - v-for="(event, index) in events" - :key="'event' + index" + v-if="event.event_type === 'create'" v-frag > <div - v-if="event.event_type === 'create'" - v-frag + v-if="event.object_type === 'feature'" + class="event" > - <div - v-if="event.object_type === 'feature'" - class="event" - > - <div class="content"> - <div class="summary"> - <div class="date"> - {{ event.created_on }} - </div> - Création du signalement - <span v-if="user">par {{ event.display_user }}</span> - </div> - </div> - </div> - <div - v-else-if="event.object_type === 'comment'" - class="event" - > - <div class="content"> - <div class="summary"> - <div class="date"> - {{ event.created_on }} - </div> - Commentaire - <span v-if="user">par {{ event.display_user }}</span> - </div> - <div class="extra text"> - {{ event.related_comment.comment }} - <div - v-if="event.related_comment.attachment" - v-frag - > - <br><a - :href=" - DJANGO_BASE_URL + - event.related_comment.attachment.url - " - target="_blank" - ><i class="paperclip fitted icon" /> - {{ event.related_comment.attachment.title }}</a> - </div> + <div class="content"> + <div class="summary"> + <div class="date"> + {{ event.created_on }} </div> + Création du signalement + <span v-if="user">par {{ event.display_user }}</span> </div> </div> </div> <div - v-else-if="event.event_type === 'update'" + v-else-if="event.object_type === 'comment'" class="event" > <div class="content"> @@ -290,136 +247,160 @@ <div class="date"> {{ event.created_on }} </div> - Signalement mis à jour + Commentaire <span v-if="user">par {{ event.display_user }}</span> </div> + <div class="extra text"> + {{ event.related_comment.comment }} + <div + v-if="event.related_comment.attachment" + v-frag + > + <br><a + :href=" + DJANGO_BASE_URL + + event.related_comment.attachment.url + " + target="_blank" + ><i class="paperclip fitted icon" /> + {{ event.related_comment.attachment.title }}</a> + </div> + </div> </div> </div> </div> - </div> - - <div - v-if="permissions && permissions.can_create_feature && isOffline() !== true" - class="ui segment" - > - <form - id="form-comment" - class="ui form" + <div + v-else-if="event.event_type === 'update'" + class="event" > - <div class="required field"> - <label - :for="comment_form.comment.id_for_label" - >Ajouter un commentaire</label> - <ul - v-if="comment_form.comment.errors" - class="errorlist" - > - <li> - {{ comment_form.comment.errors }} - </li> - </ul> - <textarea - v-model="comment_form.comment.value" - :name="comment_form.comment.html_name" - rows="2" - /> - </div> - <label>Pièce jointe (facultative)</label> - <div class="two fields"> - <div class="field"> - <label - class="ui icon button" - for="attachment_file" - > - <i class="paperclip icon" /> - <span class="label">{{ - comment_form.attachment_file.value - ? comment_form.attachment_file.value - : "Sélectionner un fichier ..." - }}</span> - </label> - <input - id="attachment_file" - type="file" - accept="application/pdf, image/jpeg, image/png" - style="display: none" - name="attachment_file" - @change="onFileChange" - > - </div> - <div class="field"> - <input - id="title" - v-model="comment_form.attachment_file.title" - type="text" - name="title" - > - {{ comment_form.attachment_file.errors }} + <div class="content"> + <div class="summary"> + <div class="date"> + {{ event.created_on }} + </div> + Signalement mis à jour + <span v-if="user">par {{ event.display_user }}</span> </div> </div> + </div> + </div> + </div> + + <div + v-if="permissions && permissions.can_create_feature && isOnline" + class="ui segment" + > + <form + id="form-comment" + class="ui form" + > + <div class="required field"> + <label + :for="comment_form.comment.id_for_label" + >Ajouter un commentaire</label> <ul - v-if="comment_form.attachment_file.errors" + v-if="comment_form.comment.errors" class="errorlist" > <li> - {{ comment_form.attachment_file.errors }} + {{ comment_form.comment.errors }} </li> </ul> - <button - type="button" - class="ui compact green icon button" - @click="postComment" - > - <i class="plus icon" /> Poster le commentaire - </button> - </form> - </div> - </div> - </div> - - <div - v-if="isCanceling" - class="ui dimmer modals page transition visible active" - style="display: flex !important" - > - <div - :class="[ - 'ui mini modal subscription', - { 'active visible': isCanceling }, - ]" - > - <i - class="close icon" - @click="isCanceling = false" - /> - <div class="ui icon header"> - <i class="trash alternate icon" /> - Supprimer le signalement - </div> - <div class="actions"> + <textarea + v-model="comment_form.comment.value" + :name="comment_form.comment.html_name" + rows="2" + /> + </div> + <label>Pièce jointe (facultative)</label> + <div class="two fields"> + <div class="field"> + <label + class="ui icon button" + for="attachment_file" + > + <i class="paperclip icon" /> + <span class="label">{{ + comment_form.attachment_file.value + ? comment_form.attachment_file.value + : "Sélectionner un fichier ..." + }}</span> + </label> + <input + id="attachment_file" + type="file" + accept="application/pdf, image/jpeg, image/png" + style="display: none" + name="attachment_file" + @change="onFileChange" + > + </div> + <div class="field"> + <input + id="title" + v-model="comment_form.attachment_file.title" + type="text" + name="title" + > + {{ comment_form.attachment_file.errors }} + </div> + </div> + <ul + v-if="comment_form.attachment_file.errors" + class="errorlist" + > + <li> + {{ comment_form.attachment_file.errors }} + </li> + </ul> <button type="button" - class="ui red compact fluid button" - @click="deleteFeature" + class="ui compact green icon button" + @click="postComment" > - Confirmer la suppression + <i class="plus icon" /> Poster le commentaire </button> - </div> + </form> </div> </div> </div> + <div - v-else - v-frag + v-if="isCanceling" + class="ui dimmer modals page transition visible active" + style="display: flex !important" > - Pas de signalement correspondant trouvé + <div + :class="[ + 'ui mini modal subscription', + { 'active visible': isCanceling }, + ]" + > + <i + class="close icon" + @click="isCanceling = false" + /> + <div class="ui icon header"> + <i class="trash alternate icon" /> + Supprimer le signalement + </div> + <div class="actions"> + <button + type="button" + class="ui red compact fluid button" + @click="deleteFeature" + > + Confirmer la suppression + </button> + </div> + </div> </div> </div> </template> <script> import frag from 'vue-frag'; -import { mapGetters, mapState, mapActions } from 'vuex'; +import { mapGetters, mapState, mapActions, mapMutations } from 'vuex'; import { mapUtil } from '@/assets/js/map-util.js'; import featureAPI from '@/services/feature-api'; import axios from '@/axios-client.js'; @@ -464,7 +445,9 @@ export default { computed: { ...mapState([ 'user', - 'USER_LEVEL_PROJECTS' + 'USER_LEVEL_PROJECTS', + 'isOnline', + 'loader', ]), ...mapState('projects', [ 'project' @@ -472,22 +455,15 @@ export default { ...mapGetters([ 'permissions', ]), - ...mapState('feature', [ - 'linked_features', - 'statusChoices' - ]), - ...mapGetters('feature_type', [ - 'feature_type', - ]), + ...mapState('feature', { + linked_features: 'linked_features', + statusChoices: 'statusChoices', + feature: 'current_feature', + }), DJANGO_BASE_URL() { return this.$store.state.configuration.VUE_APP_DJANGO_BASE; }, - feature() { - const result = this.$store.state.feature.current_feature; - return result; - }, - isFeatureCreator() { if (this.feature && this.user) { return this.feature.creator === this.user.id; @@ -536,33 +512,34 @@ export default { }, mounted() { - this.$store.commit('DISPLAY_LOADER', 'Recherche du signalement'); + this.DISPLAY_LOADER('Recherche du signalement'); + 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('projects/GET_PROJECT', this.$route.params.slug), - this.$store - .dispatch('projects/GET_PROJECT_INFO', this.$route.params.slug), - this.$store.dispatch('feature/GET_PROJECT_FEATURE', { + this.GET_PROJECT(this.$route.params.slug), + this.GET_PROJECT_INFO(this.$route.params.slug), + this.GET_PROJECT_FEATURE({ project_slug: this.$route.params.slug, feature_id: this.$route.params.slug_signal })]) .then(() => { - this.$store.commit('DISCARD_LOADER'); + this.DISCARD_LOADER(); this.initMap(); - }); - } if (!this.feature || this.feature.feature_id !== this.$route.params.slug_signal) { - this.$store.dispatch('feature/GET_PROJECT_FEATURE', { + }) + .catch(() => this.DISCARD_LOADER()); + } else if (!this.feature || this.feature.feature_id !== this.$route.params.slug_signal) { + this.GET_PROJECT_FEATURE({ project_slug: this.$route.params.slug, feature_id: this.$route.params.slug_signal }) .then(() => { - this.$store.commit('DISCARD_LOADER'); + this.DISCARD_LOADER(); this.initMap(); - }); + }) + .catch(() => this.DISCARD_LOADER()); } else { - this.$store.commit('DISCARD_LOADER'); + this.DISCARD_LOADER(); this.initMap(); } }, @@ -572,13 +549,21 @@ export default { }, methods: { + ...mapMutations([ + 'DISCARD_LOADER', + 'DISPLAY_LOADER', + ]), + ...mapActions('projects', [ + 'GET_PROJECT', + 'GET_PROJECT_INFO' + ]), ...mapActions('feature', [ - 'GET_PROJECT_FEATURES' + 'GET_PROJECT_FEATURES', + 'GET_PROJECT_FEATURE' ]), - isOffline() { - return navigator.onLine == false; - }, + pushNgo(link) { + this.DISPLAY_LOADER('Recherche du signalement'); this.$router.push({ name: 'details-signalement', params: { @@ -586,10 +571,18 @@ export default { slug_signal: link.feature_to.feature_id, }, }); + this.GET_PROJECT_FEATURE({ + project_slug: this.$route.params.slug, + feature_id: this.$route.params.slug_signal, + }) + .then(()=> { + this.addFeatureToMap(); + this.DISCARD_LOADER(); + }) + .catch(() => this.DISCARD_LOADER()); this.getFeatureEvents(); this.getFeatureAttachments(); this.getLinkedFeatures(); - this.addFeatureToMap(); }, validateForm() { @@ -696,7 +689,7 @@ export default { deleteFeature() { this.$store - .dispatch('feature/DELETE_FEATURE', this.feature.feature_id) + .dispatch('feature/DELETE_FEATURE', { feature_id: this.feature.feature_id }) .then((response) => { if (response.status === 204) { this.GET_PROJECT_FEATURES({ @@ -784,13 +777,15 @@ export default { getFeatureEvents() { featureAPI .getFeatureEvents(this.$route.params.slug_signal) - .then((data) => (this.events = data)); + .then((data) => (this.events = data)) + .catch((err)=> console.error(err)); }, getFeatureAttachments() { featureAPI .getFeatureAttachments(this.$route.params.slug_signal) - .then((data) => (this.attachments = data)); + .then((data) => (this.attachments = data)) + .catch((err)=> console.error(err)); }, getLinkedFeatures() { @@ -798,7 +793,8 @@ export default { .getFeatureLinks(this.$route.params.slug_signal) .then((data) => this.$store.commit('feature/SET_LINKED_FEATURES', data) - ); + ) + .catch((err)=> console.error(err)); }, }, }; diff --git a/src/views/feature/Feature_edit.vue b/src/views/feature/Feature_edit.vue index c54f912ee7a7d0ac66356e40e07ce5b30fd3e864..ffc3e2a571b7d264466bca5c1b9ec913572df200 100644 --- a/src/views/feature/Feature_edit.vue +++ b/src/views/feature/Feature_edit.vue @@ -74,7 +74,7 @@ v-if="feature_type && feature_type.geom_type === 'point'" v-frag > - <p v-if="isOffline() !== true"> + <p v-if="isOnline"> <button id="add-geo-image" type="button" @@ -229,12 +229,12 @@ </div> <!-- Pièces jointes --> - <div v-if="isOffline() !== true"> + <div v-if="isOnline"> <div class="ui horizontal divider"> PIÈCES JOINTES </div> <div - v-if="isOffline() !== true" + v-if="isOnline" id="formsets-attachment" > <FeatureAttachmentForm @@ -256,7 +256,7 @@ </div> <!-- Signalements liés --> - <div v-if="isOffline() !== true"> + <div v-if="isOnline"> <div class="ui horizontal divider"> SIGNALEMENTS LIÉS </div> @@ -373,7 +373,7 @@ export default { computed: { ...mapGetters(['permissions']), ...mapGetters('feature_type', ['feature_type']), - ...mapState(['user', 'USER_LEVEL_PROJECTS']), + ...mapState(['user', 'USER_LEVEL_PROJECTS', 'isOnline']), ...mapState('projects', ['project']), ...mapState('map', ['basemaps']), ...mapState('feature', [ @@ -486,9 +486,6 @@ export default { }, methods: { - isOffline() { - return navigator.onLine == false; - }, initForm() { if (this.currentRouteName === 'editer-signalement') { for (let key in this.feature) { @@ -650,8 +647,7 @@ export default { for (const linked of linkedFormset) { this.$store.commit('feature/ADD_LINKED_FORM', { dataKey: this.linkedDataKey, - relation_type: linked.relation_type, - feature_to: linked.feature_to, + ...linked }); this.linkedDataKey += 1; } diff --git a/src/views/feature/Feature_list.vue b/src/views/feature/Feature_list.vue index e6ea867becfa9da8c58c89f7e112ded3695572c9..19628e334dbf29093bf40e53e1fde18e0d5dcb6a 100644 --- a/src/views/feature/Feature_list.vue +++ b/src/views/feature/Feature_list.vue @@ -71,7 +71,7 @@ </div> <div - v-if="checkedFeatures.length > 0 && mode === 'modify'" + v-if="checkedFeatures.length > 0 && massMode === 'modify'" class="ui dropdown button compact button-hover-green margin-left-25" data-tooltip="Modifier le statut des Signalements" data-position="bottom right" @@ -100,11 +100,11 @@ </div> <div - v-if="checkedFeatures.length > 0 && mode === 'delete'" + v-if="checkedFeatures.length > 0 && massMode === 'delete'" class="ui button compact button-hover-red margin-left-25" - data-tooltip="Effacer tous les types de signalements sélectionnés" + data-tooltip="Supprimer tous les signalements sélectionnés" data-position="bottom right" - @click="modalAllDelete" + @click="toggleDeleteModal" > <i class="grey trash fitted icon" /> </div> @@ -190,10 +190,9 @@ <FeatureListTable v-show="!showMap" :paginated-features="paginatedFeatures" + :page-numbers="pageNumbers" :checked-features.sync="checkedFeatures" :features-count="featuresCount" - :clicked-features.sync="clickedFeatures" - :mode.sync="mode" :pagination="pagination" :sort="sort" @update:page="handlePageChange" @@ -202,19 +201,19 @@ <!-- MODAL ALL DELETE FEATURE TYPE --> <div - v-if="modalAllDeleteOpen" + v-if="isDeleteModalOpen" class="ui dimmer modals page transition visible active" style="display: flex !important" > <div :class="[ 'ui mini modal subscription', - { 'active visible': modalAllDeleteOpen }, + { 'active visible': isDeleteModalOpen }, ]" > <i class="close icon" - @click="modalAllDeleteOpen = false" + @click="isDeleteModalOpen = false" /> <div class="ui icon header"> <i class="trash alternate icon" /> @@ -237,15 +236,21 @@ </template> <script> -import { mapGetters, mapState, mapActions } from 'vuex'; +import { mapGetters, mapState, mapActions, mapMutations } 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'; import { allowedStatus2change } from '@/utils'; +const initialPagination = { + currentPage: 1, + pagesize: 15, + start: 0, + end: 15, +}; + export default { name: 'FeatureList', @@ -257,7 +262,6 @@ export default { data() { return { - clickedFeatures: [], currentLayer: null, featuresCount: 0, form: { @@ -269,20 +273,12 @@ export default { }, title: null, }, + isDeleteModalOpen: false, lat: null, lng: null, map: null, - modalAllDeleteOpen: false, - mode: 'modify', - next: null, paginatedFeatures: [], - pagination: { - currentPage: 1, - pagesize: 15, - start: 0, - end: 15, - }, - previous: null, + pagination: { ...initialPagination }, projectSlug: this.$route.params.slug, showMap: true, showAddFeature: false, @@ -305,7 +301,9 @@ export default { ]), ...mapState('feature', [ 'checkedFeatures', + 'clickedFeatures', 'statusChoices', + 'massMode', ]), ...mapState('feature_type', [ 'feature_types', @@ -337,6 +335,10 @@ export default { featureTypeChoices() { return this.feature_types.map((el) => el.title); }, + + pageNumbers() { + return this.createPagesArray(this.featuresCount, this.pagination.pagesize); + }, }, watch: { @@ -406,7 +408,12 @@ export default { methods: { ...mapActions('feature', [ 'GET_PROJECT_FEATURES', - 'SEND_FEATURE' + 'SEND_FEATURE', + 'DELETE_FEATURE', + ]), + + ...mapMutations('feature', [ + 'UPDATE_CHECKED_FEATURES' ]), toggleAddFeature() { @@ -419,8 +426,8 @@ export default { this.showAddFeature = false; }, - modalAllDelete() { - this.modalAllDeleteOpen = !this.modalAllDeleteOpen; + toggleDeleteModal() { + this.isDeleteModalOpen = !this.isDeleteModalOpen; }, clickOutsideDropdown(e) { @@ -444,7 +451,9 @@ export default { newStatus }).then((response) => { if (response && response.data && response.status === 200) { - this.checkedFeatures.splice(this.checkedFeatures.indexOf(response.data.id), 1); + let newCheckedFeatures = [...this.checkedFeatures]; + newCheckedFeatures.splice(this.checkedFeatures.indexOf(response.data.id), 1); + this.UPDATE_CHECKED_FEATURES(newCheckedFeatures); this.modifyStatus(newStatus); } else { this.$store.commit('DISPLAY_MESSAGE', { @@ -464,33 +473,26 @@ export default { } }, - deleteFeature(feature_id) { - const url = `${this.API_BASE_URL}features/${feature_id}/?project__slug=${this.projectSlug}`; - axios //TODO: REFACTO -> Delete function already exist in store - .delete(url, {}) - .then(() => { - if (!this.modalAllDeleteOpen) { - this.GET_PROJECT_FEATURES({ - project_slug: this.projectSlug, - }) - .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(); + const initialFeaturesCount = this.featuresCount; + const initialCurrentPage = this.pagination.currentPage; + const promises = this.checkedFeatures.map( + (feature_id) => this.DELETE_FEATURE({ feature_id, noFeatureType: true }) + ); + Promise.all(promises).then((response) => { + const deletedFeaturesCount = response.reduce((acc, curr) => curr.status === 204 ? acc += 1 : acc, 0); + const newFeaturesCount = initialFeaturesCount - deletedFeaturesCount; + const newPagesArray = this.createPagesArray(newFeaturesCount, this.pagination.pagesize); + const newLastPageNum = newPagesArray[newPagesArray.length - 1]; + this.$store.commit('feature/UPDATE_CHECKED_FEATURES', []); + if (initialCurrentPage > newLastPageNum) { //* if page doesn't exist anymore + this.toPage(newLastPageNum); //* go to new last page + } else { + this.fetchPagedFeatures(); + } + }) + .catch((err) => console.error(err)); + this.toggleDeleteModal(); }, onFilterChange() { @@ -548,8 +550,9 @@ export default { featureAPI .getFeaturesBbox(this.projectSlug, queryParams) .then((bbox) => { - if (bbox) { - mapUtil.getMap().fitBounds(bbox, { padding: [25, 25] }); + const map = mapUtil.getMap(); + if (bbox && map) { + map.fitBounds(bbox, { padding: [25, 25] }); } }); }, @@ -588,14 +591,13 @@ export default { fetchPagedFeatures(newUrl) { let url = `${this.API_BASE_URL}projects/${this.projectSlug}/feature-paginated/?output=geojson&limit=${this.pagination.pagesize}&offset=${this.pagination.start}`; - //* if receiving next & previous url + //* if receiving next & previous url (// todo : might be not used anymore, to check) 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...' @@ -618,17 +620,22 @@ export default { }, resetPaginationNfetchFeatures() { - this.pagination = { - currentPage: 1, - pagesize: 15, - start: 0, - end: 15, - }, + this.pagination = { ...initialPagination }; this.fetchPagedFeatures(); }, //* Pagination for table *// + createPagesArray(featuresCount, pagesize) { + const totalPages = Math.ceil( + featuresCount / pagesize + ); + return [...Array(totalPages).keys()].map((pageNumb) => { + ++pageNumb; + return pageNumb; + }); + }, + handlePageChange(page) { if (page === 'next') { this.toNextPage(); @@ -664,7 +671,7 @@ export default { this.pagination.end -= this.pagination.pagesize; this.pagination.currentPage -= 1; } - this.fetchPagedFeatures(this.previous); + this.fetchPagedFeatures(); } }, @@ -675,7 +682,7 @@ export default { this.pagination.end += this.pagination.pagesize; this.pagination.currentPage += 1; } - this.fetchPagedFeatures(this.next); + this.fetchPagedFeatures(); } }, }, @@ -725,6 +732,10 @@ export default { margin-right: 0 !important; } +#button-dropdown { + z-index: 1; +} + @media screen and (min-width: 767px) { .twelve-wide { width: 75% !important; diff --git a/src/views/feature_type/Feature_type_detail.vue b/src/views/feature_type/Feature_type_detail.vue index 1beab1d4ae462542b6390695b535570590d84a6e..d066c264e885239da26e3d8b1ec9483bbe7c9a72 100644 --- a/src/views/feature_type/Feature_type_detail.vue +++ b/src/views/feature_type/Feature_type_detail.vue @@ -71,13 +71,13 @@ class="ui styled accordion" > <div - :class="['title', { active: showImport }]" + :class="['title', { active: showImport && isOnline, nohover: !isOnline }]" @click="toggleShowImport" > <i class="dropdown icon" /> Importer des signalements </div> - <div :class="['content', { active: showImport }]"> + <div :class="['content', { active: showImport && isOnline }]"> <div id="form-import-features" class="ui form" @@ -120,7 +120,7 @@ </router-link> <div v-if="$route.params.geojson" - class="ui button import-catalog basic active teal no-hover" + class="ui button import-catalog basic active teal nohover" > Ressource {{ $route.params.geojson.name }} </div> @@ -149,13 +149,13 @@ </div> <div class="ui styled accordion"> <div - :class="['title', { active: !showImport }]" + :class="['title', { active: !showImport && isOnline, nohover: !isOnline }]" @click="toggleShowImport" > <i class="dropdown icon" /> Exporter les signalements </div> - <div :class="['content', { active: !showImport }]"> + <div :class="['content', { active: !showImport && isOnline}]"> <p> Vous pouvez télécharger tous les signalements qui vous sont accessibles. @@ -332,7 +332,7 @@ export default { computed: { ...mapGetters([ - 'permissions' + 'permissions', ]), ...mapGetters('projects', [ 'project' @@ -340,6 +340,7 @@ export default { ...mapState([ 'reloadIntervalId', 'configuration', + 'isOnline', ]), ...mapState('projects', [ 'project' @@ -591,7 +592,11 @@ export default { margin-bottom: 1em; } -.no-hover { +.nohover, .nohover:hover { cursor: default; } + +.ui.styled.accordion .nohover.title:hover { + color: rgba(0, 0, 0, .4); +} </style> \ No newline at end of file diff --git a/src/views/project/Project_detail.vue b/src/views/project/Project_detail.vue index ffe089daf4269ad63fa04cffe0cbe3b4ee6beaf6..3b8d4402b4157b9fc098e429b5a02bda8352c148 100644 --- a/src/views/project/Project_detail.vue +++ b/src/views/project/Project_detail.vue @@ -87,7 +87,7 @@ user && permissions && permissions.can_view_project && - isOffline() !== true + isOnline " id="subscribe-button" class="ui button button-hover-green" @@ -102,7 +102,7 @@ v-if=" permissions && permissions.can_update_project && - isOffline() !== true + isOnline " :to="{ name: 'project_edit', params: { slug } }" class="ui button button-hover-orange" @@ -113,7 +113,7 @@ <i class="inverted grey pencil alternate icon" /> </router-link> <a - v-if="isProjectAdmin && isOffline() !== true" + v-if="isProjectAdmin && isOnline" id="delete-button" class="ui button button-hover-red" data-tooltip="Supprimer le projet" @@ -152,9 +152,9 @@ <div v-if="arraysOffline.length > 0"> {{ arraysOffline.length }} modification<span v-if="arraysOffline.length>1">s</span> en attente <button - :disabled="isOffline()" + :disabled="!isOnline" class="ui fluid labeled teal icon button" - @click="sendOfflineFeatures()" + @click="sendOfflineFeatures" > <i class="upload icon" /> Envoyer au serveur @@ -244,7 +244,7 @@ project && permissions && permissions.can_create_feature_type && - isOffline() !== true + isOnline " :to="{ name: 'dupliquer-type-signalement', @@ -277,7 +277,7 @@ v-frag > <a - v-if="isProjectAdmin && isOffline() !== true" + v-if="isProjectAdmin && isOnline" class=" ui compact @@ -299,7 +299,7 @@ project && permissions && permissions.can_create_feature_type && - isOffline() !== true + isOnline " :to="{ name: 'editer-symbologie-signalement', @@ -326,7 +326,7 @@ type.is_editable && permissions && permissions.can_create_feature_type && - isOffline() !== true + isOnline " :to="{ name: 'editer-type-signalement', @@ -361,7 +361,7 @@ v-if=" permissions && permissions.can_update_project && - isOffline() !== true + isOnline " :to="{ name: 'ajouter-type-signalement', @@ -377,7 +377,7 @@ v-if=" permissions && permissions.can_update_project && - isOffline() !== true + isOnline " class=" ui @@ -414,7 +414,7 @@ IDGO && permissions && permissions.can_update_project && - isOffline() !== true + isOnline " :to="{ name: 'catalog-import', @@ -444,7 +444,10 @@ </button> </div> </div> - <div class="seven wide column"> + <div + id="map-column" + class="seven wide column" + > <div :class="{ active: mapLoading }" class="ui inverted dimmer" @@ -457,6 +460,16 @@ id="map" ref="map" /> + <div + class="ui button teal" + @click="$router.push({ + name: 'liste-signalements', + params: { slug: slug }, + })" + > + <i class="ui icon arrow right" /> + Voir tous les signalements + </div> </div> </div> @@ -577,8 +590,8 @@ <h3 class="ui header"> Paramètres du projet </h3> - <div class="ui five stackable cards"> - <div class="card"> + <div class="ui three stackable cards"> + <!-- <div class="card"> <div class="center aligned content"> <h4 class="ui center aligned icon header"> <i class="disabled grey archive icon" /> @@ -603,7 +616,7 @@ <div class="center aligned extra content"> {{ project.delete_feature }} jours </div> - </div> + </div> --> <div class="card"> <div class="content"> <h4 class="ui center aligned icon header"> @@ -752,8 +765,6 @@ import projectAPI from '@/services/project-api'; import featureTypeAPI from '@/services/featureType-api'; import featureAPI from '@/services/feature-api'; -import axios from '@/axios-client.js'; - import { fileConvertSizeToMo } from '@/assets/js/utils'; export default { @@ -813,6 +824,7 @@ export default { ]), ...mapState([ 'configuration', + 'isOnline', ]), ...mapState('feature_type', [ 'feature_types', @@ -927,6 +939,8 @@ export default { 'SET_RELOAD_INTERVAL_ID', 'CLEAR_RELOAD_INTERVAL_ID', 'DISPLAY_MESSAGE', + 'DISPLAY_LOADER', + 'DISCARD_LOADER', ]), ...mapActions('projects', [ 'GET_PROJECT_INFO', @@ -953,9 +967,6 @@ export default { } return url.replace(this.$store.state.configuration.BASE_URL, ''); //* remove duplicate /geocontrib }, - isOffline() { - return navigator.onLine === false; - }, isImporting(type) { if (this.importFeatureTypeData) { const singleImportData = this.importFeatureTypeData.find( @@ -978,13 +989,13 @@ export default { }, retrieveProjectInfo() { - this.$store.commit('DISPLAY_LOADER', 'Projet en cours de chargement.'); + this.DISPLAY_LOADER('Projet en cours de chargement.'); Promise.all([ this.GET_PROJECT(this.slug), this.GET_PROJECT_INFO(this.slug) ]) .then(() => { - this.$store.commit('DISCARD_LOADER'); + this.DISCARD_LOADER(); this.projectInfoLoading = false; setTimeout(() => { let map = mapUtil.getMap(); @@ -994,14 +1005,14 @@ export default { }) .catch((err) => { console.error(err); - this.$store.commit('DISCARD_LOADER'); + this.DISCARD_LOADER(); this.projectInfoLoading = false; }); }, checkForOfflineFeature() { let arraysOffline = []; - let localStorageArray = localStorage.getItem('geocontrib_offline'); + const localStorageArray = localStorage.getItem('geocontrib_offline'); if (localStorageArray) { arraysOffline = JSON.parse(localStorageArray); this.arraysOffline = arraysOffline.filter( @@ -1011,60 +1022,36 @@ export default { }, sendOfflineFeatures() { - var promises = []; - let self = this; this.arraysOfflineErrors = []; - this.arraysOffline.forEach((feature) => { - if (feature.type === 'post') { - promises.push( - axios - .post(`${this.API_BASE_URL}features/`, feature.geojson) - .then((response) => { - if (response.status === 201 && response.data) { - return 'OK'; - } else { - self.arraysOfflineErrors.push(feature); - } - }) - .catch((error) => { - console.error(error); - self.arraysOfflineErrors.push(feature); - }) - ); - } else if (feature.type === 'put') { - promises.push( - axios - .put( - `${this.API_BASE_URL}features/${feature.featureId}`, - feature.geojson - ) - .then((response) => { - if (response.status === 200 && response.data) { - return 'OK'; - } else { - self.arraysOfflineErrors.push(feature); - } - }) - .catch((error) => { - console.error(error); - self.arraysOfflineErrors.push(feature); - }) - ); - } - }); + const promises = this.arraysOffline.map((feature) => featureAPI.postOrPutFeature({ + data: feature.geojson, + feature_id: feature.featureId, + project__slug: feature.project, + feature_type__slug: feature.geojson.properties.feature_type, + method: feature.type.toUpperCase(), + }) + .then((response) => { + if (!response) this.arraysOfflineErrors.push(feature); + }) + .catch((error) => { + console.error(error); + this.arraysOfflineErrors.push(feature); + }) + ); + this.DISPLAY_LOADER('Envoi des signalements en cours.'); Promise.all(promises).then(() => { this.updateLocalStorage(); - window.location.reload(); + this.retrieveProjectInfo(); }); }, updateLocalStorage() { let arraysOffline = []; - let localStorageArray = localStorage.getItem('geocontrib_offline'); + const localStorageArray = localStorage.getItem('geocontrib_offline'); if (localStorageArray) { arraysOffline = JSON.parse(localStorageArray); } - let arraysOfflineOtherProject = arraysOffline.filter( + const arraysOfflineOtherProject = arraysOffline.filter( (x) => x.project !== this.slug ); this.arraysOffline = []; @@ -1164,7 +1151,6 @@ export default { .then((response) => { this.modalType = false; if (response === 'success') { - this.GET_PROJECT(); this.retrieveProjectInfo(); this.DISPLAY_MESSAGE({ comment: `Le type de signalement ${this.featureTypeToDelete.title} a bien été supprimé.`, @@ -1247,10 +1233,15 @@ export default { <style> +#map-column { + display: flex; + flex-direction: column; +} #map { width: 100%; height: 100%; min-height: 250px; + margin-bottom: 1em; } /* // ! missing style in semantic.min.css, je ne comprends pas comment... */ .ui.right.floated.button { diff --git a/src/views/project/Project_edit.vue b/src/views/project/Project_edit.vue index c7f277cacdb7253f51c29a61414a1ed975b6c453..b4b47d64cffd39c8685d8585a8c89e1c2fe6542a 100644 --- a/src/views/project/Project_edit.vue +++ b/src/views/project/Project_edit.vue @@ -100,8 +100,8 @@ PARAMÈTRES </div> - <div class="four fields"> - <div class="field"> + <div class="two fields"> + <!-- <div class="field"> <label for="archive_feature">Délai avant archivage</label> <div class="ui right labeled input"> <input @@ -145,7 +145,7 @@ jour(s) </div> </div> - </div> + </div> --> <div class="required field"> <label for="access_level_pub_feature"