diff --git a/package.json b/package.json index abb7e0b31723d7495246cdd21575cbf8367c9bcf..5ae0acebe82eec78972f9a2902f9a715b83a620c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "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/assets/styles/base.css b/src/assets/styles/base.css index 996e2d8b232b0019c8c7b34918f225cc38602448..fbfa9584141b69afcfb3f37e44e8c7ad94887f71 100644 --- a/src/assets/styles/base.css +++ b/src/assets/styles/base.css @@ -88,6 +88,12 @@ body { .important-flex { display: flex !important; } +.pointer:hover { + cursor: pointer; +} +.dimmer-anchor { + position: relative; +} /* ---------------------------------- */ /* MAIN */ /* ---------------------------------- */ diff --git a/src/components/Account/UserActivity.vue b/src/components/Account/UserActivity.vue index 45f7a8ff0ed3a5833783a7fab04a6a1c5623259e..9837974ee10053d2dfe99ce461ac9f806d55bbbe 100644 --- a/src/components/Account/UserActivity.vue +++ b/src/components/Account/UserActivity.vue @@ -18,7 +18,7 @@ <span v-if="item.event_type === 'create'"> <a v-if="item.object_type === 'feature'" - :href="modifyUrl(item.related_feature.feature_url)" + :href="modifyUrl(item.related_feature.feature_url || item.project_url)" > Signalement créé </a> diff --git a/src/components/Account/UserProjectsList.vue b/src/components/Account/UserProjectsList.vue index 70ee4fe37cd56427fd65af00f66ae7f0155536d6..d88eff53dca8939ffb3a627aef5549ce0396f5a2 100644 --- a/src/components/Account/UserProjectsList.vue +++ b/src/components/Account/UserProjectsList.vue @@ -6,9 +6,16 @@ <div class="ui divided items"> <div - v-for="project in availableProjects" - :key="project.slug" - class="item" + :class="['ui inverted dimmer', { active: projectsLoading }]" + > + <div class="ui text loader"> + Récupération des projets en cours... + </div> + </div> + <div + v-for="project in projectsArray" + :key="project.slug" + class="item" > <div v-if="user_permissions[project.slug].can_view_project" @@ -78,17 +85,38 @@ </div> </div> </div> + + <!-- PAGINATION --> + <Pagination + v-if="count" + :nb-pages="Math.ceil(count/10)" + :on-page-change="SET_CURRENT_PAGE" + @change-page="changePage" + /> </div> </div> </template> <script> + import { mapState } from 'vuex'; +import Pagination from '@/components/Pagination.vue'; + export default { name: 'UserProjectList', + components: { + Pagination, + }, + + data() { + return { + projectsLoading: true, + } + }, + computed: { ...mapState([ 'user', @@ -97,7 +125,8 @@ export default { ]), // todo : filter projects to user ...mapState('projects', [ - 'projects' + 'projects', + 'count', ]), DJANGO_BASE_URL() { @@ -113,13 +142,44 @@ export default { 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.SET_PROJECTS([]); //* empty previous project to avoid undefined user_permissions[project.slug] + this.getData(); + }, + + methods: { + ...mapMutations('projects', [ + 'SET_CURRENT_PAGE', + 'SET_PROJECTS', + ]), + + ...mapActions('projects', [ + 'GET_PROJECTS', + ]), + refreshId() { return '?ver=' + Math.random(); }, + + getData(page) { + this.loading = true; + this.GET_PROJECTS({ ismyaccount: true, projectSlug: this.$route.params.slug, page }) + .then(() => this.projectsLoading = false) + .catch(() => this.projectsLoading = false); + }, + + changePage(e) { + this.getData(e); + }, + } }; diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 1450fcaf389776a4bc7c438bf7e3e178eb941979..fadeb11aac7951213fc1ea465cca58f669b96908 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -55,7 +55,7 @@ </router-link> <router-link v-if=" - project && + project && isOnline && (user.is_administrator || user.is_superuser || isAdmin) " :to="{ @@ -68,7 +68,7 @@ </router-link> <router-link v-if=" - project && + project && isOnline && (user.is_administrator || user.is_superuser || isAdmin) " :to="{ @@ -81,8 +81,12 @@ </router-link> <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" }} @@ -132,8 +136,12 @@ </div> <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" }} @@ -201,6 +209,7 @@ export default { 'configuration', 'messages', 'loader', + 'isOnline' ]), ...mapState('projects', [ 'projects', diff --git a/src/components/FeaturesListAndMap/FeatureListTable.vue b/src/components/FeaturesListAndMap/FeatureListTable.vue index d0a0ad5a90ac960398790d161f4b7dae86dbae5b..e892519aba5b3f83ce0ea1a3a27770028f3cc58f 100644 --- a/src/components/FeaturesListAndMap/FeatureListTable.vue +++ b/src/components/FeaturesListAndMap/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/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/ProjectDetail/ProjectHeader.vue b/src/components/ProjectDetail/ProjectHeader.vue index 32d587355015cb4c3d3b1bceb550179439ab705f..124d5fffc49d30ce0eb64f8d47dd35015c3fccca 100644 --- a/src/components/ProjectDetail/ProjectHeader.vue +++ b/src/components/ProjectDetail/ProjectHeader.vue @@ -114,9 +114,9 @@ <div v-if="arraysOffline.length > 0"> {{ arraysOffline.length }} modification<span v-if="arraysOffline.length>1">s</span> en attente <button - :disabled="isOffline()" + :disabled="isOffline" class="ui fluid labeled teal icon button" - @click="sendOfflineFeatures()" + @click="sendOfflineFeatures" > <i class="upload icon" /> Envoyer au serveur @@ -180,6 +180,9 @@ export default { }, methods: { + ...mapState([ + 'isOnline' + ]), ...mapMutations('modals', [ 'OPEN_PROJECT_MODAL' ]), @@ -203,6 +206,48 @@ export default { ); }, + sendOfflineFeatures() { + this.arraysOfflineErrors = []; + + 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(); + this.$emit('retrieve-info'); + }); + }, + + updateLocalStorage() { + let arraysOffline = []; + const localStorageArray = localStorage.getItem('geocontrib_offline'); + if (localStorageArray) { + arraysOffline = JSON.parse(localStorageArray); + } + const arraysOfflineOtherProject = arraysOffline.filter( + (x) => x.project !== this.slug + ); + this.arraysOffline = []; + arraysOffline = arraysOfflineOtherProject.concat( + this.arraysOfflineErrors + ); + localStorage.setItem('geocontrib_offline', JSON.stringify(arraysOffline)); + }, + } }; diff --git a/src/components/SidebarLayers.vue b/src/components/SidebarLayers.vue index 082d679d96952f01d122c10be1985078f7d26578..9e9782e1827b4b228434b1c5509f1c4128721b9c 100644 --- a/src/components/SidebarLayers.vue +++ b/src/components/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/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/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..7dd25fb94ea383cea55ef57465533c7422199f08 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( + /^https:\/\/osm\.geo2france\.fr\/mapcache/, + new workbox.strategies.CacheFirst({ + cacheName: 'mapcache', + 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..d57046265453d56dbb28da4e8dc4dca686a461a0 100644 --- a/src/store/modules/feature.store.js +++ b/src/store/modules/feature.store.js @@ -1,13 +1,13 @@ 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, @@ -15,6 +15,7 @@ const feature = { form: null, linkedFormset: [], linked_features: [], + 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: { }, @@ -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 2f0b5eabe364686160fdef794578ec3f3260469b..68adf8ddc44f56fa4f074ac47897c7706997c80e 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/Feature/FeatureDetail.vue b/src/views/Feature/FeatureDetail.vue index 00dbd89ab737cb92332fee5ceb0c1e4a07b4ad7d..ce9347403959caba79c870d8aae16750d7031a7b 100644 --- a/src/views/Feature/FeatureDetail.vue +++ b/src/views/Feature/FeatureDetail.vue @@ -42,7 +42,7 @@ <i class="inverted grey pencil alternate icon" /> </router-link> <a - v-if="isFeatureCreator" + v-if="((permissions && permissions.can_update_feature) || isFeatureCreator) && isOnline" id="feature-delete" class="ui button button-hover-red" @click="isCanceling = true" @@ -268,7 +268,7 @@ </div> <div - v-if="permissions && permissions.can_create_feature && isOffline() !== true" + v-if="permissions && permissions.can_create_feature && isOnline" class="ui segment" > <form @@ -433,7 +433,8 @@ export default { computed: { ...mapState([ 'user', - 'USER_LEVEL_PROJECTS' + 'USER_LEVEL_PROJECTS', + 'isOnline', ]), ...mapState('projects', [ 'project' @@ -541,9 +542,7 @@ export default { ...mapActions('feature', [ 'GET_PROJECT_FEATURES' ]), - isOffline() { - return navigator.onLine == false; - }, + pushNgo(link) { this.$router.push({ name: 'details-signalement', @@ -662,7 +661,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({ diff --git a/src/views/Feature/FeatureEdit.vue b/src/views/Feature/FeatureEdit.vue index 57b7b792b695fac31e41181d5e4dc14d5c59e2ee..31c8b7c0825ba95f5c78804b794b62b4b62290e7 100644 --- a/src/views/Feature/FeatureEdit.vue +++ b/src/views/Feature/FeatureEdit.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> @@ -372,10 +372,15 @@ export default { computed: { ...mapGetters(['permissions']), +<<<<<<< HEAD:src/views/Feature/FeatureEdit.vue ...mapGetters('feature-type', [ 'feature_type' ]), ...mapState(['user', 'USER_LEVEL_PROJECTS']), +======= + ...mapGetters('feature_type', ['feature_type']), + ...mapState(['user', 'USER_LEVEL_PROJECTS', 'isOnline']), +>>>>>>> develop:src/views/feature/Feature_edit.vue ...mapState('projects', ['project']), ...mapState('map', ['basemaps']), ...mapState('feature', [ @@ -488,9 +493,6 @@ export default { }, methods: { - isOffline() { - return navigator.onLine == false; - }, initForm() { if (this.currentRouteName === 'editer-signalement') { for (let key in this.feature) { diff --git a/src/views/FeatureType/FeatureTypeDetail.vue b/src/views/FeatureType/FeatureTypeDetail.vue index 0bdca9f75f9ea6906b78bc2f4a9acc9f3d0a207b..492206d77490946f8383addaff89252b376c3007 100644 --- a/src/views/FeatureType/FeatureTypeDetail.vue +++ b/src/views/FeatureType/FeatureTypeDetail.vue @@ -70,9 +70,13 @@ <div class="ui bottom attached secondary segment"> <div - v-if="permissions.can_create_feature" - class="ui styled accordion" + :class="['title', { active: showImport && isOnline, nohover: !isOnline }]" + @click="toggleShowImport" > + <i class="dropdown icon" /> + Importer des signalements + </div> + <div :class="['content', { active: showImport && isOnline }]"> <div :class="['title', { active: showImport }]" @click="toggleShowImport" @@ -82,9 +86,8 @@ </div> <div :class="['content', { active: showImport }]"> <div - id="form-import-features" - class="ui form" - :class="loadingImportFile ? 'loading' : ''" + v-if="$route.params.geojson" + class="ui button import-catalog basic active teal nohover" > <div class="field"> <label @@ -173,6 +176,28 @@ </div> </div> </div> + <div class="ui styled accordion"> + <div + :class="['title', { active: !showImport && isOnline, nohover: !isOnline }]" + @click="toggleShowImport" + > + <i class="dropdown icon" /> + Exporter les signalements + </div> + <div :class="['content', { active: !showImport && isOnline}]"> + <p> + Vous pouvez télécharger tous les signalements qui vous sont + accessibles. + </p> + <button + type="button" + class="ui fluid teal icon button" + @click="exportFeatures" + > + <i class="download icon" /> Exporter + </button> + </div> + </div> </div> <div class="nine wide column"> @@ -337,7 +362,7 @@ export default { computed: { ...mapGetters([ - 'permissions' + 'permissions', ]), ...mapGetters('projects', [ 'project' @@ -345,6 +370,7 @@ export default { ...mapState([ 'reloadIntervalId', 'configuration', + 'isOnline', ]), ...mapState('projects', [ 'project' @@ -603,7 +629,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/FeaturesListAndMap.vue b/src/views/Project/FeaturesListAndMap.vue index ea9db05f300cb2b50d84935d6ce58efa863a288e..35135fbc7ded1f3a8025ffdae16bc931995a96a0 100644 --- a/src/views/Project/FeaturesListAndMap.vue +++ b/src/views/Project/FeaturesListAndMap.vue @@ -76,7 +76,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" @@ -105,11 +105,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> @@ -197,10 +197,9 @@ <FeatureListTable v-else :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" @@ -209,19 +208,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" /> @@ -244,16 +243,23 @@ </template> <script> -import axios from '@/axios-client.js'; -import featureAPI from '@/services/feature-api'; -import { mapGetters, mapState, mapActions } from 'vuex'; +import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'; import { mapUtil } from '@/assets/js/map-util.js'; import { allowedStatus2change } from '@/utils'; +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 SidebarLayers from '@/components/SidebarLayers'; -import FeatureListTable from '@/components/FeaturesListAndMap/FeatureListTable'; + +const initialPagination = { + currentPage: 1, + pagesize: 15, + start: 0, + end: 15, +}; export default { name: 'FeaturesListAndMap', @@ -266,7 +272,6 @@ export default { data() { return { - clickedFeatures: [], currentLayer: null, featuresCount: 0, form: { @@ -278,20 +283,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, @@ -317,7 +314,9 @@ export default { ]), ...mapState('feature', [ 'checkedFeatures', + 'clickedFeatures', 'statusChoices', + 'massMode', ]), ...mapState('feature-type', [ 'feature_types', @@ -349,6 +348,10 @@ export default { featureTypeChoices() { return this.feature_types.map((el) => el.title); }, + + pageNumbers() { + return this.createPagesArray(this.featuresCount, this.pagination.pagesize); + }, }, watch: { @@ -418,7 +421,12 @@ export default { methods: { ...mapActions('feature', [ 'GET_PROJECT_FEATURES', - 'SEND_FEATURE' + 'SEND_FEATURE', + 'DELETE_FEATURE', + ]), + + ...mapMutations('feature', [ + 'UPDATE_CHECKED_FEATURES' ]), toggleAddFeature() { @@ -431,8 +439,8 @@ export default { this.showAddFeature = false; }, - modalAllDelete() { - this.modalAllDeleteOpen = !this.modalAllDeleteOpen; + toggleDeleteModal() { + this.isDeleteModalOpen = !this.isDeleteModalOpen; }, clickOutsideDropdown(e) { @@ -456,7 +464,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', { @@ -476,33 +486,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() { @@ -560,8 +563,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] }); } }); }, @@ -600,14 +604,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...' @@ -630,17 +633,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(); @@ -676,7 +684,7 @@ export default { this.pagination.end -= this.pagination.pagesize; this.pagination.currentPage -= 1; } - this.fetchPagedFeatures(this.previous); + this.fetchPagedFeatures(); } }, @@ -687,7 +695,7 @@ export default { this.pagination.end += this.pagination.pagesize; this.pagination.currentPage += 1; } - this.fetchPagedFeatures(this.next); + this.fetchPagedFeatures(); } }, }, @@ -742,6 +750,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/Project/ProjectDetail.vue b/src/views/Project/ProjectDetail.vue index 06b2e8b56eb130f489f880d4ba876b5cd70c46de..c3eea4577ab546913056b2d9510449555ac7d174 100644 --- a/src/views/Project/ProjectDetail.vue +++ b/src/views/Project/ProjectDetail.vue @@ -38,6 +38,7 @@ <ProjectHeader :arrays-offline="arraysOffline" + @retrieveInfo="retrieveProjectInfo" /> <div class="ui grid"> @@ -231,6 +232,8 @@ export default { 'SET_RELOAD_INTERVAL_ID', 'CLEAR_RELOAD_INTERVAL_ID', 'DISPLAY_MESSAGE', + 'DISPLAY_LOADER', + 'DISCARD_LOADER', ]), ...mapMutations('modals', [ 'CLOSE_PROJECT_MODAL' @@ -257,13 +260,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(); @@ -273,14 +276,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( @@ -289,70 +292,6 @@ 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); - }) - ); - } - }); - Promise.all(promises).then(() => { - this.updateLocalStorage(); - window.location.reload(); - }); - }, - - updateLocalStorage() { - let arraysOffline = []; - let localStorageArray = localStorage.getItem('geocontrib_offline'); - if (localStorageArray) { - arraysOffline = JSON.parse(localStorageArray); - } - let arraysOfflineOtherProject = arraysOffline.filter( - (x) => x.project !== this.slug - ); - this.arraysOffline = []; - arraysOffline = arraysOfflineOtherProject.concat( - this.arraysOfflineErrors - ); - localStorage.setItem('geocontrib_offline', JSON.stringify(arraysOffline)); - }, - subscribeProject() { projectAPI .subscribeProject({ diff --git a/src/views/Projects/ProjectsList.vue b/src/views/Projects/ProjectsList.vue index 30c0b843b8be4583df5b01b534f1bfdcf6b546d9..0da07cffd48737375f3a721a3b80a7ee08296d90 100644 --- a/src/views/Projects/ProjectsList.vue +++ b/src/views/Projects/ProjectsList.vue @@ -9,14 +9,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', }" @@ -68,16 +68,20 @@ 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> </template> @@ -107,12 +111,13 @@ export default { computed: { ...mapState([ 'configuration', - 'user' + 'user', + 'isOnline', ]), ...mapState('projects', [ 'projects', 'count', - 'filters' + 'filters', ]), DJANGO_BASE_URL() { return this.$store.state.configuration.VUE_APP_DJANGO_BASE; @@ -162,13 +167,19 @@ export default { 'GET_PROJECTS' ]), +<<<<<<< HEAD:src/views/Projects/ProjectsList.vue isOffline() { return navigator.onLine == false; +======= + refreshId() { + //* change path of thumbnail to update image + return '?ver=' + Math.random(); +>>>>>>> develop:src/views/Projects.vue }, getData(page) { this.loading = true; - this.GET_PROJECTS(page) + this.GET_PROJECTS({ page }) .then(() => { this.loading = false; })