diff --git a/src/assets/styles/base.css b/src/assets/styles/base.css index 29720bb3727072c31961a57a5fb5f046d561283c..f77d223bc0e28657f22ab8017e15c4ed3f490920 100644 --- a/src/assets/styles/base.css +++ b/src/assets/styles/base.css @@ -32,6 +32,9 @@ main { .important-flex { display: flex !important; } +.pointer:hover { + cursor: pointer; +} .dimmer-anchor { position: relative; } 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..96563ef2541326c7fb244bacaaad4d44be7f9347 100644 --- a/src/components/feature/FeatureListTable.vue +++ b/src/components/feature/FeatureListTable.vue @@ -1,312 +1,310 @@ <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" + {{ getFeatureDisplayName(feature) }} + </router-link> + </td> + <td class="dt-center"> + {{ feature.properties.updated_on }} + </td> + <td + v-if="user" + class="dt-center" > - <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, - }, - }" + {{ getUserName(feature) }} + </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, - }, - }" - > - {{ getFeatureDisplayName(feature) }} - </router-link> - </td> - <td class="dt-center"> - {{ feature.properties.updated_on }} - </td> - <td - v-if="user" - class="dt-center" - > - {{ 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" + {{ feature.properties.display_last_editor }} + </td> + </tr> + <tr + v-if="featuresCount === 0" + class="odd" > - 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, @@ -316,10 +314,6 @@ export default { type: Array, default: null, }, - clickedFeatures: { - type: Array, - default: null, - }, featuresCount: { type: Number, default: 0, @@ -332,16 +326,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]; @@ -396,12 +387,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 +427,7 @@ export default { }, checkRights(feature) { - switch (this.mode) { + switch (this.massMode) { case 'modify': return this.canEditFeature(feature); case 'delete': @@ -436,12 +435,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 +558,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 +566,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 +605,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 +620,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 +643,6 @@ and also iPads specifically. padding-right: 10px; white-space: nowrap; } - /* Label the data */ @@ -650,20 +664,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/store/index.js b/src/store/index.js index 3aa9c61d5fa0359782e1ac3544647c40ab22ee7b..58d7a2498d7ebb6a9145b69d1806106b5e31bb92 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -106,7 +106,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..f80e8235750a20557abcf76453655391d8f71a47 100644 --- a/src/store/modules/feature.store.js +++ b/src/store/modules/feature.store.js @@ -8,6 +8,7 @@ const feature = { attachmentFormset: [], attachmentsToDelete: [], checkedFeatures: [], + clickedFeatures: [], extra_form: [], features: [], features_count: 0, @@ -15,6 +16,7 @@ const feature = { form: null, linkedFormset: [], linked_features: [], + massMode: 'modify', statusChoices: [ { name: 'Brouillon', @@ -100,7 +102,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: { }, diff --git a/src/views/feature/Feature_list.vue b/src/views/feature/Feature_list.vue index e6ea867becfa9da8c58c89f7e112ded3695572c9..dda67d420dbeecdd72af007b26f3b730de60a7eb 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,7 +100,7 @@ </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-position="bottom right" @@ -192,8 +192,6 @@ :paginated-features="paginatedFeatures" :checked-features.sync="checkedFeatures" :features-count="featuresCount" - :clicked-features.sync="clickedFeatures" - :mode.sync="mode" :pagination="pagination" :sort="sort" @update:page="handlePageChange" @@ -237,7 +235,7 @@ </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'; @@ -257,7 +255,6 @@ export default { data() { return { - clickedFeatures: [], currentLayer: null, featuresCount: 0, form: { @@ -273,7 +270,6 @@ export default { lng: null, map: null, modalAllDeleteOpen: false, - mode: 'modify', next: null, paginatedFeatures: [], pagination: { @@ -305,7 +301,9 @@ export default { ]), ...mapState('feature', [ 'checkedFeatures', + 'clickedFeatures', 'statusChoices', + 'massMode', ]), ...mapState('feature_type', [ 'feature_types', @@ -409,6 +407,10 @@ export default { 'SEND_FEATURE' ]), + ...mapMutations('feature', [ + 'UPDATE_CHECKED_FEATURES' + ]), + toggleAddFeature() { this.showAddFeature = !this.showAddFeature; this.showModifyStatus = false; @@ -444,7 +446,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', { @@ -725,6 +729,10 @@ export default { margin-right: 0 !important; } +#button-dropdown { + z-index: 1; +} + @media screen and (min-width: 767px) { .twelve-wide { width: 75% !important;