diff --git a/README.md b/README.md index 81e8e036e53bf71029e7ca6abe725cf993e2069f..4d0fd2340ccda019c910a4ecfc2950847435a5e0 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,11 @@ NODE_ENV=development "VUE_APP_LOGO_PATH":"/geocontrib/img/logo-neogeo-circle.png", "VUE_APP_DJANGO_BASE":"", "VUE_APP_DJANGO_API_BASE":"/geocontrib/api/", + "VUE_APP_CATALOG_NAME": "Datasud", => renseigne le nom du catalogue dans le bouton importer depuis le catalogue + "VUE_APP_IDGO": true, => si le bouton importer depuis le catalogue doit être affiché ou pas + "VUE_APP_RELOAD_INTERVAL": 15000, + "VUE_APP_DISABLE_LOGIN_BUTTON":false, + "VUE_APP_LOGIN_URL":"", "DEFAULT_BASE_MAP":{ "SERVICE": "https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png", "OPTIONS": { @@ -64,6 +69,15 @@ La commande serve lance les deux serveurs en parallèle dans le même terminal. ``` npm run serve ``` +**Pour tester les fonctionnalités d'import il faut activer redis-server et celery !** +Si redis et celery sont déjà installé, ouvrir un terminal et démarrer redis. +``` +redis-server +``` +Ouvrir un autre terminal dans le répertoire du projet backend et activer l'environnement virtuel, puis démarrer celery +``` +celery -A config worker -l info +``` ### Compiles and minifies for production ``` diff --git a/conf_apache_dev.md b/conf_apache_dev.md new file mode 100644 index 0000000000000000000000000000000000000000..03d656dfd0c6e211460a41be3ea4b98e472c4c0c --- /dev/null +++ b/conf_apache_dev.md @@ -0,0 +1,39 @@ + +# intro + +ceci permet de faire tourner le front en local sur /geocontrib +et de faire pointer /api sur n'importe quel backend (dev, local ou autre ) + +# configuration apache + +dans la configuration apache generale (httpd.conf ou commande a2enmod ), activer les modules : +* mod_headers +* mod_proxy +* mod_ssl +* mod_proxy_http + + +``` + <Location /geocontrib > + ProxyPass http://localhost:8080/geocontrib + </Location> + + + SSLProxyEngine On + <Location /api > + ProxyPass https://geocontrib.dev.neogeo.fr/geocontrib/api + RequestHeader set Referer https://geocontrib.dev.neogeo.fr/ + </Location> + ``` + + +# configuration projet vueJS + +remplacer dans le fichier config.json du projet +``` +DOMAIN":"http://localhost:8010/", par "DOMAIN":"http://localhost/", +``` +et +``` +"VUE_APP_DJANGO_API_BASE":"http://localhost:8010/api/", par "VUE_APP_DJANGO_API_BASE":"http://localhost/api/", +``` diff --git a/package-lock.json b/package-lock.json index 33c1c4d677eeb2b47707450e9731efaf4edb81eb..6a915658fd11a89d4ac5e1cfc4ff24ba785087b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "geocontrib-frontend", - "version": "2.3.2-rc2", + "version": "2.3.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8266c90418d6928c041e53696b90942f3e317965..9f160f269e2249980727f2d4bfd5165f43c18558 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geocontrib-frontend", - "version": "2.3.2-rc2", + "version": "3.0.0-rc1", "private": true, "scripts": { "serve": "npm run init-proxy & npm run init-serve", diff --git a/public/config/config.json b/public/config/config.json index 9651190e5aa2d349aead14b9d85304d92d6f2356..50e443407247901432eda2593566f6e8c0887f23 100644 --- a/public/config/config.json +++ b/public/config/config.json @@ -1,14 +1,16 @@ { "BASE_URL":"/geocontrib/", - "DOMAIN":"http://localhost:8010/", + "DOMAIN":"http://localhost/", "NODE_ENV":"development", "VUE_APP_LOCALE":"fr-FR", "VUE_APP_APPLICATION_NAME":"GéoContrib", "VUE_APP_APPLICATION_FAVICO":"/geocontrib/img/geo2f.ico", "VUE_APP_APPLICATION_ABSTRACT":"Application de saisie d'informations géographiques contributive", "VUE_APP_LOGO_PATH":"/geocontrib/img/logo-neogeo-circle.png", - "VUE_APP_DJANGO_BASE":"http://localhost:8010", + "VUE_APP_DJANGO_BASE":"http://localhost:8010/", "VUE_APP_DJANGO_API_BASE":"http://localhost:8010/api/", + "VUE_APP_CATALOG_NAME": "Datasud", + "VUE_APP_IDGO": true, "VUE_APP_RELOAD_INTERVAL": 15000, "VUE_APP_DISABLE_LOGIN_BUTTON":false, "VUE_APP_LOGIN_URL":"", @@ -30,6 +32,8 @@ }, "SELECTED_GEOCODER" : { "PROVIDER": "addok" - } + }, + "DISPLAY_FORBIDDEN_PROJECTS": true, + "DISPLAY_FORBIDDEN_PROJECTS_DEFAULT": true } \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index cb275d82cdd76fc53bb24184ca5afb6bc9b8e37f..2e19e19cef6180e34c687b222b0033c3285bf447 100644 --- a/src/App.vue +++ b/src/App.vue @@ -11,6 +11,7 @@ <div class="menu container"> <div class="ui inverted icon menu"> <router-link +<<<<<<< HEAD to="/" class="header item" > @@ -18,6 +19,12 @@ class="ui mini right spaced image" :src="logo" > +======= + :to="isSharedProject ? '' : '/'" + :class="['header item', {disable: isSharedProject}]" + > + <img class="ui mini right spaced image" :src="logo" /> +>>>>>>> develop <span class="desktop"> {{ APPLICATION_NAME }} </span> @@ -136,13 +143,22 @@ </div> </div> </div> + <div class="desktop flex push-right-desktop item title"> + <span> + {{ APPLICATION_ABSTRACT }} + </span> + </div> <div class="desktop flex push-right-desktop"> +<<<<<<< HEAD <router-link v-if="user" to="/my_account/" class="item" > +======= + <router-link v-if="user" :to="{name: 'my_account'}" class="item"> +>>>>>>> develop {{ userFullname || user.username || "Utilisateur inconnu" }} </router-link> <div @@ -191,6 +207,7 @@ class="ui stackable grid centered container" > <transition name="fadeDownUp"> +<<<<<<< HEAD <div v-if="messages && messages.length > 0" class="row" @@ -209,6 +226,19 @@ message.comment }} </ul> +======= + <div v-if="messages && messages.length > 0" class="row over-content"> + <div class="fourteen wide column"> + <div + v-for="(message, index) in messages" + :key="'message-' + index" + :class="['ui', message.level ? message.level : 'info', 'message']" + > + <i class="close icon" @click="DISCARD_MESSAGE(message)"></i> + <div class="header"> + <i class="info circle icon"></i> + Informations +>>>>>>> develop </div> </div> </div> @@ -228,6 +258,7 @@ <footer> <div class="ui compact text menu"> +<<<<<<< HEAD <router-link to="/mentions/" class="item" @@ -243,15 +274,25 @@ <p class="item"> Version {{ PACKAGE_VERSION }} </p> +======= + <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> +>>>>>>> develop </div> </footer> </div> </template> <script> +<<<<<<< HEAD import frag from 'vue-frag'; import { mapState } from 'vuex'; import { mapGetters } from 'vuex'; +======= +import frag from "vue-frag"; +import { mapMutations, mapState, mapGetters } from "vuex"; +>>>>>>> develop export default { name: 'App', @@ -271,6 +312,7 @@ export default { computed: { ...mapState([ +<<<<<<< HEAD 'projects', 'user', 'USER_LEVEL_PROJECTS', @@ -279,9 +321,26 @@ export default { 'loader', ]), ...mapGetters(['project']), +======= + "user", + "USER_LEVEL_PROJECTS", + "configuration", + "messages", + "loader", + ]), + ...mapState('projects', [ + 'projects' + ]), + ...mapGetters('projects', [ + 'project' + ]), +>>>>>>> develop APPLICATION_NAME() { return this.configuration.VUE_APP_APPLICATION_NAME; }, + APPLICATION_ABSTRACT() { + return this.$store.state.configuration.VUE_APP_APPLICATION_ABSTRACT; + }, DISABLE_LOGIN_BUTTON() { return this.configuration.VUE_APP_DISABLE_LOGIN_BUTTON; }, @@ -303,6 +362,9 @@ export default { ? true : false; }, + isSharedProject() { + return this.$route.path.includes('projet-partage'); + } }, created() { @@ -314,6 +376,7 @@ export default { }, methods: { + ...mapMutations(['DISCARD_MESSAGE']), logout() { this.$store.dispatch('LOGOUT'); }, @@ -433,6 +496,10 @@ footer { transition: none !important; } +.item.title::before { + background: none !important; +} + .bounce-enter-active { animation: bounce-in .5s; @@ -451,6 +518,11 @@ footer { transform: scale(1); } } +.ui.grid > .row.over-content { + position: absolute; + z-index: 99; + opacity: 0.95; +} .fadeDownUp-enter-active { animation: fadeInDown .5s; @@ -481,5 +553,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/assets/js/map-util.js b/src/assets/js/map-util.js index 2552bfeb211dc6b03245b4ddd4feb70ab05d6ecb..4c2434bff8808af4071d7c522c5fc2ff601962a0 100644 --- a/src/assets/js/map-util.js +++ b/src/assets/js/map-util.js @@ -307,18 +307,6 @@ const mapUtil = { color.value : typeof color === 'string' && color.length ? color : '#000000'; - // Look for a custom field - let customField; - let customFieldOption; - if ( - featureType.customfield_set && - Object.keys(properties).some(el => featureType.customfield_set.map(e => e.name).includes(el)) - ) { - customField = Object.keys(properties) - .filter(el => featureType.customfield_set.map(e => e.name).includes(el)); - customFieldOption = properties[customField[0]]; - } - const hiddenStyle = ({ radius: 0, fillOpacity: 0.5, @@ -327,38 +315,14 @@ const mapUtil = { color: featureType.color, }); - let defaultStyle; - if ( - featureType.colors_style.value.icons[customFieldOption] && - featureType.colors_style.value.icons[customFieldOption] !== 'circle' - ) { - const iconHTML = ` - <i - class="fas fa-${featureType.colors_style.value.icons[customFieldOption]} fa-lg" - style="color: ${colorValue}" - ></i> - `; - const customMapIcon = L.divIcon({ - html: iconHTML, - iconSize: [20, 20], - className: 'myDivIcon', - }); - - defaultStyle = { - icon: customMapIcon - }; - - } else { - - defaultStyle = { - radius: 4, - fillOpacity: 0.5, - weight: 3, - fill: true, - color: color, - }; + const defaultStyle = { + radius: 4, + fillOpacity: 0.5, + weight: 3, + fill: true, + color: colorValue, + }; - } // Filtre sur le feature type if (form_filters && form_filters.type.selected) { diff --git a/src/assets/styles/base.css b/src/assets/styles/base.css index 2197771371abbb1734a690797c92376429a94a55..e2719af5059e5893dc3237d738025c5ea9d91434 100644 --- a/src/assets/styles/base.css +++ b/src/assets/styles/base.css @@ -19,6 +19,13 @@ main { .margin-top { margin-top: 1rem; } +/* ---------------------------------- */ + /* UTILS */ +/* ---------------------------------- */ +.ellipsis { + text-overflow: ellipsis; + overflow: hidden; +} /* ---------------------------------- */ /* MAIN */ /* ---------------------------------- */ @@ -170,7 +177,57 @@ footer .ui.text.menu .item:not(:first-child) { } /* ---------------------------------- */ - /* ERROR LIST */ + /* PAGINATION */ +/* ---------------------------------- */ + +.custom-pagination { + display: flex; + align-items: center; + list-style: none; + font-size: 1.3em; +} + +.custom-pagination > .page-item > .page-link { + border: none; + font-weight: 400; + color: #008080; +} +.custom-pagination > .page-item.active > .page-link { + color: #008080; + background-color: transparent; + font-weight: bolder; + text-shadow: 0 0 2px #008080; + padding: 0.325em 0.75em; + pointer-events: none; +} +.custom-pagination > .page-item.disabled > .page-link { + opacity: 0.5; + pointer-events: none; +} + +.custom-pagination > div > .page-item > .page-link { + border: none; + font-weight: 400; + color: #008080; + padding: 0.325em 0.75em; +} +.custom-pagination > div > .page-item.active > .page-link { + color: #008080; + background-color: transparent; + font-weight: bolder; + font-size: 1.2em; + text-shadow: 0 0 2px #008080; + padding: 0.325em 0.75em; + pointer-events: none; +} +.custom-pagination > div > .page-item.disabled > .page-link { + opacity: 0.5; + padding: 0.325em 0.75em; + pointer-events: none; +} + +/* ---------------------------------- */ + /* MULTISELECT */ /* ---------------------------------- */ .multiselect__tags { border: 2px solid #ced4da; @@ -211,4 +268,15 @@ footer .ui.text.menu .item:not(:first-child) { background-color: #fff; opacity: 1; top: 2px; +} + +#search-projects > .multiselect > .multiselect__tags { + border-radius: 0 !important; +} + +.menu.projects > .item > .multiselect { + min-height: 0px !important; +} +.menu.projects > .item > .multiselect > .multiselect__tags { + min-height: 0px !important; } \ No newline at end of file diff --git a/src/components/ImportTask.vue b/src/components/ImportTask.vue index e8a233b48a51e9c1810c27e6cdc75b388be434b2..677f09ff485650315f27e662a90e645f7069d3f4 100644 --- a/src/components/ImportTask.vue +++ b/src/components/ImportTask.vue @@ -51,8 +51,8 @@ data-tooltip="Statut en attente. Clickez pour rafraichir." > <i - :class="['orange icon', ready ? 'sync' : 'hourglass half']" @click="fetchImports()" + :class="['orange icon', ready && !reloading ? 'sync' : 'hourglass half rotate']" /> </span> </td> @@ -67,6 +67,14 @@ import { mapState } from 'vuex'; export default { + data() { + return { + open: false, + ready: true, + }; + }, + + props: ["data", "reloading"], filters: { setDate: function (value) { @@ -135,23 +143,19 @@ export default { padding-top: 1em; } -@keyframes rotateIn { - from { - transform: rotate3d(0, 0, 1, -200deg); - opacity: 0; - } - - to { - transform: translate3d(0, 0, 0); - opacity: 1; - } +i.icon { + width: 20px !important; + height: 20px !important; } -.rotateIn { - animation-name: rotateIn; - transform-origin: center; - animation: 2s; +.rotate { + -webkit-animation:spin 1s linear infinite; + -moz-animation:spin 1s linear infinite; + animation:spin 1s linear infinite; } +@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } +@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } +@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } } /* Max width before this PARTICULAR table gets nasty diff --git a/src/components/Pagination.vue b/src/components/Pagination.vue new file mode 100644 index 0000000000000000000000000000000000000000..dfee522be8eeed3dd10e7e80bd988f12bf79ca0f --- /dev/null +++ b/src/components/Pagination.vue @@ -0,0 +1,137 @@ +<template> + <div style="display: flex;"> + <nav style="margin: 0 auto;"> + <ul class="custom-pagination"> + <li + class="page-item" + :class="{ disabled: page === 1 }" + > + <a + class="page-link" + href="#" + @click="page -= 1" + > + <i class="ui icon big angle left" /> + </a> + </li> + <div v-if="nbPages > 5" style="display: contents;"> + <li + v-for="index in pagination(page, nbPages)" + :key="index" + class="page-item" + :class="{ active: page === index }" + > + <a + class="page-link" + href="#" + @click="changePage(index)" + > + {{ index }} + </a> + </li> + </div> + <div v-else style="display: contents;"> + <li + v-for="index in nbPages" + :key="index" + class="page-item" + :class="{ active: page === index }" + > + <a + class="page-link" + href="#" + @click="page = index" + > + {{ index }} + </a> + </li> + </div> + <li + class="page-item" + :class="{ disabled: page === nbPages }" + > + <a + class="page-link" + href="#" + @click="page += 1" + > + <i class="ui icon big angle right" /> + </a> + </li> + </ul> + </nav> + </div> +</template> + +<script> +export default { + name: 'Pagination', + + props: { + nbPages: { + type: Number, + default: 1 + }, + + onPageChange: { + type: Function, + default: () => { + return () => 1; + } + } + }, + + data() { + return { + page: 1 + } + }, + + watch: { + page: function(newValue, oldValue) { + if (newValue !== oldValue) { + this.onPageChange(newValue); + this.$emit('change-page', newValue); + } + } + }, + + methods: { + pagination(c, m) { + const current = c, + last = m, + delta = 2, + left = current - delta, + right = current + delta + 1, + range = [], + rangeWithDots = []; + let l; + + for (let i = 1; i <= last; i++) { + if (i === 1 || i === last || i >= left && i < right) { + range.push(i); + } + } + for (const i of range) { + if (l) { + if (i - l === 2) { + rangeWithDots.push(l + 1); + } else if (i - l !== 1) { + rangeWithDots.push('...'); + } + } + rangeWithDots.push(i); + l = i; + } + + return rangeWithDots; + }, + + changePage(num) { + if (typeof num === 'number') { + this.page = num; + } + } + } +} +</script> diff --git a/src/components/Projects/DropdownMenuItem.vue b/src/components/Projects/DropdownMenuItem.vue new file mode 100644 index 0000000000000000000000000000000000000000..ae0ebdb63ad019c1cee232e43df0c10b6de8c580 --- /dev/null +++ b/src/components/Projects/DropdownMenuItem.vue @@ -0,0 +1,89 @@ +<template> + <Multiselect + v-model="selection" + :options="options" + :allow-empty="true" + track-by="label" + label="label" + :reset-after="false" + select-label="" + selected-label="" + deselect-label="" + :searchable="false" + :placeholder="placeholder" + :clear-on-select="false" + :preserve-search="true" + @select="select" + @close="close" + > + <!-- <template slot="clear"> + <div + v-if="selection" + class="multiselect__clear" + @click.prevent.stop="selection = options[0]" + > + <i class="close icon"></i> + </div> + </template> --> + </Multiselect> +</template> + +<script> +import Multiselect from 'vue-multiselect'; + +export default { + name: 'DropdownMenuItem', + + components: { + Multiselect + }, + + props: { + placeholder: { + type: String, + default: 'Sélectionnez une valeur' + }, + options: { + type: Array, + default: () => { + return []; + } + } + }, + + data() { + return { + selection: null, + } + }, + + watch: { + selection: { + deep: true, + handler(newValue) { + if (!newValue) { + this.selection = this.options[0]; + this.$emit('filter', this.selection); + } + } + } + }, + + created() { + this.selection = this.options[0]; + }, + + methods: { + select(e) { + this.$emit('filter', e); + }, + close() { + this.$emit('close', this.selection); + } + } +} +</script> + +<style lang="less" scoped> + +</style> diff --git a/src/components/Projects/ProjectsMenu.vue b/src/components/Projects/ProjectsMenu.vue new file mode 100644 index 0000000000000000000000000000000000000000..9baad025de922013a9c43dabde877b0ebfa66a3a --- /dev/null +++ b/src/components/Projects/ProjectsMenu.vue @@ -0,0 +1,223 @@ +<template> + <div class="filters-container"> + <div class="ui styled accordion"> + <div class="title collapsible-filters"> + FILTRES + <i + :class="isFiltersVisible ? 'caret down' : 'caret right'" + class="ui icon" + /> + </div> + </div> + <div + :class="isFiltersVisible ? '' : 'hidden'" + class="ui menu filters" + > + <div class="item"> + <label> + Niveau d'autorisation requis + </label> + <DropdownMenuItem + :options="accessLevelOptions" + v-on="$listeners" + /> + </div> + <div class="item"> + <label> + Mon niveau d'autorisation + </label> + <DropdownMenuItem + :options="userAccessLevelOptions" + v-on="$listeners" + /> + </div> + <div class="item"> + <label> + Modération + </label> + <DropdownMenuItem + :options="moderationOptions" + v-on="$listeners" + /> + </div> + <div class="right item"> + <label> + Rechercher un projet + </label> + <search-projects + :search-function="SEARCH_PROJECTS" + /> + </div> + </div> + </div> +</template> + +<script> +import { mapActions } from 'vuex'; + +import DropdownMenuItem from '@/components/Projects/DropdownMenuItem.vue'; +import SearchProjects from '@/components/Projects/SearchProjects.vue'; + +export default { + name: 'ProjectsMenu', + + components: { + DropdownMenuItem, + SearchProjects, + }, + + data() { + return { + isFiltersVisible: false, + moderationOptions: [ + { + label: 'Tous', + filter: 'moderation', + value: null + }, + { + label: 'Projet modéré', + filter: 'moderation', + value: 'true' + }, + { + label: 'Projet non modéré', + filter: 'moderation', + value: 'false' + }, + ], + accessLevelOptions: [ + { + label: 'Tous', + filter: 'access_level', + value: null + }, + { + label: 'Utilisateur connecté', + filter: 'access_level', + value: 'logged_user' + }, + { + label: 'Contributeur', + filter: 'access_level', + value: 'contributor' + }, + { + label: 'Modérateur', + filter: 'access_level', + value: 'moderator' + }, + { + label: 'Administrateur projet', + filter: 'access_level', + value: 'admin' + }, + ], + userAccessLevelOptions: [ + { + label: 'Tous', + filter: 'user_access_level', + value: null + }, + { + label: 'Utilisateur connecté', + filter: 'user_access_level', + value: '1' + }, + { + label: 'Contributeur', + filter: 'user_access_level', + value: '2' + }, + { + label: 'Modérateur', + filter: 'user_access_level', + value: '3' + }, + { + label: 'Administrateur projet', + filter: 'user_access_level', + value: '4' + }, + ] + } + }, + + mounted() { + const el = document.getElementsByClassName('collapsible-filters'); + + el[0].addEventListener('click', function() { + const content = document.getElementsByClassName('filters'); + content[0].classList.toggle('hidden'); + if (content[0].style.maxHeight){ + content[0].style.maxHeight = null; + } else { + content[0].style.maxHeight = content[0].scrollHeight + 5 + "px"; + } + }); + }, + + methods: { + ...mapActions('projects', [ + 'SEARCH_PROJECTS' + ]) + } +} +</script> + +<style lang="less" scoped> +.transition-properties(...) { + -webkit-transition: @arguments; + -moz-transition: @arguments; + -o-transition: @arguments; + transition: @arguments; +} + +.filters-container { + width: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-end; + .accordion { + width: fit-content; + .collapsible-filters { + cursor: pointer; + font-size: 1.25em; + padding-right: 0; + } + } + .filters { + width: 100%; + height:auto; + min-height: 0; + max-height:75px; + margin: 0 0 1em 0; + .transition-properties(max-height 0.2s ease-out;); + .item { + display: flex; + flex-direction: column; + align-items: flex-start !important; + + padding: 0.4em 0.6em; + + label { + margin-bottom: 0.2em; + font-size: 0.9em; + font-weight: 600; + } + } + .item { + width: 25%; + } + .right.item::before { + width: 0; + } + } + .filters.hidden { + max-height: 0; + overflow: hidden; + border: none; + } +} +</style> diff --git a/src/components/Projects/SearchProjects.vue b/src/components/Projects/SearchProjects.vue new file mode 100644 index 0000000000000000000000000000000000000000..f27e41221f2bffb680cb169fe9ce04c637c4549c --- /dev/null +++ b/src/components/Projects/SearchProjects.vue @@ -0,0 +1,67 @@ +<template> + <div id="search-projects"> + <input + v-model="text" + type="search" + placeholder="Rechercher..." + > + </div> +</template> + +<script> +import _ from 'lodash'; +import { mapMutations } from 'vuex'; + +export default { + name: 'SearchProjects', + + props: { + searchFunction: { + type: Function, + default: () => { return {} } + } + }, + + data() { + return { + text: null + } + }, + + watch: { + text: _.debounce(function(newValue) { + this.$emit('loading', true); + this.SET_CURRENT_PAGE(1); + this.searchFunction(newValue) + .then(() => { + this.$emit('loading', false); + }); + }, 100) + }, + + methods: { + ...mapMutations('projects', [ + 'SET_CURRENT_PAGE' + ]) + } +} +</script> + +<style lang="less" scoped> +#search-projects { + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + input { + width: 100%; + height: 98%; + text-align: left; + color: #35495e; + padding: 8px 40px 0 8px; + border-radius: 5px; + border: 1px solid #e8e8e8; + font-size: 14px; + } +} +</style> \ No newline at end of file diff --git a/src/components/feature/FeatureExtraForm.vue b/src/components/feature/FeatureExtraForm.vue index d4e10cf5acc33f27ed850117d93ebaa987e891b6..33a2216c102d46e9027cf8a67ce5a96535311651 100644 --- a/src/components/feature/FeatureExtraForm.vue +++ b/src/components/feature/FeatureExtraForm.vue @@ -47,6 +47,7 @@ <div class="ui checkbox"> <input :id="field.name" + 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 283bbdd2533188cc705b5edee73553683758182f..cb4827855954eed4c49bcb1dc770abc0e8bb266b 100644 --- a/src/components/feature/FeatureListTable.vue +++ b/src/components/feature/FeatureListTable.vue @@ -1,17 +1,19 @@ <template> - <div - data-tab="list" - class="dataTables_wrapper no-footer" - > - <table - id="table-features" - class="ui compact table" - > + <div data-tab="list" class="dataTables_wrapper no-footer"> + <table id="table-features" class="ui compact table dataTable"> <thead> <tr> - <th class="center" /> + <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="{ @@ -19,21 +21,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="{ @@ -41,10 +45,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="{ @@ -52,71 +57,55 @@ up: isSortedDesc('updated_on'), }" class="icon sort" - @click="changeSort('updated_on')" /> + </div> </th> - <th - v-if="user" - class="center" - > - 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 - v-if="user" - class="center" - > - 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"> + <tr v-for="(feature, index) in paginatedFeatures" :key="index"> + <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 - :id="feature.id" - v-model="checked" type="checkbox" + v-model="checked" + @input="storeClickedFeature(feature)" + :id="feature.id" :value="feature.id" - :disabled=" - feature.properties.creator.username !== user.username && - !user.is_superuser && - !isUserProjectAdministrator - " - > - <label /> + :disabled="!checkRights(feature)" + name="select" + /> + <label for="select"></label> </div> </td> - <td class="center"> + <td class="dt-center"> <div v-if="feature.properties.status.value === 'archived'" data-tooltip="Archivé" @@ -142,7 +131,7 @@ <i class="orange pencil alternate icon" /> </div> </td> - <td class="center"> + <td class="dt-center"> <router-link :to="{ name: 'details-type-signalement', @@ -154,7 +143,7 @@ {{ feature.properties.feature_type.title }} </router-link> </td> - <td class="center"> + <td class="dt-center"> <router-link :to="{ name: 'details-signalement', @@ -167,19 +156,13 @@ {{ getFeatureDisplayName(feature) }} </router-link> </td> - <td class="center"> + <td class="dt-center"> {{ feature.properties.updated_on }} </td> - <td - v-if="user" - class="center" - > + <td class="dt-center" v-if="user"> {{ getUserName(feature) }} </td> - <td - v-if="user" - class="center" - > + <td class="dt-center" v-if="user"> {{ feature.properties.display_last_editor }} </td> </tr> @@ -291,6 +274,10 @@ export default { type: Array, default: null, }, + clickedFeatures: { + type: Array, + default: null, + }, featuresCount: { type: Number, default: 0, @@ -303,15 +290,16 @@ export default { type: Object, default: null, }, + mode: { + type: String, + default: null, + } }, computed: { ...mapState(['user']), ...mapGetters(['project', 'permissions']), - - isUserProjectAdministrator() { - return this.permissions.is_project_administrator; - }, + ...mapState(["user", "USER_LEVEL_PROJECTS"]), checked: { get() { @@ -343,16 +331,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)) { + 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); @@ -360,6 +350,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 ' ---- '; @@ -389,6 +423,9 @@ export default { } }, }, + destroyed() { + this.$store.commit("feature/UPDATE_CHECKED_FEATURES", []); + }, }; </script> @@ -398,6 +435,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, @@ -480,7 +520,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 @@ -550,8 +606,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/feature_type/SymbologySelector.vue b/src/components/feature_type/SymbologySelector.vue index 8bcd60d2e8feb483528009c68226242d5d208159..2aa443c1bc9b187b02b37654ca4d37e926750ff9 100644 --- a/src/components/feature_type/SymbologySelector.vue +++ b/src/components/feature_type/SymbologySelector.vue @@ -14,7 +14,7 @@ :name="form.color.html_name" > </div> - <div class="required inline field"> + <!-- <div class="required inline field"> <label>Symbole</label> <button class="ui icon button picker-button" @@ -27,7 +27,7 @@ class="icon alt" /> </button> - </div> + </div> --> </div> <div ref="iconsPickerModal" diff --git a/src/components/project/ProjectMappingContextLayer.vue b/src/components/project/ProjectMappingContextLayer.vue index be25f9ad49e1ca474af338c6a75217db80f8c94c..46d0968998eba574c939f5578fe4a5b6b7d771e6 100644 --- a/src/components/project/ProjectMappingContextLayer.vue +++ b/src/components/project/ProjectMappingContextLayer.vue @@ -43,6 +43,7 @@ > <input :checked="layer.queryable" + class="hidden" type="checkbox" name="queryable" > diff --git a/src/main.js b/src/main.js index 3a76fc5c6ad8fd5ff58411143abcad82a2b9248b..a27c6cb2d15b6181399ab503ad0da2ff0e07d2fa 100644 --- a/src/main.js +++ b/src/main.js @@ -1,10 +1,17 @@ const axios = require('axios'); import Vue from 'vue'; +<<<<<<< HEAD import App from './App.vue'; import './registerServiceWorker'; import router from './router'; import store from './store'; +======= +import App from './App.vue' +import './registerServiceWorker' +import router from '@/router' +import store from '@/store' +>>>>>>> develop import 'leaflet/dist/leaflet.css'; import 'leaflet-draw/dist/leaflet.draw.css'; import '@/assets/resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.css'; @@ -47,6 +54,7 @@ let onConfigLoaded = function(config){ link.href = config.VUE_APP_APPLICATION_FAVICO; document.head.appendChild(link); +<<<<<<< HEAD window.proxy_url=config.VUE_APP_DJANGO_API_BASE+'proxy/'; axios.all([store.dispatch('USER_INFO'), store.dispatch('GET_ALL_PROJECTS'), @@ -55,6 +63,17 @@ let onConfigLoaded = function(config){ store.dispatch('map/GET_AVAILABLE_LAYERS'), store.dispatch('GET_USER_LEVEL_PERMISSIONS'), store.dispatch('GET_LEVELS_PERMISSIONS'), +======= + window.proxy_url=config.VUE_APP_DJANGO_API_BASE+"proxy/"; + axios.all([ + store.dispatch("USER_INFO"), + store.dispatch('projects/GET_PROJECTS'), + store.dispatch("GET_STATIC_PAGES"), + store.dispatch("GET_USER_LEVEL_PROJECTS"), + store.dispatch("map/GET_AVAILABLE_LAYERS"), + store.dispatch("GET_USER_LEVEL_PERMISSIONS"), + store.dispatch("GET_LEVELS_PERMISSIONS"), +>>>>>>> develop ]).then(axios.spread(function () { new Vue({ router, diff --git a/src/router/index.js b/src/router/index.js index be9ac2c3ed83324d2ff5de4926a6273ae18186b8..fef7a994cf3773a7e1d12bbd6a54fc611740e2a8 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,14 +1,19 @@ -import Vue from 'vue'; -import VueRouter from 'vue-router'; -import Index from '../views/Index.vue'; +import Vue from 'vue' +import VueRouter from 'vue-router' +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: '/', name: 'index', - component: Index + component: Projects }, { path: '/connexion/', @@ -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,69 +66,75 @@ 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') }, + { + path: '/projet/:slug/catalog/:feature_type_slug', + name: 'catalog-import', + component: () => import('../views/Catalog.vue') + }, + { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('../views/NotFound.vue') }, ]; //let routerHistory = []; diff --git a/src/services/feature-api.js b/src/services/feature-api.js index 9eec2425700a40faf7f1ab0cf21efd207a53e79d..465542baf477ef212fd3fb252f6ea710f086f8a8 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/services/featureType-api.js b/src/services/featureType-api.js new file mode 100644 index 0000000000000000000000000000000000000000..dca96343ae4d474d3b612c83ce585912162f40ae --- /dev/null +++ b/src/services/featureType-api.js @@ -0,0 +1,22 @@ +import axios from "@/axios-client.js"; +import store from '../store' + +const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; + +const featureTypeAPI = { + async deleteFeatureType(featureType_slug) { + const response = await axios.delete( + `${baseUrl}feature-types/${featureType_slug}` + ); + if ( + response.status === 204 + ) { + return 'success' + } else { + return null; + } + }, + +} + +export default featureTypeAPI; diff --git a/src/services/misc-api.js b/src/services/misc-api.js new file mode 100644 index 0000000000000000000000000000000000000000..64750eb4a78f7850a13a4b352056ec95165d4bed --- /dev/null +++ b/src/services/misc-api.js @@ -0,0 +1,43 @@ +import axios from '@/axios-client.js'; +import store from '../store'; + + +const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; + +const miscAPI = { + + async getIdgoCatalog(username) { + try { + const response = await axios.get( + `${baseUrl}idgo-catalog/?user=${username}` + ); + if ( + response.status === 200 && + response.data + ) { + return response.data; + } else { + return null; + } + } catch (err) { + return err; + } + }, + + async getExternalGeojson(queryParams) { + const response = await axios.get( + `${baseUrl}external-geojson/${queryParams ? '?' + queryParams : ""}` + ); + if ( + response.status === 200 && + response.data + ) { + return response.data; + } else { + return null; + } + }, + +} + +export default miscAPI; diff --git a/src/services/project-api.js b/src/services/project-api.js index 7ee8d6323912a1da78f017784e6a68a310b834a3..94255cdd150eeb0470fa6039d40f019ffa9523e9 100644 --- a/src/services/project-api.js +++ b/src/services/project-api.js @@ -1,13 +1,8 @@ import axios from '@/axios-client.js'; -import store from '../store'; - - - - -const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const projectAPI = { - async getProjectSubscription({ projectSlug }) { + + async getProjectSubscription({ baseUrl, projectSlug }) { const response = await axios.get( `${baseUrl}projects/${projectSlug}/subscription/` ); @@ -21,7 +16,7 @@ const projectAPI = { } }, - async subscribeProject({ projectSlug, suscribe }) { + async subscribeProject({ baseUrl, projectSlug, suscribe }) { const response = await axios.put( `${baseUrl}projects/${projectSlug}/subscription/`, { is_suscriber: suscribe } @@ -35,6 +30,41 @@ const projectAPI = { return null; } }, -}; + + async getProjects(baseUrl, filters, page) { + 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]}`); + } + } + } + + const response = await axios.get(filteredUrl ? filteredUrl : url); + if (response.status === 200 && response.data) { + return response.data; + } + } catch (error) { + console.error(error); + throw error; + } + }, + + async deleteProject(baseUrl, projectSlug) { + const response = await axios.delete( + `${baseUrl}projects/${projectSlug}` + ); + if ( response.status === 204 ) { + return 'success'; + } else { + return null; + } + }, +} export default projectAPI; diff --git a/src/store/index.js b/src/store/index.js index 77664d54cb35307ce274e88eef2b2793d93ced23..90373bf5e18e554792637dbe5d6e950e78519879 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,10 +1,8 @@ import axios from '@/axios-client.js'; import Vue from 'vue'; import Vuex from 'vuex'; -import router from '../router'; -import feature_type from './modules/feature_type'; -import feature from './modules/feature'; -import map from './modules/map'; +import router from '../router' +import modules from './modules'; Vue.use(Vuex); @@ -24,17 +22,12 @@ const noPermissions = { }; export default new Vuex.Store({ - modules: { - feature_type, - feature, - map - }, + modules, + state: { logged: false, user: false, configuration: null, - project_slug: null, - projects: [], last_comments: [], staticPages: null, USER_LEVEL_PROJECTS: null, @@ -51,15 +44,6 @@ export default new Vuex.Store({ }, mutations: { - SET_PROJECTS(state, projects) { - state.projects = projects; - }, - ADD_PROJECT(state, project) { - state.projects = [project, ...state.projects]; - }, - SET_PROJECT_SLUG(state, slug) { - state.project_slug = slug; - }, SET_USER(state, payload) { state.user = payload; }, @@ -93,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 = []; }, @@ -130,30 +117,10 @@ export default new Vuex.Store({ }, getters: { - project: state => state.projects.find((project) => project.slug === state.project_slug), - permissions: state => state.user_permissions ? state.user_permissions[state.project_slug] : noPermissions, - project_types: state => state.projects.filter(projet => projet.is_project_type), - project_user: state => state.projects.filter(projet => projet.creator === state.user.id), + permissions: state => state.user_permissions ? state.user_permissions[state.projects.project_slug] : noPermissions, }, actions: { - GET_ALL_PROJECTS({ commit }) { - function parseDate(date) { - let dateArr = date.split('/').reverse(); - return new Date(dateArr[0], dateArr[1] - 1, dateArr[2]); - } - return axios - .get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}projects/`) - .then((response) => { - if (response.status === 200 && response.data) { - const orderedProjects = response.data.sort((a, b) => parseDate(b.created_on) - parseDate(a.created_on)); - commit('SET_PROJECTS', orderedProjects); - } - }) - .catch((error) => { - throw error; - }); - }, GET_STATIC_PAGES({ commit }) { return axios .get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}flat-pages/`) @@ -297,7 +264,7 @@ export default new Vuex.Store({ }, async GET_PROJECT_INFO({ state, commit, dispatch }, slug) { - commit('SET_PROJECT_SLUG', slug); + commit('projects/SET_PROJECT_SLUG', slug, { root: true }); let promises = [ dispatch('GET_PROJECT_LAST_MESSAGES', slug).then(response => response), dispatch('feature_type/GET_PROJECT_FEATURE_TYPES', slug).then(response => response), diff --git a/src/store/modules/feature.js b/src/store/modules/feature.store.js similarity index 90% rename from src/store/modules/feature.js rename to src/store/modules/feature.store.js index 0e7e8592e38eb4c0e3713783bf924fa6bd4e51ff..71e90546837668337b5babaac08f4dfb5eb20555 100644 --- a/src/store/modules/feature.js +++ b/src/store/modules/feature.store.js @@ -179,7 +179,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', @@ -195,10 +194,10 @@ 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('GET_ALL_PROJECTS', null, { root:true }); //* & refresh project list + dispatch('projects/GET_ALL_PROJECTS', null, { root:true }) //* & refresh project list }); } @@ -208,30 +207,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 + } } }; - 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}`; + 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}` } return axios({ diff --git a/src/store/modules/feature_type.js b/src/store/modules/feature_type.store.js similarity index 80% rename from src/store/modules/feature_type.js rename to src/store/modules/feature_type.store.js index 31221ca56e5b68e135921afe9d08dc8665575b00..b2488f889fc6a069522f201f468a5b279ba75910 100644 --- a/src/store/modules/feature_type.js +++ b/src/store/modules/feature_type.store.js @@ -166,50 +166,47 @@ const feature_type = { }, async SEND_FEATURES_FROM_GEOJSON({ state, dispatch, rootGetters }, payload) { - const { feature_type_slug } = payload; + let { feature_type_slug, geojson } = payload; + //* check if geojson then build a file + if(!geojson && !state.fileToImport && state.fileToImport.size === 0 ) return + let formData = new FormData(); + let fileToImport; + const {name, type} = geojson || state.fileToImport; - if (state.fileToImport.size > 0) { - let formData = new FormData(); - - if (!rootGetters.project.moderation) { + if (!rootGetters.project.moderation) { + if (state.fileToImport && state.fileToImport.size > 0) { //* if data in a binary file, read it as text const textFile = await state.fileToImport.text(); - const geojson = JSON.parse(textFile); - const unmoderatedFeatures = pending2draftFeatures(geojson.features); - const newGeojson= { - type: 'FeatureCollection', features: unmoderatedFeatures - }; - const newFile = new File( - [JSON.stringify(newGeojson)], - state.fileToImport.name, - { type: state.fileToImport.type } - ); - formData.append('json_file', newFile); - } else { - formData.append('json_file', state.fileToImport); + geojson = JSON.parse(textFile); } - - formData.append('feature_type_slug', feature_type_slug); - let url = - this.state.configuration.VUE_APP_DJANGO_API_BASE + - 'import-tasks/'; - return axios - .post(url, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) - .then((response) => { - if (response && response.status === 200) { - dispatch('GET_IMPORTS', { - feature_type: feature_type_slug - }); - } - return response; - }) - .catch((error) => { - throw (error); - }); + const unmoderatedFeatures = pending2draftFeatures(geojson.features); + geojson= { + "type": "FeatureCollection", "features": unmoderatedFeatures + }; } + fileToImport = new File([JSON.stringify(geojson)], name, {type}); + + formData.append('json_file', geojson ? fileToImport : state.fileToImport); + formData.append('feature_type_slug', feature_type_slug); + let url = + this.state.configuration.VUE_APP_DJANGO_API_BASE + + 'import-tasks/' + return axios + .post(url, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then((response) => { + if (response && response.status === 200) { + return dispatch("GET_IMPORTS", { + feature_type: feature_type_slug + }); + } + return response + }) + .catch((error) => { + throw (error); + }); }, GET_IMPORTS({ commit }, { project_slug, feature_type }) { @@ -220,12 +217,13 @@ const feature_type = { if (feature_type) { url = url.concat('', `${url.includes('?') ? '&' : '?'}feature_type_slug=${feature_type}`); } - axios + return axios .get(url) .then((response) => { if (response) { commit('SET_IMPORT_FEATURE_TYPES_DATA', response.data); } + return response; }) .catch((error) => { throw (error); diff --git a/src/store/modules/index.js b/src/store/modules/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d6e07fc7dbab29baf8d622222c06f2b98896fe1c --- /dev/null +++ b/src/store/modules/index.js @@ -0,0 +1,15 @@ +/** + * Automatically imports all the modules and exports as a single module object +**/ +const requireModule = require.context('.', false, /\.store\.js$/); +const modules = {}; + +requireModule.keys().forEach(filename => { + // create the module name from fileName + // remove the store.js extension + const moduleName = filename.replace(/(\.\/|\.store\.js)/g, ''); + + modules[moduleName] = requireModule(filename).default || requireModule(filename); +}); + +export default modules; \ No newline at end of file diff --git a/src/store/modules/map.js b/src/store/modules/map.store.js similarity index 100% rename from src/store/modules/map.js rename to src/store/modules/map.store.js diff --git a/src/store/modules/projects.store.js b/src/store/modules/projects.store.js new file mode 100644 index 0000000000000000000000000000000000000000..fbe495300f69b356fa4a5718a77785a4a5954dd4 --- /dev/null +++ b/src/store/modules/projects.store.js @@ -0,0 +1,145 @@ +import axios from '@/axios-client.js'; +import projectAPI from '@/services/project-api'; + +const projects = { + + namespaced: true, + + state: { + currentPage: 1, + projects: [], + count: 0, + project_slug: null, + filters: { + moderation: null, + access_level: null, + user_access_level: null, + accessible: null + }, + searchProjectsFilter: null, + isProjectsListSearched: null, + }, + + getters: { + project: state => state.projects.find((project) => project.slug === state.project_slug), + 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; + }, + + SET_PROJECTS(state, projects) { + if (projects.results) { + state.projects = projects.results; + state.count = projects.count; + } else { + state.projects = projects; + state.count = projects.length; + } + }, + + ADD_PROJECT(state, project) { + state.projects = [project, ...state.projects]; + }, + + SET_PROJECT_SLUG(state, slug) { + state.project_slug = slug; + }, + + SET_PROJECTS_FILTER(state, payload) { + state.filters[payload.filter] = payload.value; + }, + + SET_PROJECTS_SEARCH_STATE(state, payload) { + state.isProjectsListSearched = payload.isSearched; + state.searchProjectsFilter = payload.text; + }, + }, + + actions: { + async GET_ALL_PROJECTS({ rootState, commit }) { + try { + const response = await axios + .get(`${rootState.configuration.VUE_APP_DJANGO_API_BASE}projects/`); + if (response.status === 200 && response.data) { + // const orderedProjects = response.data.sort((a, b) => parseDate(b.created_on) - parseDate(a.created_on)); + commit('SET_PROJECTS', response.data); + } + } catch (error) { + console.error(error); + throw error; + } + }, + + async GET_PROJECTS({ state, rootState, commit }, page) { + if (!page) { + page = state.currentPage; + } + const baseUrl = rootState.configuration.VUE_APP_DJANGO_API_BASE; + const projects = await projectAPI.getProjects(baseUrl, state.filters, page); + commit('SET_PROJECTS', projects); + }, + + async SEARCH_PROJECTS({ commit, dispatch }, text) { + if (text) { + await dispatch('HANDLE_PROJECTS_SEARCH_REQUEST', text); + } else { + commit('SET_PROJECTS_SEARCH_STATE', { + isSearched: false, + text: null + }); + await dispatch('GET_PROJECTS'); + } + }, + + async HANDLE_PROJECTS_SEARCH_REQUEST({ state, rootState, commit }, text) { + + if (rootState.cancellableSearchRequest.length > 0) { + const currentRequestCancelToken = + rootState.cancellableSearchRequest[rootState.cancellableSearchRequest.length - 1]; + currentRequestCancelToken.cancel(); + } + + const cancelToken = axios.CancelToken.source(); + commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true }); + + const url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}projects/?search=${text}`; + let filteredUrl; + if (Object.values(state.filters).some(el => el && el.length > 0)) { + filteredUrl = url; + for (const filter in state.filters) { + if (state.filters[filter]) { + filteredUrl = filteredUrl.concat('', `&${filter}=${state.filters[filter]}`); + } + } + } + + try { + const response = await axios.get( + filteredUrl ? filteredUrl : url, + { + cancelToken: cancelToken.token, + } + ); + if (response.status === 200) { + const projects = response.data; + if (projects) { + commit('SET_PROJECTS', projects); + commit('SET_PROJECTS_SEARCH_STATE', { + isSearched: true, + text: text + }); + } + } + } catch(err) { + console.error(err); + } + }, + } + +}; + +export default projects; diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f4e38a208239a1dea567de3f9059854fcbd7048e --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,4 @@ +export function parseDate(date) { + let dateArr = date.split("/").reverse(); + return new Date(dateArr[0], dateArr[1] - 1, dateArr[2]); +} \ No newline at end of file diff --git a/src/views/Catalog.vue b/src/views/Catalog.vue new file mode 100644 index 0000000000000000000000000000000000000000..aecdb5ec40cc16833c807c4d2ee465baa739371a --- /dev/null +++ b/src/views/Catalog.vue @@ -0,0 +1,343 @@ +<template> + <div class="to-left"> + <h1 v-if="project"> + Créer un nouveau type de signalement pour le projet « + {{ project.title }} » depuis le catalogue Datasud + </h1> + Liste des ressources géographiques publiées par vos organisations : + <div class="table"> + <div class="row header"> + <div>Organisation</div> + <div>Dataset</div> + <div>Ressource</div> + </div> + <div v-if="resources && resources.length > 0" > + <div + v-for="(resource, index) in paginatedResources" + :key="`${resource.resource_name}-${index}`" + @click="selectResource(resource)" + :class="[ + 'row', + { + selected: + selectedResource && resource.layer === selectedResource.layer, + }, + ]" + > + <div>{{ resource.organization_name }}</div> + <div>{{ resource.dataset_name }}</div> + <div>{{ resource.resource_name }}</div> + </div> + </div> + <div class="no-response" v-else>Pas de données trouvées pour l'utilisateur {{this.user.username}}</div> + </div> + + <div class="pagination_wrapper"> + <div + v-if="nbPages.length > 1" + id="table-features_info" + class="dataTables_info" + > + Affichage de l'élément {{ pagination.start + 1 }} à + {{ displayedPageEnd }} + sur {{ resources.length }} éléments + </div> + <div + v-if="nbPages.length > 1" + id="table-features_paginate" + class="dataTables_paginate paging_simple_numbers" + > + <a + @click="toPreviousPage" + id="table-features_previous" + :class="[ + 'paginate_button previous', + { disabled: pagination.currentPage === 1 }, + ]" + >Précédent</a + > + <span> + <a + v-for="pageNumber in nbPages" + :key="'page' + pageNumber" + @click="toPage(pageNumber)" + :class="[ + 'paginate_button', + { current: pageNumber === pagination.currentPage }, + ]" + >{{ pageNumber }}</a + > + </span> + <!-- // TODO : <span v-if="nbPages > 4" class="ellipsis">...</span> --> + <a + id="table-features_next" + :class="[ + 'paginate_button next', + { disabled: pagination.currentPage === nbPages.length }, + ]" + @click="toNextPage" + >Suivant</a + > + </div> + </div> + + <div class="import"> + <button + :disabled="!selectedResource" + @click="launchImport" + class="ui fluid teal icon button" + > + <i class="upload icon"></i> Lancer l'import avec le fichier + <span v-if="selectedResource"> + {{ selectedResource.resource }} + </span> + </button> + </div> + </div> +</template> + +<script> +import { mapGetters, mapState } from "vuex"; +import miscAPI from "@/services/misc-api"; + +export default { + name: "Catalog", + + data() { + return { + resources: [], + pagination: { + currentPage: 1, + pagesize: 15, + start: 0, + end: 15, + }, + selectedResource: null, + }; + }, + + computed: { + ...mapState(["user"]), + ...mapGetters(["project", "permissions"]), + ...mapGetters("feature_type", ["feature_type"]), + + paginatedResources() { + return this.resources.slice(this.pagination.start, this.pagination.end); + }, + + nbPages() { + let N = Math.ceil(this.resources.length / this.pagination.pagesize); + const arr = [...Array(N).keys()].map(function (x) { + ++x; + return x; + }); + return arr; + }, + + displayedPageEnd() { + return this.resources.length <= this.pagination.end + ? this.resources.length + : this.pagination.end; + }, + }, + + methods: { + selectResource(resource) { + this.selectedResource = resource; + }, + toPage(pageNumber) { + const toAddOrRemove = + (pageNumber - this.pagination.currentPage) * this.pagination.pagesize; + this.pagination.start += toAddOrRemove; + this.pagination.end += toAddOrRemove; + this.pagination.currentPage = pageNumber; + }, + + toPreviousPage() { + if (this.pagination.start > 0) { + this.pagination.start -= this.pagination.pagesize; + this.pagination.end -= this.pagination.pagesize; + this.pagination.currentPage -= 1; + } + }, + + toNextPage() { + if (this.pagination.end < this.resources.length) { + this.pagination.start += this.pagination.pagesize; + this.pagination.end += this.pagination.pagesize; + this.pagination.currentPage += 1; + } + }, + + redirect(geojson) { + const name = + this.$route.params.feature_type_slug === "create" + ? "ajouter-type-signalement" + : "details-type-signalement"; + this.$router.push({ + name: name, + params: { + geojson, + type: "external-geojson", + }, + }); + }, + + launchImport() { + const queryParams = `typename=${this.selectedResource.layer}`; + miscAPI.getExternalGeojson(queryParams).then((data) => { + if (data) this.redirect(data); + }); + }, + }, + + mounted() { + this.$store.commit("DISPLAY_LOADER", "Interrogation du catologue datasud."); + this.$store.dispatch("GET_PROJECT_INFO", this.$route.params.slug); + miscAPI.getIdgoCatalog(this.user.username).then((data) => { + if (data && data.layers) this.resources = data.layers; + this.$store.commit("DISCARD_LOADER"); + }); + }, +}; +</script> + +<style scoped> +.to-left { + text-align: left; +} + +h1 { + margin: 0.5em 0; +} + +.table { + width: 100%; + border: 1px solid #c0c0c0; + margin: 2rem 0; +} +.table .row { + display: flex; + transition: all ease-out 0.2s; +} +.table .row:not(.header).selected { + background-color: #8bddd9; +} +.table .row:not(.header):hover { + background-color: #009c95; + color: #ffffff; + cursor: pointer; +} +.table .row:not(:last-child) { + border-bottom: 1px solid #cacaca; +} +.table .row > div { + width: 100%; + padding: 0.5rem; +} +.table .header { + background-color: #e0e0e0; +} + +.no-response { + padding: 1rem; + text-align: center; + color: #585858; +} + +.import { + display: flex; + align-items: center; + margin-top: 1em; +} + +/* datatables */ +.pagination_wrapper { + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} +.pagination_wrapper > div { + margin: 0.25em 0; +} +@media only screen and (max-width: 767px) { + .pagination_wrapper { + justify-content: center; + } +} + +.dataTables_length, +.dataTables_filter, +.dataTables_info, +.dataTables_processing, +.dataTables_paginate { + color: #333; +} +/* .dataTables_info { + clear: both; + float: left; + padding-top: 0.755em; +} */ +/* .dataTables_paginate { + float: right; + text-align: right; + padding-top: 0.25em; +} */ +.dataTables_paginate .paginate_button.current, +.dataTables_paginate .paginate_button.current:hover { + color: #333 !important; + border: 1px solid #979797; + background-color: white; + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0%, #fff), + color-stop(100%, #dcdcdc) + ); + background: -webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%); + background: -moz-linear-gradient(top, #fff 0%, #dcdcdc 100%); + background: -ms-linear-gradient(top, #fff 0%, #dcdcdc 100%); + background: -o-linear-gradient(top, #fff 0%, #dcdcdc 100%); + background: linear-gradient(to bottom, #fff 0%, #dcdcdc 100%); +} +.dataTables_paginate .paginate_button { + box-sizing: border-box; + display: inline-block; + min-width: 1.5em; + padding: 0.5em 1em; + margin-left: 2px; + text-align: center; + text-decoration: none !important; + cursor: pointer; + color: #333 !important; + border: 1px solid transparent; + border-radius: 2px; +} +.dataTables_paginate .paginate_button:hover { + color: white !important; + border: 1px solid #111; + background-color: #585858; + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0%, #585858), + color-stop(100%, #111) + ); + background: -webkit-linear-gradient(top, #585858 0%, #111 100%); + background: -moz-linear-gradient(top, #585858 0%, #111 100%); + background: -ms-linear-gradient(top, #585858 0%, #111 100%); + background: -o-linear-gradient(top, #585858 0%, #111 100%); + background: linear-gradient(to bottom, #585858 0%, #111 100%); +} +.dataTables_paginate .paginate_button.disabled, +.dataTables_paginate .paginate_button.disabled:hover, +.dataTables_paginate .paginate_button.disabled:active { + cursor: default; + color: #666 !important; + border: 1px solid transparent; + background: transparent; + box-shadow: none; +} +</style> \ No newline at end of file diff --git a/src/views/My_account.vue b/src/views/My_account.vue index 9304195e19340036a7918d79b96a20292aca783d..a21a20dd1ce306d29fa2c7d4684234d508cdbe71 100644 --- a/src/views/My_account.vue +++ b/src/views/My_account.vue @@ -317,10 +317,19 @@ export default { computed: { // todo : filter projects to user ...mapState([ +<<<<<<< HEAD 'user', 'projects', 'USER_LEVEL_PROJECTS', 'user_permissions', +======= + "user", + "USER_LEVEL_PROJECTS", + "user_permissions", +>>>>>>> develop + ]), + ...mapState('projects', [ + 'projects' ]), DJANGO_BASE_URL: function () { return this.$store.state.configuration.VUE_APP_DJANGO_BASE; diff --git a/src/views/Index.vue b/src/views/Projects.vue similarity index 55% rename from src/views/Index.vue rename to src/views/Projects.vue index 716ff08b14e30728729dc41e9f36cc671ac8cba9..e00cfc26b3e03a3eda7112655e9fc797e1a56165 100644 --- a/src/views/Index.vue +++ b/src/views/Projects.vue @@ -1,5 +1,6 @@ <template> <div class="fourteen wide column"> +<<<<<<< HEAD:src/views/Index.vue <img class="ui centered small image" :src="logo" @@ -20,6 +21,13 @@ > PROJETS </h4> +======= + + <h2 class="ui horizontal divider header"> + PROJETS + </h2> + +>>>>>>> develop:src/views/Projects.vue <div class="flex"> <router-link v-if="user && user.can_create_project && isOffline() != true" @@ -39,6 +47,7 @@ </router-link> </div> +<<<<<<< HEAD:src/views/Index.vue <div v-if="projects" class="ui divided items" @@ -48,6 +57,33 @@ :key="project.slug" class="item" > +======= + <!-- FILTRES DES PROJETS --> + <projects-menu + @filter="setProjectsFilters" + /> + + <div + v-if="configuration.DISPLAY_FORBIDDEN_PROJECTS" + id="forbidden-projects" + class="ui toggle checkbox" + > + <input + v-model="displayForbiddenProjects" + type="checkbox" + /> + <label> + N'afficher que les projets disponibles à la consultation + </label> + </div> + + <!-- LISTE DES PROJETS --> + <div v-if="projects" class="ui divided items dimmable dimmed"> + <div :class="{ active: loading }" class="ui inverted dimmer"> + <div class="ui loader" /> + </div> + <div v-for="project in projects" class="item" :key="project.slug"> +>>>>>>> develop:src/views/Projects.vue <div class="ui tiny image"> <img :src=" @@ -115,10 +151,20 @@ <div class="item" /> </div> + + <!-- PAGINATION --> + <pagination + v-if="count" + :nbPages="Math.ceil(count/10)" + :on-page-change="SET_CURRENT_PAGE" + @change-page="changePage" + /> + </div> </template> <script> +<<<<<<< HEAD:src/views/Index.vue import { mapState } from 'vuex'; export default { @@ -126,6 +172,39 @@ export default { computed: { ...mapState(['projects', 'user', 'USER_LEVEL_PROJECTS']), +======= +import { mapState, mapMutations, mapActions } from 'vuex'; + +import ProjectsMenu from '@/components/Projects/ProjectsMenu.vue'; +import Pagination from '@/components/Pagination.vue'; + +export default { + name: 'Projects', + + components: { + ProjectsMenu, + Pagination + }, + + data() { + return { + loading: false, + displayForbiddenProjects: false + } + }, + + computed: { + ...mapState([ + 'configuration', + 'user', + 'USER_LEVEL_PROJECTS' + ]), + ...mapState('projects', [ + 'projects', + 'count', + 'filters' + ]), +>>>>>>> develop:src/views/Projects.vue APPLICATION_NAME() { return this.$store.state.configuration.VUE_APP_APPLICATION_NAME; }, @@ -140,13 +219,58 @@ export default { }, }, +<<<<<<< HEAD:src/views/Index.vue created() { if (this.$store.getters.project) { this.$store.commit('SET_PROJECT_SLUG', null); +======= + watch: { + filters: { + deep: true, + handler(newValue) { + if (newValue) { + this.getData(); + } + } + }, + displayForbiddenProjects(newValue) { + if (newValue) { + this.SET_PROJECTS_FILTER({ + filter: 'accessible', + value: 'true' + }); + } else { + this.SET_PROJECTS_FILTER({ + filter: 'accessible', + value: null + }); + } + this.getData(); + } + }, + + created() { + this.SET_PROJECTS_FILTER({ + filter: 'accessible', + value: 'true' + }); + this.displayForbiddenProjects = this.configuration.DISPLAY_FORBIDDEN_PROJECTS_DEFAULT; + + if (this.$store.getters.project) { + this.$store.commit("SET_PROJECT_SLUG", null); +>>>>>>> develop:src/views/Projects.vue } }, methods: { + ...mapMutations('projects', [ + 'SET_CURRENT_PAGE', + 'SET_PROJECTS_FILTER' + ]), + ...mapActions('projects', [ + 'GET_PROJECTS' + ]), + isOffline() { return navigator.onLine == false; }, @@ -154,13 +278,57 @@ export default { //* change path of thumbnail to update image return '?ver=' + Math.random(); }, +<<<<<<< HEAD:src/views/Index.vue }, +======= + + getData(page) { + this.loading = true; + this.GET_PROJECTS(page) + .then(() => { + this.loading = false; + }) + .catch(() => { + this.loading = false; + }); + }, + + changePage(e) { + this.getData(e); + }, + + setProjectsFilters(e) { + this.SET_PROJECTS_FILTER(e); + }, + } +>>>>>>> develop:src/views/Projects.vue }; </script> -<style scoped> +<style lang="less" scoped> + .flex { display: flex; justify-content: space-between; } + +#filters-divider { + padding-top: 0; + color: gray !important; +} + +#forbidden-projects.checkbox { + font-size: 1.2em; + font-weight: 600; + label { + color: rgb(94, 94, 94); + } + input:checked ~ label::before { + background-color: teal !important; + } + input:checked ~ label { + color: teal !important; + } +} + </style> \ No newline at end of file diff --git a/src/views/feature/Feature_detail.vue b/src/views/feature/Feature_detail.vue index 0e40ed7c266f25cbe7ab91152fb615861b81e112..da281a0398229b8d370ce6413a41c3ae19203933 100644 --- a/src/views/feature/Feature_detail.vue +++ b/src/views/feature/Feature_detail.vue @@ -590,7 +590,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 b50a313c0cd7358f2e303453f4d9849090180fab..cb797cd7cd090c8979ca7f6ddf1d842b8b89083b 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="toggleAddFeature" class="ui dropdown button compact button-hover-green" data-tooltip="Ajouter un signalement" - data-position="bottom left" - @click="showAddFeature = !showAddFeature" + data-position="bottom right" > <i class="plus fitted icon" /> <div v-if="showAddFeature" - class="menu transition visible" + class="menu left transition visible" style="z-index: 9999" > <div class="header"> @@ -70,12 +71,38 @@ </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" - @click="modalAllDelete" + data-position="bottom right" > <i class="grey trash fitted icon" /> </div> @@ -160,11 +187,15 @@ <SidebarLayers v-if="basemaps && map" /> </div> <!-- | --> + <!-- v-on:update:clickedFeatures="handleClickedFeatures" --> <FeatureListTable v-show="!showMap" :paginated-features="paginatedFeatures" :checked-features.sync="checkedFeatures" :features-count="featuresCount" + :clickedFeatures.sync="clickedFeatures" + :mode.sync="mode" + :featuresCount="featuresCount" :pagination="pagination" :sort="sort" @update:page="handlePageChange" @@ -255,7 +286,9 @@ export default { title: null, }, baseUrl: this.$store.state.configuration.BASE_URL, + clickedFeatures: [], modalAllDeleteOpen: false, + mode: "modify", map: null, zoom: null, lat: null, @@ -277,39 +310,10 @@ export default { }, showMap: true, showAddFeature: false, + showModifyStatus: false, }; }, - computed: { - ...mapGetters([ - 'project', 'permissions' - ]), - ...mapState('feature', [ - 'checkedFeatures' - ]), - ...mapState('feature_type', [ - 'feature_types' - ]), - ...mapState('map', [ - 'basemaps' - ]), - - API_BASE_URL() { - return this.$store.state.configuration.VUE_APP_DJANGO_API_BASE; - }, - - statusChoices() { - //* if project is not moderate, remove pending status - return this.form.status.choices.filter((el) => - this.project && this.project.moderation ? true : el.value !== 'pending' - ); - }, - - featureTypeChoices() { - return this.feature_types.map((el) => el.title); - }, - }, - watch: { 'form.type.selected'() { this.fetchPagedFeatures(); @@ -358,30 +362,174 @@ export default { if (!this.project) { // Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh this.$store - .dispatch('GET_PROJECT_INFO', this.$route.params.slug) + .dispatch("GET_PROJECT_INFO", this.$route.params.slug) .then(() => this.initMap()); } else { 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'); + this.$store.commit("DISCARD_LOADER"); + }, + + computed: { + ...mapState(["user", "USER_LEVEL_PROJECTS"]), + ...mapGetters([ + 'project', 'permissions' + ]), + ...mapState('feature', [ + 'checkedFeatures' + ]), + ...mapState('feature_type', [ + 'feature_types' + ]), + ...mapState('map', [ + 'basemaps' + ]), + + API_BASE_URL() { + return this.$store.state.configuration.VUE_APP_DJANGO_API_BASE; + }, + + statusChoices() { + //* if project is not moderate, remove pending status + return this.form.status.choices.filter((el) => + this.project && this.project.moderation ? true : el.value !== "pending" + ); + }, + + availableStatus() { + 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); + }, }, 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) { @@ -390,7 +538,6 @@ export default { }) .then(() => { this.fetchPagedFeatures(); - this.getNloadGeojsonFeatures(); this.checkedFeatures.splice(feature_id); }); } @@ -403,8 +550,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(); }, @@ -505,8 +652,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 (newUrl && typeof newUrl === 'string') { - //* if receiving next & previous url + //* if receiving next & previous url + if (newUrl && typeof newUrl === "string") { //newUrl = newUrl.replace("8000", "8010"); //* for dev uncomment to use proxy link url = newUrl; } @@ -597,10 +744,6 @@ export default { z-index: 1; } -.center { - text-align: center !important; -} - #feature-list-container { justify-content: flex-start; } @@ -623,20 +766,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; @@ -644,10 +790,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_detail.vue b/src/views/feature_type/Feature_type_detail.vue index a7852840c42478c642d1ff625117e0ce0487f969..ba82219c44f86d6d551e3b2ef06ffe0d23e0e074 100644 --- a/src/views/feature_type/Feature_type_detail.vue +++ b/src/views/feature_type/Feature_type_detail.vue @@ -5,7 +5,7 @@ > <div class="five wide column"> <div class="ui attached secondary segment"> - <h1 class="ui center aligned header"> + <h1 class="ui center aligned header ellipsis"> <img v-if="structure.geom_type === 'point'" class="ui medium image" @@ -84,11 +84,16 @@ :class="loadingImportFile ? 'loading' : ''" > <div class="field"> +<<<<<<< HEAD <label class="ui icon button" for="json_file" > <i class="file icon" /> +======= + <label class="ui icon button ellipsis" for="json_file"> + <i class="file icon"></i> +>>>>>>> develop <span class="label">{{ fileToImport.name }}</span> </label> <input @@ -100,16 +105,45 @@ @change="onFileChange" > </div> +<<<<<<< HEAD <ul v-if="importError" class="errorlist" > +======= + + <router-link + v-if=" + IDGO && + permissions && + permissions.can_create_feature + " + :to="{ + name: 'catalog-import', + params: { + slug: project.slug, + feature_type_slug: $route.params.feature_type_slug + }, + }" + class="ui icon button import-catalog" + >Importer les signalements à partir de {{ CATALOG_NAME|| 'IDGO'}} + </router-link> + <div v-if="$route.params.geojson" class="ui button import-catalog basic active teal no-hover"> + Ressource {{$route.params.geojson.name}} + </div> + <ul v-if="importError" class="errorlist"> +>>>>>>> develop <li> {{ importError }} </li> </ul> <button +<<<<<<< HEAD :disabled="fileToImport.size === 0" +======= + :disabled="fileToImport.size === 0 && !$route.params.geojson" + @click="importGeoJson" +>>>>>>> develop class="ui fluid teal icon button" @click="importGeoJson" > @@ -118,6 +152,7 @@ <ImportTask v-if="importFeatureTypeData && importFeatureTypeData.length" :data="importFeatureTypeData" + :reloading="reloadingImport" /> </div> </div> @@ -171,6 +206,15 @@ l'import, cliquez sur "Importer des Signalements". </p> </div> + <div + v-else-if="waitMessage" + class="ui message info" + > + <p> + L'import des signalements a été lancé. + Vous pourrez suivre le statut de l'import dans quelques instants... + </p> + </div> <div v-for="(feature, index) in lastFeatures" :key="feature.feature_id + index" @@ -264,6 +308,7 @@ export default { ImportTask: ImportTask, }, +<<<<<<< HEAD filters: { formatDate(value) { let date = new Date(value); @@ -271,6 +316,9 @@ export default { return date.substr(0, date.length - 3); //* quick & dirty way to remove seconds from date }, }, +======= + props: ["geojson", "typename", ], +>>>>>>> develop data() { return { @@ -282,13 +330,45 @@ export default { showImport: false, featuresLoading: true, loadingImportFile: false, +<<<<<<< HEAD +======= + waitMessage: false, + reloadingImport: false, +>>>>>>> develop }; }, computed: { +<<<<<<< HEAD ...mapGetters(['project', 'permissions']), ...mapState('feature', ['features', 'features_count']), ...mapState('feature_type', ['feature_types', 'importFeatureTypeData']), +======= + ...mapGetters([ + 'project', + 'permissions' + ]), + ...mapState([ + 'reloadIntervalId' + ]), + ...mapState('feature', [ + 'features', + 'features_count' + ]), + ...mapState([ + 'configuration', + ]), + ...mapState('feature_type', [ + 'feature_types', + 'importFeatureTypeData' + ]), + CATALOG_NAME() { + return this.configuration.VUE_APP_CATALOG_NAME; + }, + IDGO() { + return this.$store.state.configuration.VUE_APP_IDGO; + }, +>>>>>>> develop structure: function () { if (Object.keys(this.feature_types).length) { let st = this.feature_types.find( @@ -326,21 +406,59 @@ export default { structure(newValue) { if (newValue.slug) { this.GET_IMPORTS({ +<<<<<<< HEAD feature_type: this.$route.params.feature_type_slug, }); } }, +======= + feature_type: this.$route.params.feature_type_slug + }) + } + }, + + importFeatureTypeData: { + deep: true, + handler(newValue, oldValue) { + if (newValue && newValue.some(el => el.status === 'pending')) { + setTimeout(() => { + this.reloadingImport = true; + this.GET_IMPORTS({ + feature_type: this.$route.params.feature_type_slug + }).then(()=> { + setTimeout(() => { + this.reloadingImport = false; + }, 1000); + }) + }, this.$store.state.configuration.VUE_APP_RELOAD_INTERVAL); + } else if (oldValue && oldValue.some(el => el.status === 'pending')) { + this.getFeatures(); + } + } + }, +>>>>>>> develop }, created() { if (!this.project) { this.GET_PROJECT_INFO(this.$route.params.slug); } +<<<<<<< HEAD this.$store.commit('feature/SET_FEATURES', []); //* empty features remaining in case they were in geojson format and will be fetch anyway this.getFeatures(); // .then(res => resolve(res)) // .catch(err => reject(err)); this.SET_CURRENT_FEATURE_TYPE_SLUG(this.$route.params.feature_type_slug); +======= + this.$store.commit("feature/SET_FEATURES", []); //* empty remaining features in case they were in geojson format and will be fetch anyway + this.getFeatures(); + this.SET_CURRENT_FEATURE_TYPE_SLUG( + this.$route.params.feature_type_slug + ); + if (this.$route.params.type === "external-geojson") { + this.showImport = true; + } +>>>>>>> develop }, methods: { @@ -351,9 +469,14 @@ export default { toggleShowImport() { this.showImport = !this.showImport; if (this.showImport) { +<<<<<<< HEAD this.$store.dispatch('feature_type/GET_IMPORTS', { feature_type: this.$route.params.feature_type_slug, }); +======= + this.GET_IMPORTS({ + feature_type: this.$route.params.feature_type_slug }); +>>>>>>> develop } }, @@ -442,11 +565,23 @@ export default { }, importGeoJson() { - this.$store.dispatch('feature_type/SEND_FEATURES_FROM_GEOJSON', { - slug: this.$route.params.slug, - feature_type_slug: this.$route.params.feature_type_slug, - fileToImport: this.fileToImport, - }); + this.waitMessage = true; + let payload = { + slug: this.$route.params.slug, + feature_type_slug: this.$route.params.feature_type_slug, + } + if (this.$route.params.geojson) { + payload["geojson"] = this.$route.params.geojson + } else if (this.fileToImport && !this.fileToImport.name) { + payload["fileToImport"] = this.fileToImport; + } else { + this.importError = "La ressource n'a pas pu être récupéré." + return + } + this.$store.dispatch('feature_type/SEND_FEATURES_FROM_GEOJSON', payload) + .then(() => { + this.waitMessage = false; + }); }, exportFeatures() { @@ -481,4 +616,12 @@ export default { .margin-25 { margin: 0 0.25em 0.25em 0 !important; } + +.import-catalog { + margin-bottom: 1em; +} + +.no-hover { + cursor: default; +} </style> \ No newline at end of file diff --git a/src/views/feature_type/Feature_type_edit.vue b/src/views/feature_type/Feature_type_edit.vue index 77f11119c735109e333026ca941d8aed4354eb01..6fc41d9a8c67f2c95a4a4c5b14f63ddad920a807 100644 --- a/src/views/feature_type/Feature_type_edit.vue +++ b/src/views/feature_type/Feature_type_edit.vue @@ -93,11 +93,22 @@ <div class="field"> <div class="ui checkbox"> <input +<<<<<<< HEAD +======= + class="hidden" + :id="form.title_optional.html_name" + :name="form.title_optional.html_name" +>>>>>>> develop v-model="form.title_optional.value" :name="form.title_optional.html_name" type="checkbox" +<<<<<<< HEAD > <label>{{ form.title_optional.label }}</label> +======= + /> + <label :for="form.title_optional.html_name">{{ form.title_optional.label }}</label> +>>>>>>> develop </div> </div> @@ -118,6 +129,23 @@ :placeholder="'Sélectionner la liste de valeurs'" /> </div> + <div class="colors_selection" id="id_colors_selection" hidden> + <div + v-for="(value, key, index) in form.colors_style.value.colors" + :key="'colors_style-' + index" + > + <div v-if="key" class="color-input"> + <label>{{ key }}</label + ><input + :name="key" + type="color" + :value="value" + @input="setColorStyles" + /> + </div> + </div> + </div> + </div> <span v-if="action === 'duplicate' || action === 'edit'" /> @@ -185,6 +213,7 @@ export default { FeatureTypeCustomForm, }, +<<<<<<< HEAD props: { geojson: { type: Object, @@ -195,6 +224,9 @@ export default { default: null, } }, +======= + props: ["geojson", "typename", ], +>>>>>>> develop data() { return { @@ -270,9 +302,15 @@ export default { }, computed: { +<<<<<<< HEAD ...mapGetters(['project']), ...mapState('feature_type', ['customForms', 'colorsStyleList']), ...mapGetters('feature_type', ['feature_type']), +======= + ...mapGetters(["project"]), + ...mapState("feature_type", ["customForms", "colorsStyleList", "fileToImport"]), + ...mapGetters("feature_type", ["feature_type"]), +>>>>>>> develop selectedGeomType: { get() { const currentGeomType = this.geomTypeChoices.find( @@ -529,9 +567,10 @@ export default { .dispatch('feature_type/SEND_FEATURES_FROM_GEOJSON', { slug: this.$route.params.slug, feature_type_slug, + geojson: this.geojson }) .then((response) => { - if (response.status === 200) { + if (response && response.status === 200) { this.goBackToProject(); } else { this.displayMessage( @@ -547,9 +586,14 @@ export default { }, async postFeatureTypeThenFeatures() { +<<<<<<< HEAD this.loading = true; const requestType = this.action === 'edit' ? 'put' : 'post'; +======= + const requestType = this.action === "edit" ? "put" : "post"; +>>>>>>> develop if (this.checkForms()) { + this.loading = true; await this.$store .dispatch('feature_type/SEND_FEATURE_TYPE', requestType) .then(({ feature_type_slug }) => { @@ -634,6 +678,56 @@ export default { } }, }, +<<<<<<< HEAD +======= + + created() { + if (!this.project) { + this.$store.dispatch("GET_PROJECT_INFO", this.$route.params.slug); + } + this.$store.commit( + "feature_type/SET_CURRENT_FEATURE_TYPE_SLUG", + this.$route.params.slug_type_signal + ); + + this.definePageType(); + }, + + mounted() { + if (this.action === "edit" || this.action === "duplicate") { + if (this.feature_type) { + //* add datas from store to state to avoid mutating directly store with v-model (not good practice), could have used computed with getter and setter as well + this.fillFormData(this.feature_type); + } + if (this.action === "duplicate") { + //* replace original name with new default title + this.form.title.value += ` (Copie-${new Date() + .toLocaleString() + .slice(0, -3) + .replace(",", "")})`; + this.updateStore(); // * initialize form in store in case this.form would not be modified + } + } + //* when creation from a geojson + if (this.geojson) { + this.importGeoJsonFeatureType(); + if (this.fileToImport && this.fileToImport.name) { + this.form.title.value = // * use the filename as title by default + this.fileToImport.name.split(".")[0]; + } else { //* case when the geojson comes from datasud catalog + this.form.title.value = this.geojson.name;// * use the typename as title by default + } + } + }, + beforeDestroy() { + this.$store.commit("feature_type/EMPTY_FORM"); + this.$store.commit("feature_type/EMPTY_CUSTOM_FORMS"); + this.$store.commit( + "feature_type/SET_FILE_TO_IMPORT", + null + ); + }, +>>>>>>> develop }; </script> diff --git a/src/views/feature_type/Feature_type_symbology.vue b/src/views/feature_type/Feature_type_symbology.vue index bd667ae4835896181ac9a6e9959574e383efd6a3..c40d90ed067cdccd6da6034977d30c085293bebd 100644 --- a/src/views/feature_type/Feature_type_symbology.vue +++ b/src/views/feature_type/Feature_type_symbology.vue @@ -204,13 +204,17 @@ export default { } this.SET_CURRENT_FEATURE_TYPE_SLUG(this.$route.params.slug_type_signal); if (this.feature_type) { - // Init form - this.form.color = JSON.parse(JSON.stringify(this.feature_type.color)); - this.form.icon = JSON.parse(JSON.stringify(this.feature_type.icon)); - this.form.colors_style = { - ...this.form.colors_style, - ...JSON.parse(JSON.stringify(this.feature_type.colors_style)) - }; + this.initForm(); + } else { + this.loading = true; + this.GET_PROJECT_FEATURE_TYPES(this.$route.params.slug) + .then(() => { + this.initForm(); + this.loading = false; + }) + .catch(() => { + this.loading = false; + }); } }, @@ -226,6 +230,19 @@ export default { 'GET_PROJECT_INFO' ]), + initForm() { + this.form.color = JSON.parse(JSON.stringify(this.feature_type.color)); + this.form.icon = JSON.parse(JSON.stringify(this.feature_type.icon)); + this.form.colors_style = { + ...this.form.colors_style, + ...JSON.parse(JSON.stringify(this.feature_type.colors_style)) + }; + if (this.feature_type.colors_style && Object.keys(this.feature_type.colors_style.colors).length > 0) { + this.selectedCustomfield = + this.feature_type.customfield_set.find(el => el.name === this.feature_type.colors_style.custom_field_name).name; + } + }, + setDefaultStyle(e) { const value = e.value; this.form.color = value.color.value; @@ -248,7 +265,7 @@ export default { .then(() => { this.loading = false; this.success = - 'La modification de la symbologie a été prise en compte. Vous allez être redirigé vers la page d\'acceuil du projet.'; + 'La modification de la symbologie a été prise en compte. Vous allez être redirigé vers la page d\'accueil du projet.'; setTimeout(() => { this.$router.push({ name: 'project_detail', diff --git a/src/views/project/Project_detail.vue b/src/views/project/Project_detail.vue index 9d7adc2c9b831d5a09d26be337cae6bf0467dc4c..e6774bad5721f374a5cacc9e5a5ba78692603ad3 100644 --- a/src/views/project/Project_detail.vue +++ b/src/views/project/Project_detail.vue @@ -1,5 +1,6 @@ <template> <div v-frag> +<<<<<<< HEAD <div v-if="permissions && permissions.can_view_project && project" v-frag @@ -16,6 +17,12 @@ <!-- <div class="header">You are eligible for a reward</div> --> <p><i class="check icon" /> {{ tempMessage }}</p> +======= + <div v-frag v-if="permissions && permissions.can_view_project && project"> + <div id="message" class="fullwidth"> + <div v-if="tempMessage" class="ui positive message"> + <p><i class="check icon"></i> {{ tempMessage }}</p> +>>>>>>> develop </div> </div> <div @@ -70,6 +77,7 @@ }} </div> </div> +<<<<<<< HEAD <div class="ten wide column"> <h1 class="ui header"> <div class="content"> @@ -122,9 +130,101 @@ <div class="ui hidden divider" /> <div class="sub header"> {{ project.description }} +======= + <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 + v-if="user && user.is_administrator && !isSharedProject && project.generate_share_link" + class="ui teal left labeled icon button" + @click="copyLink" + > + <i class="left icon share square"></i> + Copier le lien de partage + </button> + </div> + <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> +>>>>>>> develop </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> @@ -180,14 +280,7 @@ > {{ type.title }} </router-link> - <!-- {% if project and feature_types and - permissions|lookup:'can_create_feature' %} --> - <!-- // ? should we get type.is_editable ? --> - <!-- v-if=" - project && - permissions.can_create_feature && - type.is_editable - " --> + <div class="middle aligned content"> <router-link v-if=" @@ -207,7 +300,7 @@ button button-hover-green " data-tooltip="Ajouter un signalement" - data-position="left center" + data-position="top right" data-variation="mini" > <i class="ui plus icon" /> @@ -233,7 +326,7 @@ button button-hover-green " data-tooltip="Dupliquer un type de signalement" - data-position="left center" + data-position="top right" data-variation="mini" > <i class="inverted grey copy alternate icon" /> @@ -245,9 +338,86 @@ <i class="info circle icon" /> Import en cours </div> +<<<<<<< HEAD <div v-else v-frag +======= + <div v-else v-frag> + <a + v-if=" + user_permissions && + user_permissions[project.slug] && + user_permissions[project.slug].is_project_administrator && + isOffline() !== true + " + @click="toggleDeleteFeatureType(type)" + class=" + ui + compact + small + icon + right + floated + button button-hover-red + " + data-tooltip="Supprimer le type de signalement" + data-position="top center" + data-variation="mini" + > + <i class="inverted grey trash alternate icon"></i> + </a> + <router-link + :to="{ + name: 'editer-symbologie-signalement', + params: { slug_type_signal: type.slug }, + }" + v-if=" + project && + permissions && + permissions.can_create_feature_type && + isOffline() != true + " + class=" + ui + compact + small + icon + right + floated + button button-hover-orange + " + data-tooltip="Éditer la symbologie du type de signalement" + data-position="top center" + data-variation="mini" + > + <i class="inverted grey paint brush alternate icon"></i> + </router-link> + <router-link + :to="{ + name: 'editer-type-signalement', + params: { slug_type_signal: type.slug }, + }" + v-if=" + project && + type.is_editable && + permissions && + permissions.can_create_feature_type && + isOffline() !== true + " + class=" + ui + compact + small + icon + right + floated + button button-hover-orange + " + data-tooltip="Éditer le type de signalement" + data-position="top left" + data-variation="mini" +>>>>>>> develop > <router-link v-if=" @@ -312,7 +482,7 @@ </div> </div> - <div class="nouveau-type-signalement"> + <div id="nouveau-type-signalement"> <router-link v-if=" permissions && @@ -329,7 +499,7 @@ </router-link> </div> <div class="nouveau-type-signalement"> - <a + <div v-if=" permissions && permissions.can_update_project && @@ -340,9 +510,7 @@ compact basic button button-hover-green - important-flex - align-center - text-left + button-align-left " > <i class="ui plus icon" /> @@ -362,6 +530,7 @@ style="display: none" name="json_file" @change="onFileChange" +<<<<<<< HEAD > </a> <br> @@ -377,8 +546,43 @@ <i class="upload icon" /> Lancer l'import avec le fichier {{ fileToImport.name }} </button> +======= + /> +>>>>>>> develop </div> </div> + + <div class="nouveau-type-signalement"> + <router-link + v-if=" + IDGO && + permissions && + permissions.can_update_project && + isOffline() !== true + " + :to="{ + name: 'catalog-import', + params: { + slug: project.slug, + feature_type_slug: 'create' + }, + }" + class="ui compact basic button button-hover-green button-align-left" + > + <i class="ui plus icon"></i>Créer un nouveau type de signalement à partir du catalogue {{ CATALOG_NAME|| 'IDGO'}} + </router-link> + </div> + + <div id="button-import" v-if="fileToImport.size > 0"> + <button + :disabled="fileToImport.size === 0" + @click="toNewFeatureType" + class="ui fluid teal icon button" + > + <i class="upload icon"></i> Lancer l'import avec le fichier + {{ fileToImport.name }} + </button> + </div> </div> <div class="seven wide column"> <div @@ -591,16 +795,17 @@ </span> <div - v-if="isModalOpen" + v-if="modalType" class="ui dimmer modals page transition visible active" style="display: flex !important" > <div :class="[ 'ui mini modal subscription', - { 'transition visible active': isModalOpen }, + { 'transition visible active': modalType }, ]" > +<<<<<<< HEAD <i class="close icon" @click="isModalOpen = false" @@ -608,18 +813,51 @@ <div class="ui icon header"> <i class="envelope icon" /> Notifications du projet +======= + <i @click="modalType = false" class="close icon"></i> + <div class="ui icon header"> + <i :class="[modalType === 'subscribe' ? 'envelope' : 'trash', 'icon']"></i> + {{ + modalType === 'subscribe' ? 'Notifications' : 'Suppression' + }} du {{ + modalType === 'deleteFeatureType' ? 'type de signalement ' + featureTypeToDelete.title : 'projet' + }} +>>>>>>> develop </div> - <div class="content"> + <div v-if="modalType !== 'subscribe'" > + + <p class="centered-text"> + Confirmez vous la suppression du {{ modalType === 'deleteProject' ? 'projet, ainsi que les types de signalements' : 'type de signalement'}} et tous les signalements associés ? + </p> + <p class="centered-text alert"> + Attention cette action est irreversible ! + </p> + </div> <button +<<<<<<< HEAD :class="['ui compact fluid button', is_suscriber ? 'red' : 'green']" @click="subscribeProject" +======= + @click="handleModalClick" + :class="['ui compact fluid button', modalType === 'subscribe' && !is_suscriber ? 'green' : 'red']" +>>>>>>> develop > + <span v-if="modalType === 'subscribe'"> {{ is_suscriber ? "Se désabonner de ce projet" : "S'abonner à ce projet" }} + </span> + <span v-else> + Supprimer le + {{ + modalType === 'deleteProject' + ? 'projet' + : 'type de signalement' + }} + </span> </button> </div> </div> @@ -656,11 +894,20 @@ </template> <script> +<<<<<<< HEAD import frag from 'vue-frag'; import { mapUtil } from '@/assets/js/map-util.js'; import { mapGetters, mapState, mapActions, mapMutations } from 'vuex'; import projectAPI from '@/services/project-api'; import featureAPI from '@/services/feature-api'; +======= +import frag from "vue-frag"; +import { mapUtil } from "@/assets/js/map-util.js"; +import { mapGetters, mapState, mapActions, mapMutations } from "vuex"; +import projectAPI from "@/services/project-api"; +import featureTypeAPI from "@/services/featureType-api"; +import featureAPI from "@/services/feature-api"; +>>>>>>> develop import axios from '@/axios-client.js'; @@ -698,14 +945,16 @@ export default { importMessage: null, arraysOffline: [], arraysOfflineErrors: [], + confirmMsg: false, geojsonImport: [], fileToImport: { name: '', size: 0 }, slug: this.$route.params.slug, - isModalOpen: false, + modalType: false, is_suscriber: false, tempMessage: null, projectInfoLoading: true, featureTypeImporting: false, + featureTypeToDelete: null, featuresLoading: true, isFileSizeModalOpen: false, // mapFeatures: null, @@ -714,20 +963,60 @@ export default { }, computed: { +<<<<<<< HEAD ...mapGetters(['project', 'permissions']), ...mapState('feature_type', ['feature_types', 'importFeatureTypeData']), ...mapState('feature', ['features']), ...mapState(['last_comments', 'user', 'reloadIntervalId']), ...mapState('map', ['map']), +======= + ...mapGetters([ + 'permissions' + ]), + ...mapGetters('projects', [ + 'project' + ]), + ...mapState([ + 'configuration', + ]), + ...mapState('feature_type', [ + 'feature_types', + 'importFeatureTypeData' + ]), + ...mapState('feature', [ + 'features' + ]), + ...mapState([ + 'last_comments', + 'user', + 'user_permissions', + 'reloadIntervalId', + ]), + ...mapState('map', [ + 'map' + ]), +>>>>>>> develop DJANGO_BASE_URL() { - return this.$store.state.configuration.VUE_APP_DJANGO_BASE; + return this.configuration.VUE_APP_DJANGO_BASE; }, API_BASE_URL() { - return this.$store.state.configuration.VUE_APP_DJANGO_API_BASE; + return this.configuration.VUE_APP_DJANGO_API_BASE; + }, + CATALOG_NAME() { + return this.configuration.VUE_APP_CATALOG_NAME; + }, + IDGO() { + return this.$store.state.configuration.VUE_APP_IDGO; }, fileSize() { return fileConvertSizeToMo(this.fileToImport.size); }, +<<<<<<< HEAD +======= + isSharedProject() { + return this.$route.path.includes('projet-partage'); + } +>>>>>>> develop }, watch: { @@ -767,6 +1056,7 @@ export default { } }, }, +<<<<<<< HEAD features: { deep: true, @@ -782,19 +1072,25 @@ export default { this.mapLoading = false; } }, +======= +>>>>>>> develop }, created() { if (this.user) { projectAPI - .getProjectSubscription({ projectSlug: this.$route.params.slug }) - .then((data) => (this.is_suscriber = data.is_suscriber)); + .getProjectSubscription({ + baseUrl: this.$store.state.configuration.VUE_APP_DJANGO_API_BASE, + projectSlug: this.$route.params.slug + }) + .then((data) => (this.is_suscriber = data.is_suscriber)); } this.$store.commit('feature/SET_FEATURES', []); //* empty features remaining in case they were in geojson format and will be fetch after map initialization anyway this.$store.commit('feature_type/SET_FEATURE_TYPES', []); //* empty feature_types remaining from previous project }, mounted() { +<<<<<<< HEAD this.GET_PROJECT_INFO(this.slug) .then(() => { this.projectInfoLoading = false; @@ -804,6 +1100,9 @@ export default { console.error(err); this.projectInfoLoading = false; }); +======= + this.retrieveProjectInfo(); +>>>>>>> develop if (this.message) { this.tempMessage = this.message; @@ -819,17 +1118,48 @@ export default { }, methods: { +<<<<<<< HEAD ...mapMutations(['SET_RELOAD_INTERVAL_ID', 'CLEAR_RELOAD_INTERVAL_ID']), ...mapActions(['GET_PROJECT_INFO']), ...mapActions('map', ['INITIATE_MAP']), ...mapActions('feature_type', ['GET_IMPORTS']), ...mapActions('feature', ['GET_PROJECT_FEATURES']), ...mapActions('feature_type', ['GET_PROJECT_FEATURE_TYPES']), +======= + ...mapMutations([ + 'SET_RELOAD_INTERVAL_ID', + 'CLEAR_RELOAD_INTERVAL_ID', + 'DISPLAY_MESSAGE', + ]), + ...mapActions([ + 'GET_PROJECT_INFO', + 'GET_ALL_PROJECTS', + ]), + ...mapActions('map', [ + 'INITIATE_MAP' + ]), + ...mapActions('feature_type', [ + 'GET_IMPORTS' + ]), + ...mapActions('feature', [ + 'GET_PROJECT_FEATURES' + ]), + ...mapActions('feature_type', [ + 'GET_PROJECT_FEATURE_TYPES' + ]), +>>>>>>> develop refreshId() { return '?ver=' + Math.random(); }, getRouteUrl(url) { +<<<<<<< HEAD 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 +>>>>>>> develop }, isOffline() { return navigator.onLine === false; @@ -844,6 +1174,33 @@ 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(() => { + this.projectInfoLoading = false; + setTimeout(() => { + let map = mapUtil.getMap(); + if (map) map.remove(); + this.initMap(); + }, 1000); + }) + .catch((err) => { + console.error(err) + this.projectInfoLoading = false; + }); + }, + checkForOfflineFeature() { let arraysOffline = []; let localStorageArray = localStorage.getItem('geocontrib_offline'); @@ -970,12 +1327,13 @@ export default { subscribeProject() { projectAPI .subscribeProject({ + baseUrl: this.$store.state.configuration.VUE_APP_DJANGO_API_BASE, suscribe: !this.is_suscriber, projectSlug: this.$route.params.slug, }) .then((data) => { this.is_suscriber = data.is_suscriber; - this.isModalOpen = false; + this.modalType = false; if (this.is_suscriber) this.infoMessage = 'Vous êtes maintenant abonné aux notifications de ce projet.'; @@ -985,6 +1343,54 @@ export default { setTimeout(() => (this.infoMessage = ''), 3000); }); }, + + deleteProject() { + projectAPI.deleteProject(this.API_BASE_URL, this.project.slug) + .then((response) => { + if (response === 'success') { + this.GET_ALL_PROJECTS(); + this.$router.push('/'); + this.DISPLAY_MESSAGE(`Le projet ${this.project.title} a bien été supprimé.`) + } else { + this.DISPLAY_MESSAGE(`Une erreur est survenu lors de la suppression du projet ${this.project.title}.`) + } + }) + }, + + deleteFeatureType() { + featureTypeAPI.deleteFeatureType(this.featureTypeToDelete.slug) + .then((response) => { + this.modalType = false; + if (response === 'success') { + this.GET_ALL_PROJECTS(); + this.retrieveProjectInfo(); + this.DISPLAY_MESSAGE(`Le type de signalement ${this.featureTypeToDelete.title} a bien été supprimé.`) + } else { + this.DISPLAY_MESSAGE(`Une erreur est survenu lors de la suppression du type de signalement ${this.featureTypeToDelete.title}.`) + } + this.featureTypeToDelete = null; + }) + }, + + handleModalClick() { + switch (this.modalType) { + case 'subscribe': + this.subscribeProject(); + break; + case 'deleteProject': + this.deleteProject(); + break; + case 'deleteFeatureType': + this.deleteFeatureType(); + break; + } + }, + + toggleDeleteFeatureType(featureType) { + this.featureTypeToDelete = featureType; + this.modalType = 'deleteFeatureType'; + }, + async initMap() { if (this.project && this.permissions.can_view_project) { await this.INITIATE_MAP(this.$refs.map); @@ -1004,6 +1410,7 @@ export default { true, this.$store.state.feature_type.feature_types ); + this.mapLoading = false; this.GET_PROJECT_FEATURES({ project_slug: this.slug, @@ -1037,17 +1444,18 @@ export default { height: 100%; min-height: 250px; } -.list-image-type { - margin-right: 5px; - height: 25px; - vertical-align: bottom; -} /* // ! missing style in semantic.min.css, je ne comprends pas comment... */ .ui.right.floated.button { float: right; - margin: 0 0 0 1em; } +</style> +<style scoped> +.list-image-type { + margin-right: 5px; + height: 25px; + vertical-align: bottom; + } .feature-type-container { display: flex; justify-content: space-between; @@ -1066,27 +1474,35 @@ export default { } .nouveau-type-signalement { - cursor: pointer; - padding-top: 1em; + margin-top: 1em; } +<<<<<<< HEAD .nouveau-type-signalement > a > .ui > .label { +======= +.nouveau-type-signalement .label{ +>>>>>>> develop cursor: pointer; } #button-import { - padding-top: 0.5em; + margin-top: 0.5em; } .fullwidth { width: 100%; } -.important-flex { - display: flex !important; +.button-align-left { + display: flex; + align-items: center; + text-align: left; + width: fit-content; } -.align-center { - align-items: center !important; +.space-between { + justify-content: space-between; } -.text-left { - text-align: left !important; + +.flex-column-right { + flex-direction: column !important; + align-items: flex-end; } .import-message { @@ -1095,3 +1511,15 @@ export default { color: teal; } </style> + +<style scoped> +.ui.button, .ui.button .button, .tiny-margin { + margin: 0.1rem 0 0.1rem 0.1rem !important; +} +.alert { + color: red; +} +.centered-text { + text-align: center; +} +</style> diff --git a/src/views/project/Project_edit.vue b/src/views/project/Project_edit.vue index a655a012829c2f668aaec815968a6d89777d407c..08c617cf4c58e72f97c7680728a196bc406780b5 100644 --- a/src/views/project/Project_edit.vue +++ b/src/views/project/Project_edit.vue @@ -193,27 +193,48 @@ <div class="field"> <div class="ui checkbox"> <input +<<<<<<< HEAD id="moderation" +======= + class="hidden" + type="checkbox" +>>>>>>> develop v-model="form.moderation" type="checkbox" name="moderation" > <label for="moderation">Modération</label> </div> - <!-- {{ form.moderation.errors }} --> </div> <div class="field"> <div class="ui checkbox"> <input +<<<<<<< HEAD id="is_project_type" +======= + class="hidden" + type="checkbox" +>>>>>>> develop v-model="form.is_project_type" type="checkbox" name="is_project_type" > <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" /> @@ -233,6 +254,7 @@ import axios from '@/axios-client.js'; import Dropdown from '@/components/Dropdown.vue'; +<<<<<<< HEAD import { mapState, mapGetters } from 'vuex'; // axios.defaults.headers.common["X-CSRFToken"] = ((name) => { @@ -240,6 +262,9 @@ import { mapState, mapGetters } from 'vuex'; // var value = re.exec(document.cookie); // return value !== null ? unescape(value[1]) : null; // })("csrftoken"); +======= +import { mapState, mapGetters, mapActions } from "vuex"; +>>>>>>> develop export default { name: 'ProjectEdit', @@ -283,6 +308,7 @@ export default { nb_published_features_comments: 0, nb_contributors: 0, is_project_type: false, + generate_share_link: false, }, thumbnailFileSrc: '', }; @@ -349,6 +375,9 @@ export default { }, methods: { + ...mapActions('projects', [ + 'GET_ALL_PROJECTS' + ]), definePageType() { if (this.$router.history.current.name === 'project_create') { this.action = 'create'; @@ -426,9 +455,15 @@ export default { goBackNrefresh(slug) { Promise.all([ +<<<<<<< HEAD this.$store.dispatch('GET_USER_LEVEL_PROJECTS'), //* refresh projects user levels this.$store.dispatch('GET_USER_LEVEL_PERMISSIONS'), //* refresh projects permissions this.$store.dispatch('GET_ALL_PROJECTS'), //* & refresh project list +======= + this.$store.dispatch("GET_USER_LEVEL_PROJECTS"), //* refresh projects user levels + this.$store.dispatch("GET_USER_LEVEL_PERMISSIONS"), //* refresh projects permissions + this.GET_ALL_PROJECTS(), //* & refresh project list +>>>>>>> develop ]).then(() => // * go back to project list this.$router.push({ @@ -504,6 +539,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 4683a90156c8a93c9dfdb6738a4af8861a20064f..13dcd39c33186c4ab94ffdb971cfbb4d8fdf1f0b 100644 --- a/src/views/project/Project_members.vue +++ b/src/views/project/Project_members.vue @@ -304,12 +304,24 @@ export default { ) .then((response) => { if (response.status === 200) { +<<<<<<< HEAD this.$store.dispatch('GET_USER_LEVEL_PROJECTS'); //* update user status in top right menu this.$store.commit('DISPLAY_MESSAGE', 'Permissions mises à jour'); } else { this.$store.commit( 'DISPLAY_MESSAGE', "Une erreur s'est produite pendant la mises à jour des permissions" +======= + this.$store.dispatch("GET_USER_LEVEL_PROJECTS"); //* update user status in top right menu + this.$store.commit("DISPLAY_MESSAGE", {comment: "Permissions mises à jour", level: "positive"}); + } else { + this.$store.commit( + "DISPLAY_MESSAGE", + { + comment : "Une erreur s'est produite pendant la mises à jour des permissions", + level: "negative" + } +>>>>>>> develop ); } }) diff --git a/src/views/registration/Login.vue b/src/views/registration/Login.vue index 2e5d99c9f8c7477feb667c2b0b99e00edf8ed992..9852de0a4225565fd0fc504a5395b0663c907264 100644 --- a/src/views/registration/Login.vue +++ b/src/views/registration/Login.vue @@ -125,5 +125,18 @@ export default { .catch(); }, }, +<<<<<<< HEAD +======= + + mounted() { + if (this.$store.state.user) { + this.$store.commit( + "DISPLAY_MESSAGE", + {comment :"Vous êtes déjà connecté, vous allez être redirigé vers la page d'accueil."} + ); + setTimeout(() => this.$router.push("/"), 3100); + } + }, +>>>>>>> develop }; </script> \ No newline at end of file