diff --git a/src/App.vue b/src/App.vue index 99f38c1d22924e0053f0fa10808a491ebd695ac0..6213eb57f3c95d6abfa19fa522fd36c8f87a9e69 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,7 +10,10 @@ <header class="header-menu"> <div class="menu container"> <div class="ui inverted icon menu"> - <router-link to="/" class="header item"> + <router-link + :to="isSharedProject ? '' : '/'" + :class="['header item', {disable: isSharedProject}]" + > <img class="ui mini right spaced image" :src="logo" /> <span class="desktop"> {{ APPLICATION_NAME }} @@ -122,7 +125,7 @@ </div> <div class="desktop flex push-right-desktop"> - <router-link v-if="user" to="/my_account/" class="item"> + <router-link v-if="user" :to="{name: 'my_account'}" class="item"> {{ userFullname || user.username || "Utilisateur inconnu" }} </router-link> <div @@ -157,15 +160,17 @@ <main> <div id="content" class="ui stackable grid centered container"> <transition name="fadeDownUp"> - <div v-if="messages && messages.length > 0" class="row"> + <div v-if="messages && messages.length > 0" class="row over-content"> <div class="fourteen wide column"> <div v-for="(message, index) in messages" :key="'message-' + index" - class="ui info message" + :class="['ui', message.level ? message.level : 'info', 'message']" > + <i class="close icon" @click="DISCARD_MESSAGE(message)"></i> <div class="header"> - <i class="info circle icon"></i> Informations + <i class="info circle icon"></i> + Informations </div> <ul class="list"> {{ @@ -188,8 +193,8 @@ <footer> <div class="ui compact text menu"> - <router-link to="/mentions/" class="item">Mentions légales</router-link> - <router-link to="/aide/" class="item">Aide</router-link> + <router-link :to="{name: 'mentions'}" class="item">Mentions légales</router-link> + <router-link :to="{name: 'aide'}" class="item">Aide</router-link> <p class="item">Version {{ PACKAGE_VERSION }}</p> </div> </footer> @@ -198,7 +203,7 @@ <script> import frag from "vue-frag"; -import { mapState, mapGetters } from "vuex"; +import { mapMutations, mapState, mapGetters } from "vuex"; export default { name: "App", @@ -257,9 +262,13 @@ export default { ? true : false; }, + isSharedProject() { + return this.$route.path.includes('projet-partage'); + } }, methods: { + ...mapMutations(['DISCARD_MESSAGE']), logout() { this.$store.dispatch("LOGOUT"); }, @@ -409,6 +418,11 @@ footer { transform: scale(1); } } +.ui.grid > .row.over-content { + position: absolute; + z-index: 99; + opacity: 0.95; +} .fadeDownUp-enter-active { animation: fadeInDown .5s; @@ -439,5 +453,22 @@ footer { } } +.ui.message > .close.icon { + cursor: pointer; + position: absolute; + margin: 0em; + top: 0.78575em; + right: 0.5em; + opacity: 0.7; + -webkit-transition: opacity 0.1s ease; + transition: opacity 0.1s ease; +} + </style> - \ No newline at end of file + + <style scoped> + .disable:hover { + cursor: default !important; + background-color: #373636 !important; + } + </style> \ No newline at end of file diff --git a/src/components/feature/FeatureExtraForm.vue b/src/components/feature/FeatureExtraForm.vue index c0deb2df754b5d0e73b1142a52c8d45d54e639c7..3171439c2262ff1711068dcad8abc6d2d2b21c8e 100644 --- a/src/components/feature/FeatureExtraForm.vue +++ b/src/components/feature/FeatureExtraForm.vue @@ -34,6 +34,7 @@ <div v-frag v-else-if="field.field_type === 'boolean'"> <div class="ui checkbox"> <input + class="hidden" type="checkbox" :checked="field.value" :name="field.name" diff --git a/src/components/feature/FeatureListTable.vue b/src/components/feature/FeatureListTable.vue index 5817a4fa2ab3f7acb9aadf77cef804e68c27e0be..5aa26043cf82cb948b39470a50b4cae4fc274e64 100644 --- a/src/components/feature/FeatureListTable.vue +++ b/src/components/feature/FeatureListTable.vue @@ -1,12 +1,20 @@ <template> <div data-tab="list" class="dataTables_wrapper no-footer"> - <table id="table-features" class="ui compact table"> + <table id="table-features" class="ui compact table dataTable"> <thead> <tr> - <th class="center"></th> + <th class="dt-center"> + <div @click="switchMode" class="switch-buttons pointer" :data-tooltip="`Passer en mode ${mode === 'modify' ? 'suppression':'édition'}`"> + <div><i :class="['icon pencil', {disabled: mode !== 'modify'}]"></i></div> + <span class="grey">| </span> + <div><i :class="['icon trash', {disabled: mode !== 'delete'}]"></i></div> + </div> + </th> + + <th class="dt-center"> + <div class="pointer" @click="changeSort('status')"> - <th class="center"> Statut <i :class="{ @@ -14,21 +22,23 @@ up: isSortedDesc('status'), }" class="icon sort" - @click="changeSort('status')" /> + </div> </th> - <th class="center"> - Type - <i - :class="{ - down: isSortedAsc('feature_type'), - up: isSortedDesc('feature_type'), - }" - class="icon sort" - @click="changeSort('feature_type')" - /> + <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="center"> + <th class="dt-center"> + <div class="pointer" @click="changeSort('title')"> Nom <i :class="{ @@ -36,10 +46,11 @@ up: isSortedDesc('title'), }" class="icon sort" - @click="changeSort('title')" /> + </div> </th> - <th class="center"> + <th class="dt-center"> + <div class="pointer" @click="changeSort('updated_on')"> Dernière modification <i :class="{ @@ -47,62 +58,57 @@ up: isSortedDesc('updated_on'), }" class="icon sort" - @click="changeSort('updated_on')" /> + </div> </th> - <th class="center" v-if="user"> - Auteur - <i - :class="{ - down: isSortedAsc('display_creator'), - up: isSortedDesc('display_creator'), - }" - class="icon sort" - @click="changeSort('display_creator')" - /> + <th class="dt-center" v-if="user"> + <div class="pointer" @click="changeSort('display_creator')"> + Auteur + <i + :class="{ + down: isSortedAsc('display_creator'), + up: isSortedDesc('display_creator'), + }" + class="icon sort" + /> + </div> </th> - <th class="center" v-if="user"> - Dernier éditeur - <i - :class="{ - down: isSortedAsc('display_last_editor'), - up: isSortedDesc('display_last_editor'), - }" - class="icon sort" - @click="changeSort('display_last_editor')" - /> + <th class="dt-center" v-if="user"> + <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"> - <td class="center"> + <td class="dt-center"> <div - class="ui checkbox" - :class=" - feature.properties.creator.username !== user.username && - !user.is_superuser && - !isUserProjectAdministrator - ? 'disabled' - : '' - " + :class="['ui checkbox', {disabled: !checkRights(feature)}]" > <input type="checkbox" + v-model="checked" + @input="storeClickedFeature(feature)" :id="feature.id" :value="feature.id" - v-model="checked" - :disabled=" - feature.properties.creator.username !== user.username && - !user.is_superuser && - !isUserProjectAdministrator - " + :disabled="!checkRights(feature)" + name="select" /> - <label></label> + <label for="select"></label> </div> + <!-- {{canDeleteFeature(feature)}} + {{canEditFeature(feature)}} --> </td> - <td class="center"> + <td class="dt-center"> <div v-if="feature.properties.status.value === 'archived'" data-tooltip="Archivé" @@ -128,7 +134,7 @@ <i class="orange pencil alternate icon"></i> </div> </td> - <td class="center"> + <td class="dt-center"> <router-link :to="{ name: 'details-type-signalement', @@ -140,7 +146,7 @@ {{ feature.properties.feature_type.title }} </router-link> </td> - <td class="center"> + <td class="dt-center"> <router-link :to="{ name: 'details-signalement', @@ -152,13 +158,13 @@ >{{ getFeatureDisplayName(feature) }}</router-link > </td> - <td class="center"> + <td class="dt-center"> {{ feature.properties.updated_on }} </td> - <td class="center" v-if="user"> + <td class="dt-center" v-if="user"> {{ getUserName(feature) }} </td> - <td class="center" v-if="user"> + <td class="dt-center" v-if="user"> {{ feature.properties.display_last_editor }} </td> </tr> @@ -261,18 +267,17 @@ export default { props: [ "paginatedFeatures", "checkedFeatures", + "clickedFeatures", "featuresCount", "pagination", "sort", + "mode" ], computed: { ...mapState(["user"]), ...mapGetters(["project", "permissions"]), - - isUserProjectAdministrator() { - return this.permissions.is_project_administrator; - }, + ...mapState(["user", "USER_LEVEL_PROJECTS"]), checked: { get() { @@ -304,16 +309,18 @@ export default { }, displayedPageNumbers() { + //* s'il y a moins de 5 pages, renvoyer toutes les pages + if (this.lastPageNumber < 5) return this.pageNumbers //* si la page courante est inférieur à 5, la liste commence à l'index 0 et on retourne 5 pages let firstPageInList = 0; let pagesQuantity = 5; - //* à partir de la 5ième page et jusqu'à la 4ième page avant la fin : n'afficher que 3 page entre les ellipses et la page courante doit être au milieu - if (this.pagination.currentPage >= 5 && !(this.lastPageNumber - this.pagination.currentPage < 4)) { + //* à partir de la 5ième page et jusqu'à la 4ième page avant la fin : n'afficher que 3 page entre les ellipses et la page courante doit être au milieu + if (this.pagination.currentPage >= 5 && !((this.lastPageNumber - this.pagination.currentPage) < 4)) { firstPageInList = this.pagination.currentPage - 2; pagesQuantity = 3 } - //* a partir de 4 résultat avant la fin afficher seulement les 5 derniers résultats - if (this.lastPageNumber - this.pagination.currentPage < 4) { + //* à partir de 4 résultat avant la fin afficher seulement les 5 derniers résultats + if ((this.lastPageNumber - this.pagination.currentPage) < 4) { firstPageInList = this.lastPageNumber - 5; } return this.pageNumbers.slice(firstPageInList, firstPageInList + pagesQuantity); @@ -321,6 +328,50 @@ export default { }, methods: { + storeClickedFeature(feature) { + this.clickedFeatures.push({feature_id: feature.id, feature_type: feature.properties.feature_type.slug}) + }, + + canDeleteFeature(feature) { + return feature.properties.creator.username !== this.user.username && + !this.user.is_superuser && + !this.permissions.is_project_administrator + }, + + canEditFeature(feature) { + const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug]; + const access = { + "Administrateur projet" : ["draft", "published", "archived"], + "Modérateur" : ["pending"], + "Super Contributeur" : ["draft", this.project.moderation ? "pending" : "published"], + "Contributeur" : ["draft", this.project.moderation ? "pending" : "published"], + }; + + //if (userStatus === "Super Contributeur" || userStatus === "Contributeur") { //? should super contributeur behave the same, I don't think so + if (userStatus === "Contributeur" && feature.properties.creator.username !== this.user.username) { + return false; + } else if (access[userStatus]) { + return access[userStatus].includes(feature.properties.status.value); + } else { + return false + } + }, + + checkRights(feature) { + switch (this.mode) { + case 'modify': + return this.canEditFeature(feature) + case 'delete': + return this.canDeleteFeature(feature) + } + }, + + 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 " ---- "; @@ -352,6 +403,9 @@ export default { } }, }, + destroyed() { + this.$store.commit("feature/UPDATE_CHECKED_FEATURES", []); + }, }; </script> @@ -361,6 +415,9 @@ export default { position: relative; clear: both; } +table.dataTable th.dt-center, table.dataTable td.dt-center, table.dataTable td.dataTables_empty { + text-align: center; +} .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, @@ -443,7 +500,23 @@ export default { 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; +} + +.ui.dropdown .menu .left.menu, .ui.dropdown > .left.menu .menu { + margin-right: 0 !important; +} /* Max width before this PARTICULAR table gets nasty This query will take effect for any screen smaller than 760px @@ -513,8 +586,8 @@ and also iPads specifically. content: "Auteur"; } - .center { - text-align: right !important; + table.dataTable th.dt-center, table.dataTable td.dt-center, table.dataTable td.dataTables_empty { + text-align: right; } #table-features { diff --git a/src/components/project/ProjectMappingContextLayer.vue b/src/components/project/ProjectMappingContextLayer.vue index 567d47ab2a9d226110ddd3b4574467c24247e24e..cfd82b70d16a14d08c4138848a515550dba02966 100644 --- a/src/components/project/ProjectMappingContextLayer.vue +++ b/src/components/project/ProjectMappingContextLayer.vue @@ -39,7 +39,7 @@ @click="updateLayer({ ...layer, queryable: !layer.queryable })" class="ui checkbox" > - <input type="checkbox" v-model="layer.queryable" name="queryable" /> + <input class="hidden" type="checkbox" v-model="layer.queryable" name="queryable" /> <label for="queryable"> Requêtable</label> </div> <!-- {{ form.queryable.errors }} --> diff --git a/src/router/index.js b/src/router/index.js index 4967a9621fddcfe84f3e101272358ecc6f136cb9..9aaed61eb1a304565beac70c149c901afb72e30c 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -4,6 +4,11 @@ import Projects from '../views/Projects.vue' Vue.use(VueRouter) +let projectBase = "projet" +if (window.location.pathname.includes("projet-partage")) { + projectBase = "projet-partage" +} + const routes = [ { path: '/', @@ -19,17 +24,17 @@ const routes = [ component: () => import(/* webpackChunkName: "login" */'../views/registration/Login.vue') }, { - path: '/my_account/', + path: `${projectBase === 'projet' ? '': '/' + projectBase}/my_account/`, name: 'my_account', component: () => import('../views/My_account.vue') }, { - path: '/mentions/', + path: `${projectBase === 'projet' ? '': '/' + projectBase}/mentions/`, name: 'mentions', component: () => import('../views/flatpages/with_right_menu.vue') }, { - path: '/aide/', + path: `${projectBase === 'projet' ? '': '/' + projectBase}/aide/`, name: 'aide', component: () => import('../views/flatpages/Default.vue') }, @@ -40,13 +45,13 @@ const routes = [ component: () => import('../views/project/Project_edit.vue') }, { - path: '/projet/:slug', + path: `/${projectBase}/:slug`, name: 'project_detail', props: true, component: () => import('../views/project/Project_detail.vue'), }, { - path: '/projet/:slug/editer', + path: `/${projectBase}/:slug/editer`, name: 'project_edit', component: () => import('../views/project/Project_edit.vue') }, @@ -61,65 +66,65 @@ const routes = [ component: () => import('../views/project/Project_edit.vue') }, { - path: '/projet/:slug/administration-carte/', + path: `/${projectBase}/:slug/administration-carte/`, name: 'project_mapping', component: () => import('../views/project/Project_mapping.vue') }, { - path: '/projet/:slug/membres/', + path: `/${projectBase}/:slug/membres/`, name: 'project_members', component: () => import('../views/project/Project_members.vue') }, // * FEATURE TYPE { - path: '/projet/:slug/type-signalement/ajouter/', + path: `/${projectBase}/:slug/type-signalement/ajouter/`, name: 'ajouter-type-signalement', props: true, component: () => import('../views/feature_type/Feature_type_edit.vue') }, { - path: '/projet/:slug/type-signalement/ajouter/create_from/:slug_type_signal', + path: `/${projectBase}/:slug/type-signalement/ajouter/create_from/:slug_type_signal`, name: 'dupliquer-type-signalement', component: () => import('../views/feature_type/Feature_type_edit.vue') }, { - path: '/projet/:slug/type-signalement/:feature_type_slug/', + path: `/${projectBase}/:slug/type-signalement/:feature_type_slug/`, name: 'details-type-signalement', component: () => import('../views/feature_type/Feature_type_detail.vue') }, { - path: '/projet/:slug/type-signalement/:slug_type_signal/editer/', + path: `/${projectBase}/:slug/type-signalement/:slug_type_signal/editer/`, name: 'editer-type-signalement', component: () => import('../views/feature_type/Feature_type_edit.vue') }, { - path: '/projet/:slug/type-signalement/:slug_type_signal/symbologie/', + path: `/${projectBase}/:slug/type-signalement/:slug_type_signal/symbologie/`, name: 'editer-symbologie-signalement', component: () => import('../views/feature_type/Feature_type_symbology.vue') }, // * FEATURE { - path: '/projet/:slug/signalement/lister/', + path: `/${projectBase}/:slug/signalement/lister/`, name: 'liste-signalements', component: () => import('../views/feature/Feature_list.vue') }, { - path: '/projet/:slug/type-signalement/:slug_type_signal/signalement/ajouter/', + path: `/${projectBase}/:slug/type-signalement/:slug_type_signal/signalement/ajouter/`, name: 'ajouter-signalement', component: () => import('../views/feature/Feature_edit.vue') }, { - path: '/projet/:slug/type-signalement/:slug_type_signal/signalement/:slug_signal', + path: `/${projectBase}/:slug/type-signalement/:slug_type_signal/signalement/:slug_signal`, name: 'details-signalement', component: () => import('../views/feature/Feature_detail.vue') }, { - path: '/projet/:slug/type-signalement/:slug_type_signal/offline', + path: `/${projectBase}/:slug/type-signalement/:slug_type_signal/offline`, name: 'offline-signalement', component: () => import('../views/feature/Feature_offline.vue') }, { - path: '/projet/:slug/type-signalement/:slug_type_signal/signalement/:slug_signal/editer/', + path: `/${projectBase}/:slug/type-signalement/:slug_type_signal/signalement/:slug_signal/editer/`, name: 'editer-signalement', component: () => import('../views/feature/Feature_edit.vue') }, diff --git a/src/services/feature-api.js b/src/services/feature-api.js index 30b3fb8e55ab30d617b6abf4e2564aa186f86a12..3b21882327d377f1ea9013f83801ad40519f7c49 100644 --- a/src/services/feature-api.js +++ b/src/services/feature-api.js @@ -62,6 +62,21 @@ const featureAPI = { } }, + 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}` + + const response = await axios({ + url, + method: "PATCH", + data: { id: feature_id, status: newStatus, feature_type: feature_type__slug } + }) + if (response.status === 200 && response.data) { + return response; + } else { + return null; + } + }, + async postComment({ featureId, comment }) { const response = await axios.post( `${baseUrl}features/${featureId}/comments/`, { comment } diff --git a/src/store/index.js b/src/store/index.js index 2a97c4b84c79b40715175a709792b42d26d6ed5e..661b004bc6774f414a06c74c719c65255f7985f5 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -77,13 +77,16 @@ export default new Vuex.Store({ SET_EVENTS(state, events) { state.events = events; }, - DISPLAY_MESSAGE(state, comment) { - state.messages = [{ comment }, ...state.messages]; + DISPLAY_MESSAGE(state, message) { + state.messages = [message, ...state.messages]; if (document.getElementById("content")) document.getElementById("content").scrollIntoView({ block: "start", inline: "nearest" }); setTimeout(() => { state.messages = []; }, 3000); }, + DISCARD_MESSAGE(state, message) { + state.messages = state.messages.filter((el) => el.comment !== message.comment) + }, CLEAR_MESSAGES(state) { state.messages = []; }, diff --git a/src/store/modules/feature.store.js b/src/store/modules/feature.store.js index 888710fa57035fbe1bb1f59f4cbc9f0e85c5f69a..0ae63837993517da7a8e93e8a3ff574983956226 100644 --- a/src/store/modules/feature.store.js +++ b/src/store/modules/feature.store.js @@ -172,7 +172,6 @@ const feature = { SEND_FEATURE({ state, rootState, commit, dispatch }, routeName) { commit("DISPLAY_LOADER", "Le signalement est en cours de création", { root: true }) - const message = routeName === "editer-signalement" ? "Le signalement a été mis à jour" : "Le signalement a été crée"; function redirect(featureId) { dispatch( 'GET_PROJECT_FEATURE', @@ -188,7 +187,7 @@ const feature = { params: { slug_type_signal: rootState.feature_type.current_feature_type_slug, slug_signal: featureId, - message, + message: routeName === "editer-signalement" ? "Le signalement a été mis à jour" : "Le signalement a été crée" }, }); dispatch('projects/GET_ALL_PROJECTS', null, { root:true }) //* & refresh project list @@ -201,30 +200,32 @@ const feature = { redirect(featureId); } - //* prepare feature data to send - let extraFormObject = {}; //* prepare an object to be flatten in properties of geojson - for (const field of state.extra_form) { - extraFormObject[field.name] = field.value; - } - const geojson = { - "id": state.form.feature_id, - "type": "Feature", - "geometry": state.form.geometry, - "properties": { - "title": state.form.title, - "description": state.form.description.value, - "status": state.form.status.value, - "project": rootState.project_slug, - "feature_type": rootState.feature_type.current_feature_type_slug, - ...extraFormObject + function createGeojson() { //* prepare feature data to send + let extraFormObject = {}; //* prepare an object to be flatten in properties of geojson + for (const field of state.extra_form) { + extraFormObject[field.name] = field.value; + } + return { + "id": state.form.feature_id, + "type": "Feature", + "geometry": state.form.geometry, + "properties": { + "title": state.form.title, + "description": state.form.description.value, + "status": state.form.status.value, + "project": rootState.project_slug, + "feature_type": rootState.feature_type.current_feature_type_slug, + ...extraFormObject + } } } + const geojson = createGeojson(); let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/` if (routeName === "editer-signalement") { - url += `${state.form.feature_id}/?` + - `feature_type__slug=${rootState.feature_type.current_feature_type_slug}` + - `&project__slug=${rootState.project_slug}` + url += `${state.form.feature_id}/? + feature_type__slug=${rootState.feature_type.current_feature_type_slug} + &project__slug=${rootState.project_slug}` } return axios({ diff --git a/src/views/feature/Feature_detail.vue b/src/views/feature/Feature_detail.vue index 1203b2652d9c91605698d6ffcfba74894a23a46a..bf97c9702bd6405d6966a05d84b05fd3e83242dc 100644 --- a/src/views/feature/Feature_detail.vue +++ b/src/views/feature/Feature_detail.vue @@ -504,7 +504,7 @@ export default { }, confirmComment() { - this.$store.commit("DISPLAY_MESSAGE", "Ajout du commentaire confirmé"); + this.$store.commit("DISPLAY_MESSAGE", {comment: "Ajout du commentaire confirmé", level: "positive"}); this.getFeatureEvents(); //* display new comment on the page this.comment_form.attachment_file.file = null; this.comment_form.attachment_file.fileName = ""; diff --git a/src/views/feature/Feature_list.vue b/src/views/feature/Feature_list.vue index e72194a62133f38071157354004fc5d170ad3aa3..9f08055a416e30b2b9627ff828d3ee9d23b37f13 100644 --- a/src/views/feature/Feature_list.vue +++ b/src/views/feature/Feature_list.vue @@ -36,18 +36,19 @@ feature_types.length > 0 && permissions.can_create_feature " + id="button-dropdown" class="item right" > <div - @click="showAddFeature = !showAddFeature" + @click="toggleAddFeature" class="ui dropdown button compact button-hover-green" data-tooltip="Ajouter un signalement" - data-position="bottom left" + data-position="bottom right" > <i class="plus fitted icon"></i> <div v-if="showAddFeature" - class="menu transition visible" + class="menu left transition visible" style="z-index: 9999" > <div class="header">Ajouter un signalement du type</div> @@ -67,13 +68,40 @@ </div> </div> + <div - v-if="checkedFeatures.length" + v-if="checkedFeatures.length > 0 && mode === 'modify'" + @click="toggleModifyStatus" + class="ui dropdown button compact button-hover-green margin-left-25" + data-tooltip="Modifier le statut des Signalements" + data-position="bottom right" + > + <i class="pencil fitted icon"></i> + <div + v-if="showModifyStatus" + class="menu left transition visible" + style="z-index: 9999" + > + <div class="header">Modifier le statut des Signalements</div> + <div class="scrolling menu text-wrap"> + <span + v-for="status in availableStatus" + :key="status.value" + @click="modifyStatus(status.value)" + class="item" + > + {{ status.name }} + </span> + </div> + </div> + </div> + + <div + v-if="checkedFeatures.length > 0 && mode === 'delete'" @click="modalAllDelete" class="ui button compact button-hover-red margin-left-25" data-tooltip="Effacer tous les types de signalements sélectionnés" - data-position="left center" - data-variation="mini" + data-position="bottom right" > <i class="grey trash fitted icon"></i> </div> @@ -136,12 +164,15 @@ <SidebarLayers v-if="basemaps && map" /> </div> <!-- | --> + <!-- v-on:update:clickedFeatures="handleClickedFeatures" --> <FeatureListTable v-show="!showMap" v-on:update:page="handlePageChange" v-on:update:sort="handleSortChange" :paginatedFeatures="paginatedFeatures" - :checkedFeatures.sync="checkedFeatures" + :checkedFeatures="checkedFeatures" + :clickedFeatures.sync="clickedFeatures" + :mode.sync="mode" :featuresCount="featuresCount" :pagination="pagination" :sort="sort" @@ -228,7 +259,9 @@ export default { title: null, }, baseUrl: this.$store.state.configuration.BASE_URL, + clickedFeatures: [], modalAllDeleteOpen: false, + mode: "modify", map: null, zoom: null, lat: null, @@ -250,6 +283,7 @@ export default { }, showMap: true, showAddFeature: false, + showModifyStatus: false, }; }, @@ -298,6 +332,7 @@ export default { }, computed: { + ...mapState(["user", "USER_LEVEL_PROJECTS"]), ...mapGetters([ 'project', 'permissions' ]), @@ -322,6 +357,58 @@ export default { ); }, + availableStatus() { //* attente de réponse sur le ticket + //return this.statusChoices.filter((status) => status) + + if (this.project) { + const isModerate = this.project.moderation; + const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug]; + const isOwnFeature = this.feature + ? this.feature.creator === this.user.id //* prevent undefined feature + : false; //* si le contributeur est l'auteur du signalement + if ( + //* si admin, modérateur ou super contributeur, statuts toujours disponible: Brouillon, Publié, Archivé + userStatus === "Administrateur projet" || + (userStatus === "Super Contributeur" && !isModerate) + ) { + return this.statusChoices.filter((el) => el.value !== "pending"); + } else if (userStatus === "Super Contributeur" && isModerate) { + return this.statusChoices.filter( + (el) => el.value === "draft" || el.value === "pending" + ); + } else if (userStatus === "Modérateur") { + return this.statusChoices.filter( + (el) => el.value === "draft" || el.value === "published" + ); + } else if (userStatus === "Contributeur") { + //* cas particuliers du contributeur + if ( + this.currentRouteName === "ajouter-signalement" || + !isOwnFeature + ) { + //* même cas à l'ajout d'une feature ou si feature n'a pas été créé par le contributeur + return isModerate + ? this.statusChoices.filter( + (el) => el.value === "draft" || el.value === "pending" + ) + : this.statusChoices.filter( + (el) => el.value === "draft" || el.value === "published" + ); + } else { + //* à l'édition d'une feature et si le contributeur est l'auteur de la feature + return isModerate + ? this.statusChoices.filter( + (el) => el.value !== "published" //* toutes sauf "Publié" + ) + : this.statusChoices.filter( + (el) => el.value !== "pending" //* toutes sauf "En cours de publication" + ); + } + } + } + return []; + }, + featureTypeChoices() { return this.feature_types.map((el) => el.title); }, @@ -329,15 +416,76 @@ export default { methods: { ...mapActions('feature', [ - 'GET_PROJECT_FEATURES' + 'GET_PROJECT_FEATURES', + 'SEND_FEATURE' ]), + + toggleAddFeature() { + this.showAddFeature = !this.showAddFeature; + this.showModifyStatus = false; + }, + + toggleModifyStatus() { + this.showModifyStatus = !this.showModifyStatus; + this.showAddFeature = false; + }, + modalAllDelete() { this.modalAllDeleteOpen = !this.modalAllDeleteOpen; }, + clickOutsideDropdown(e) { + if (!e.target.closest("#button-dropdown")) { + this.showModifyStatus = false; + setTimeout(() => { //* timout necessary to give time to click on link to add feature + this.showAddFeature = false; + }, 500); + } + }, + + async modifyStatus(newStatus) { + let errorCount = 0 + const promises = this.checkedFeatures.map((feature_id) => { + let feature = this.clickedFeatures.find((el) => el.feature_id === feature_id) + if (feature) { + return featureAPI.updateFeature({ + feature_id, + feature_type__slug: feature.feature_type, + project__slug: this.$route.params.slug, newStatus + }) + } else { + errorCount += 1; + } + }) + const promisesResult = await Promise.all(promises) + promisesResult.forEach((response) => { + if (response && response.data && response.status === 200) { + this.checkedFeatures.splice(this.checkedFeatures.indexOf(response.data.id), 2); + } else { + errorCount += 1; + } + }) + let message = { + comment: "Tous les signalements ont été modifié avec succès.", + level: "positive" + } + if (errorCount) { + //* display error message + if(errorCount === 1) { + message.comment = "Un signalement n'a pas pu être modifié. (Il reste sélectionné)" + } else { + message.comment = `${errorCount} signalements n'ont pas pu être modifiés. (Ils restent sélectionnés)` + } + message.level = "negative" + } + this.fetchPagedFeatures(); + this.$store.commit("DISPLAY_MESSAGE", message); + }, + + deleteFeature(feature_id) { const url = `${this.API_BASE_URL}features/${feature_id}/?project__slug=${this.project.slug}`; - axios + axios //TODO: REFACTO -> Delete function already exist in store .delete(url, {}) .then(() => { if (!this.modalAllDeleteOpen) { @@ -346,7 +494,6 @@ export default { }) .then(() => { this.fetchPagedFeatures(); - this.getNloadGeojsonFeatures(); this.checkedFeatures.splice(feature_id); }); } @@ -359,8 +506,8 @@ export default { deleteAllFeatureSelection() { let feature = {}; this.checkedFeatures.forEach((feature_id) => { - feature = { feature_id: feature_id }; - this.deleteFeature(feature.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(); }, @@ -461,8 +608,8 @@ export default { fetchPagedFeatures(newUrl) { this.onFilterChange(); //* use paginated event to watch change in filters and modify features on map let url = `${this.API_BASE_URL}projects/${this.$route.params.slug}/feature-paginated/?output=geojson&limit=${this.pagination.pagesize}&offset=${this.pagination.start}`; + //* if receiving next & previous url if (newUrl && typeof newUrl === "string") { - //* if receiving next & previous url //newUrl = newUrl.replace("8000", "8010"); //* for dev uncomment to use proxy link url = newUrl; } @@ -550,9 +697,11 @@ export default { this.initMap(); } this.fetchPagedFeatures(); + window.addEventListener("mousedown", this.clickOutsideDropdown); }, destroyed() { + window.removeEventListener("mousedown", this.clickOutsideDropdown); //* allow user to change page if ever stuck on loader this.$store.commit("DISCARD_LOADER"); }, @@ -570,10 +719,6 @@ export default { z-index: 1; } -.center { - text-align: center !important; -} - #feature-list-container { justify-content: flex-start; } @@ -600,20 +745,23 @@ export default { padding: 0 !important; } +.ui.dropdown .menu .left.menu, .ui.dropdown > .left.menu .menu { + margin-right: 0 !important; +} + @media screen and (min-width: 767px) { .twelve-wide { width: 75% !important; } } + @media screen and (max-width: 767px) { #feature-list-container > .mobile-fullwidth { width: 100% !important; } - .no-margin-mobile { margin: 0 !important; } - .no-padding-mobile { padding-top: 0 !important; padding-bottom: 0 !important; @@ -621,10 +769,12 @@ export default { .mobile-column { flex-direction: column !important; } + #button-dropdown { + transform: translate(-50px, -60px); + } #form-filters > .field.column { width: 100% !important; } - .map-container { width: 100%; } diff --git a/src/views/feature_type/Feature_type_edit.vue b/src/views/feature_type/Feature_type_edit.vue index c01b58538f9adec34edc21ffef3ce1eefc959298..823914679f737ce657ffa3fc09024a00b0e0216f 100644 --- a/src/views/feature_type/Feature_type_edit.vue +++ b/src/views/feature_type/Feature_type_edit.vue @@ -80,11 +80,13 @@ <div class="field"> <div class="ui checkbox"> <input + class="hidden" + :id="form.title_optional.html_name" :name="form.title_optional.html_name" v-model="form.title_optional.value" type="checkbox" /> - <label>{{ form.title_optional.label }}</label> + <label :for="form.title_optional.html_name">{{ form.title_optional.label }}</label> </div> </div> diff --git a/src/views/project/Project_detail.vue b/src/views/project/Project_detail.vue index 8517b9d61174957002b43aeda0724d6644b6a077..52a6604639da7a6468a906ba2079452451a08cd7 100644 --- a/src/views/project/Project_detail.vue +++ b/src/views/project/Project_detail.vue @@ -3,9 +3,6 @@ <div v-frag v-if="permissions && permissions.can_view_project && project"> <div id="message" class="fullwidth"> <div v-if="tempMessage" class="ui positive message"> - <!-- <i class="close icon"></i> --> - <!-- <div class="header">You are eligible for a reward</div> --> - <p><i class="check icon"></i> {{ tempMessage }}</p> </div> </div> @@ -48,74 +45,99 @@ >{{ project.nb_published_features_comments }} </div> </div> - <div class="ten wide column"> - <h1 class="ui header"> - <div class="content"> - {{ project.title }} - <div v-if="arraysOffline.length > 0"> - {{ arraysOffline.length }} modification<span v-if="arraysOffline.length>1">s</span> en attente + <div class="ten wide column important-flex space-between"> + <div> + <h1 class="ui header"> + {{ project.title }} + </h1> + <div class="ui hidden divider"></div> + <div class="sub header"> + {{ project.description }} + </div> + </div> + + <div class="content flex flex-column-right"> + <div class="flex flex-column-right"> + <div class="ui icon right compact buttons flex-column-right"> + <div> + <a + v-if=" + user && + permissions && + permissions.can_view_project && + isOffline() !== true + " + id="subscribe-button" + class="ui button button-hover-green" + data-tooltip="S'abonner au projet" + data-position="top center" + data-variation="mini" + @click="modalType = 'subscribe'" + > + <i class="inverted grey envelope icon"></i> + </a> + <router-link + v-if=" + permissions && + permissions.can_update_project && + isOffline() !== true + " + :to="{ name: 'project_edit', params: { slug: project.slug } }" + class="ui button button-hover-orange" + data-tooltip="Modifier le projet" + data-position="top center" + data-variation="mini" + > + <i class="inverted grey pencil alternate icon"></i> + </router-link> + <a + v-if=" + user_permissions && + user_permissions[project.slug] && + user_permissions[project.slug].is_project_administrator && + isOffline() !== true + " + id="delete-button" + class="ui button button-hover-red" + data-tooltip="Supprimer le projet" + data-position="top center" + data-variation="mini" + @click="modalType = 'deleteProject'" + > + <i class="inverted grey trash icon"></i> + </a> + </div> <button - :disabled="isOffline()" - @click="sendOfflineFeatures()" - class="ui fluid teal icon button" + v-if="user && user.is_administrator && !isSharedProject && project.generate_share_link" + class="ui teal left labeled icon button" + @click="copyLink" > - <i class="upload icon"></i> Envoyer au serveur + <i class="left icon share square"></i> + Copier le lien de partage </button> </div> - <div class="ui icon right floated compact buttons"> - <a - v-if=" - user && - permissions && - permissions.can_view_project && - isOffline() !== true - " - id="subscribe-button" - class="ui button button-hover-green" - data-tooltip="S'abonner au projet" - data-position="top center" - data-variation="mini" - @click="modalType = 'subscribe'" - > - <i class="inverted grey envelope icon"></i> - </a> - <router-link - v-if=" - permissions && - permissions.can_update_project && - isOffline() !== true - " - :to="{ name: 'project_edit', params: { slug: project.slug } }" - class="ui button button-hover-orange" - data-tooltip="Modifier le projet" - data-position="top center" - data-variation="mini" - > - <i class="inverted grey pencil alternate icon"></i> - </router-link> - <a - v-if=" - user_permissions && - user_permissions[project.slug] && - user_permissions[project.slug].is_project_administrator && - isOffline() !== true - " - id="delete-button" - class="ui button button-hover-red" - data-tooltip="Supprimer le projet" - data-position="top center" - data-variation="mini" - @click="modalType = 'deleteProject'" - > - <i class="inverted grey trash icon"></i> - </a> - </div> - <div class="ui hidden divider"></div> - <div class="sub header"> - {{ project.description }} + <div v-if="confirmMsg"> + <div class="ui positive tiny-margin message"> + <span> + Le lien a été copié dans le presse-papier + </span> + + <i class="close icon" @click="confirmMsg = ''" /> + </div> </div> </div> - </h1> + </div> + </div> + <div v-if="arraysOffline.length > 0"> + {{ arraysOffline.length }} modification<span v-if="arraysOffline.length>1">s</span> en attente + <button + :disabled="isOffline()" + @click="sendOfflineFeatures()" + class="ui fluid labeled teal icon button" + > + <i class="upload icon"></i> + Envoyer au serveur + </button> </div> </div> @@ -704,6 +726,7 @@ export default { importMessage: null, arraysOffline: [], arraysOfflineErrors: [], + confirmMsg: false, geojsonImport: [], fileToImport: { name: "", size: 0 }, slug: this.$route.params.slug, @@ -751,6 +774,9 @@ export default { }, fileSize() { return fileConvertSizeToMo(this.fileToImport.size); + }, + isSharedProject() { + return this.$route.path.includes('projet-partage'); } }, @@ -839,7 +865,10 @@ export default { return "?ver=" + Math.random(); }, getRouteUrl(url) { - return "/" + url.replace(this.$store.state.configuration.BASE_URL, ""); // remove duplicate /geocontrib + if (this.isSharedProject) { + url = url.replace("projet", "projet-partage") + } + return url.replace(this.$store.state.configuration.BASE_URL, ""); //* remove duplicate /geocontrib }, isOffline() { return navigator.onLine === false; @@ -852,6 +881,17 @@ export default { return false; }, + copyLink() { + const sharedLink = window.location.href.replace("projet", "projet-partage"); + navigator.clipboard.writeText(sharedLink).then(()=> { + console.log("success") + this.confirmMsg = true; + }, () => { + console.log("failed") + } + ) + }, + retrieveProjectInfo() { this.GET_PROJECT_INFO(this.slug) .then(() => { @@ -1121,7 +1161,6 @@ export default { /* // ! missing style in semantic.min.css, je ne comprends pas comment... */ .ui.right.floated.button { float: right; - margin: 0 0 0 1em; } .feature-type-container { @@ -1164,6 +1203,14 @@ export default { .text-left { text-align: left !important; } +.space-between { + justify-content: space-between; +} + +.flex-column-right { + flex-direction: column !important; + align-items: flex-end; +} .import-message { width: fit-content; @@ -1173,6 +1220,9 @@ export default { </style> <style scoped> +.ui.button, .ui.button .button, .tiny-margin { + margin: 0.1rem 0 0.1rem 0.1rem !important; +} .alert { color: red; } diff --git a/src/views/project/Project_edit.vue b/src/views/project/Project_edit.vue index c930db6951ebee55f17307c0971c804498aa4f3c..f8ad6836291f32b91d8060c5b17d6c57ab0218d4 100644 --- a/src/views/project/Project_edit.vue +++ b/src/views/project/Project_edit.vue @@ -158,6 +158,7 @@ <div class="field"> <div class="ui checkbox"> <input + class="hidden" type="checkbox" v-model="form.moderation" name="moderation" @@ -165,12 +166,12 @@ /> <label for="moderation">Modération</label> </div> - <!-- {{ form.moderation.errors }} --> </div> <div class="field"> <div class="ui checkbox"> <input + class="hidden" type="checkbox" v-model="form.is_project_type" name="is_project_type" @@ -178,7 +179,19 @@ /> <label for="is_project_type">Est un projet type</label> </div> - <!-- {{ form.is_project_type.errors }} --> + </div> + + <div class="field"> + <div class="ui checkbox"> + <input + class="hidden" + type="checkbox" + v-model="form.generate_share_link" + name="generate_share_link" + id="generate_share_link" + /> + <label for="generate_share_link">Génération d'un lien de partage externe</label> + </div> </div> <div class="ui divider"></div> @@ -196,12 +209,6 @@ import Dropdown from "@/components/Dropdown.vue"; import { mapState, mapGetters, mapActions } from "vuex"; -// axios.defaults.headers.common["X-CSRFToken"] = ((name) => { -// var re = new RegExp(name + "=([^;]+)"); -// var value = re.exec(document.cookie); -// return value !== null ? unescape(value[1]) : null; -// })("csrftoken"); - export default { name: "Project_edit", @@ -244,6 +251,7 @@ export default { nb_published_features_comments: 0, nb_contributors: 0, is_project_type: false, + generate_share_link: false, }, thumbnailFileSrc: "", }; @@ -452,6 +460,7 @@ export default { archive_feature: this.form.archive_feature, delete_feature: this.form.delete_feature, is_project_type: this.form.is_project_type, + generate_share_link: this.form.generate_share_link, moderation: this.form.moderation, }; let url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/`; diff --git a/src/views/project/Project_members.vue b/src/views/project/Project_members.vue index aea8f4b1340c1ea3ed81f8ee426985195632eb74..dc2dff3db450ad731794a04efdeaa16d70b010fb 100644 --- a/src/views/project/Project_members.vue +++ b/src/views/project/Project_members.vue @@ -273,11 +273,14 @@ export default { .then((response) => { if (response.status === 200) { this.$store.dispatch("GET_USER_LEVEL_PROJECTS"); //* update user status in top right menu - this.$store.commit("DISPLAY_MESSAGE", "Permissions mises à jour"); + this.$store.commit("DISPLAY_MESSAGE", {comment: "Permissions mises à jour", level: "positive"}); } else { this.$store.commit( "DISPLAY_MESSAGE", - "Une erreur s'est produite pendant la mises à jour des permissions" + { + comment : "Une erreur s'est produite pendant la mises à jour des permissions", + level: "negative" + } ); } }) diff --git a/src/views/registration/Login.vue b/src/views/registration/Login.vue index f26884b1113a2d6bb9728c6e84c49ed48826a2b5..f9614efa8e415cb53704a50200c3668d964152cb 100644 --- a/src/views/registration/Login.vue +++ b/src/views/registration/Login.vue @@ -102,7 +102,7 @@ export default { if (this.$store.state.user) { this.$store.commit( "DISPLAY_MESSAGE", - "Vous êtes déjà connecté, vous allez être redirigé vers la page d'accueil." + {comment :"Vous êtes déjà connecté, vous allez être redirigé vers la page d'accueil."} ); setTimeout(() => this.$router.push("/"), 3100); }