diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b5cd343d96bc1009e1d2203399c337a20b907548..0eebfc0f9b8fa4a114826e30b8c3877168bd8a2d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -44,7 +44,6 @@ build tagged docker image: stage: build only: - tags - when: manual tags: - build image: diff --git a/package.json b/package.json index 0f0843731dba2192fdb958a3b2d3b750015d43fd..3e59989c8975a3211f294b94343c58eee650a539 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geocontrib-frontend", - "version": "2.1.1", + "version": "2.1.2", "private": true, "scripts": { "serve": "npm run init-proxy & npm run init-serve", diff --git a/src/components/feature/FeatureListTable.vue b/src/components/feature/FeatureListTable.vue index c7ebb5c23e78db573e2df94005f12a000d1f2a3b..c55b807a95f87ffed6ba8eaca90757c21a92625b 100644 --- a/src/components/feature/FeatureListTable.vue +++ b/src/components/feature/FeatureListTable.vue @@ -132,7 +132,7 @@ {{ feature.properties.updated_on }} </td> <td class="center" v-if="user"> - {{ feature.properties.creator.username || " ---- " }} + {{ getUserName(feature) }} </td> </tr> <tr v-if="filteredFeatures.length === 0" class="odd"> @@ -296,6 +296,12 @@ export default { }, methods: { + getUserName(feature){ + if(!feature.properties.creator) { + return " ---- "; + } + return feature.properties.creator.username || " ---- " + }, getFeatureDisplayName(feature) { return feature.properties.title || feature.id; }, diff --git a/src/components/feature_type/FeatureTypeCustomForm.vue b/src/components/feature_type/FeatureTypeCustomForm.vue index 30b0a1e8014f0d788bc73e8d066b63de73315e2f..08b77649d2cd65edd15cbf2ce4d6c09e5c107ead 100644 --- a/src/components/feature_type/FeatureTypeCustomForm.vue +++ b/src/components/feature_type/FeatureTypeCustomForm.vue @@ -303,6 +303,12 @@ export default { checkCustomForm() { this.form.label.errors = []; this.form.name.errors = []; + this.form.options.errors = []; + console.log( + this.form.field_type.value, + this.form.field_type.value === "list", + this.form.options.value.length < 2 + ); if (!this.form.label.value) { //* vérifier que le label est renseigné this.form.label.errors = ["Veuillez compléter ce champ."]; @@ -323,6 +329,13 @@ export default { "Les champs personnalisés ne peuvent pas avoir des noms similaires.", ]; return false; + } else if ( + this.form.field_type.value === "list" && + this.form.options.value.length < 2 + ) { + //* s'il s'agit d'un type liste, vérifier que le champ option est bien renseigné + this.form.options.errors = ["Veuillez compléter ce champ."]; + return false; } return true; }, diff --git a/src/main.js b/src/main.js index 0a69972e540c108429f5a9c6939c30e66010786a..4a1f3128cd2ba8ca6a755be82f949b99d29851ad 100644 --- a/src/main.js +++ b/src/main.js @@ -10,26 +10,49 @@ import 'leaflet-draw/dist/leaflet.draw.css'; import '@/assets/resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.css'; Vue.config.productionTip = false +// gestion mise à jour du serviceWorker et du precache +var refreshing=false; +navigator.serviceWorker.addEventListener('controllerchange', () => { + // We'll also need to add 'refreshing' to our data originally set to false. + if (refreshing) return + refreshing = true + // Here the actual reload of the page occurs + window.location.reload() +}) + + +let onConfigLoaded = function(config){ + store.commit("SET_CONFIG", config); + window.proxy_url=config.VUE_APP_DJANGO_API_BASE+"proxy/"; + axios.all([store.dispatch("USER_INFO"), + store.dispatch("GET_ALL_PROJECTS"), + store.dispatch("GET_STATIC_PAGES"), + store.dispatch("GET_USER_LEVEL_PROJECTS"), + store.dispatch("map/GET_AVAILABLE_LAYERS"), + store.dispatch("GET_USER_LEVEL_PERMISSIONS"), + ]).then(axios.spread(function () { + new Vue({ + router, + store, + render: h => h(App) + }).$mount('#app') + })); + +} axios.get("./config/config.json") + .catch((error)=>{ + console.log(error); + console.log("try to get From Localstorage"); + let conf=localStorage.getItem("geontrib_conf"); + if(conf){ + onConfigLoaded(JSON.parse(conf)) + } + }) .then((response) => { if (response && response.status === 200) { - store.commit("SET_CONFIG", response.data); - window.proxy_url = response.data.VUE_APP_DJANGO_API_BASE + "proxy/" - axios.all([ - store.dispatch("USER_INFO"), - store.dispatch("GET_ALL_PROJECTS"), - store.dispatch("GET_STATIC_PAGES"), - store.dispatch("GET_USER_LEVEL_PROJECTS"), - store.dispatch("map/GET_AVAILABLE_LAYERS"), - store.dispatch("GET_USER_LEVEL_PERMISSIONS"), - ]).then(axios.spread(function () { - new Vue({ - router, - store, - render: h => h(App) - }).$mount('#app') - })) + localStorage.setItem("geontrib_conf",JSON.stringify(response.data)); + onConfigLoaded(response.data) } }) .catch((error) => { diff --git a/src/registerServiceWorker.js b/src/registerServiceWorker.js index 76cede074d8a8393586f6567de3020e2e506591d..0fea28955951af66f5cce9aa2df7e57716b5b329 100644 --- a/src/registerServiceWorker.js +++ b/src/registerServiceWorker.js @@ -10,8 +10,14 @@ if (process.env.NODE_ENV === 'production') { 'For more details, visit https://goo.gl/AFskqB' ) }, - registered () { - console.log('Service worker has been registered.') + registered (registration) { + //console.log('Service worker has been registered.') + console.log( + 'Service worker has been registered and now polling for updates.' + ) + setInterval(() => { + registration.update() + }, 10000) // every 10 seconds }, cached () { console.log('Content has been cached for offline use.') @@ -19,8 +25,14 @@ if (process.env.NODE_ENV === 'production') { updatefound () { console.log('New content is downloading.') }, - updated () { - console.log('New content is available; please refresh.') + updated (registration) { + alert('Une nouvelle version de l\'application est disponible, l\'application va se recharger'); + console.log('New content is available; please refresh.'); + // + if (!registration || !registration.waiting) return + // Send message to SW to skip the waiting and activate the new SW + registration.waiting.postMessage({ type: 'SKIP_WAITING' }) + //window.location.reload(true); }, offline () { console.log('No internet connection found. App is running in offline mode.') diff --git a/src/router/index.js b/src/router/index.js index 035bafb0d27e113d77fab3d5fc207db3050e7356..f0eb56d71e8a8442ffd6d914291a1114e2eb3281 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -108,6 +108,11 @@ const routes = [ name: 'details-signalement', component: () => import('../views/feature/Feature_detail.vue') }, + { + path: '/projet/: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/', name: 'editer-signalement', diff --git a/src/service-worker.js b/src/service-worker.js new file mode 100644 index 0000000000000000000000000000000000000000..7497fb0ca7f6f2d73661d696f8fa189c69640695 --- /dev/null +++ b/src/service-worker.js @@ -0,0 +1,64 @@ +// custom service-worker.js +if (workbox) { + // adjust log level for displaying workbox logs + //workbox.core.setLogLevel(workbox.core.LOG_LEVELS.debug) + + // apply precaching. In the built version, the precacheManifest will + // be imported using importScripts (as is workbox itself) and we can + // precache this. This is all we need for precaching + workbox.precaching.precacheAndRoute(self.__precacheManifest); + + //workbox.core.skipWaiting(); + + // Make sure to return a specific response for all navigation requests. + // Since we have a SPA here, this should be index.html always. + // https://stackoverflow.com/questions/49963982/vue-router-history-mode-with-pwa-in-offline-mode + workbox.routing.registerNavigationRoute('/geocontrib/index.html') + + workbox.routing.registerRoute( + new RegExp('.*/config/config.json'), + new workbox.strategies.StaleWhileRevalidate({ + cacheName: 'config', + }) + ) + + workbox.routing.registerRoute( + new RegExp('.*/api/.*'), + new workbox.strategies.NetworkFirst({ + cacheName: 'api', + }) + ) + workbox.routing.registerRoute( + /^https:\/\/c\.tile\.openstreetmap\.fr/, + new workbox.strategies.CacheFirst({ + cacheName: 'osm', + plugins: [ + new workbox.cacheableResponse.Plugin({ + statuses: [0, 200], + }), + new workbox.expiration.Plugin({ + maxAgeSeconds: 60 * 60 * 24 * 365, + // maxEntries: 30, pour limiter le nombre d'entrée dans le cache + }), + ], + }) + ) + +} + +// This code listens for the user's confirmation to update the app. +self.addEventListener('message', (e) => { + if (!e.data) { + return; + } + //console.log(e.data); + switch (e.data.type) { + case 'SKIP_WAITING': + self.skipWaiting(); + break; + default: + // NOOP + break; + } +}) + diff --git a/src/store/modules/feature.js b/src/store/modules/feature.js index 9b629c72ca3f301918d20e238778fe29739d02e9..72b383a7be2634c741475ed4aa65250095467371 100644 --- a/src/store/modules/feature.js +++ b/src/store/modules/feature.js @@ -179,6 +179,33 @@ const feature = { }) .catch((error) => { commit("DISCARD_LOADER", null, { root: true }) + if(error.message=="Network Error" ||window.navigator.onLine==false){ + let arraysOffline=[]; + let localStorageArray=localStorage.getItem("geocontrib_offline"); + if(localStorageArray){ + arraysOffline=JSON.parse(localStorageArray); + } + let updateMsg={ + project:rootState.project_slug, + type:'put', + featureId:state.form.feature_id, + geojson:geojson + }; + arraysOffline.push(updateMsg); + localStorage.setItem("geocontrib_offline",JSON.stringify(arraysOffline)); + router.push({ + name: "offline-signalement", + params: { + slug_type_signal: rootState.feature_type.current_feature_type_slug + }, + }); + + } + else{ + console.log(error) + throw error; + } + throw error; }); } else { @@ -195,7 +222,32 @@ const feature = { }) .catch((error) => { commit("DISCARD_LOADER", null, { root: true }) - throw error; + if(error.message=="Network Error" ||window.navigator.onLine==false){ + let arraysOffline=[]; + let localStorageArray=localStorage.getItem("geocontrib_offline"); + if(localStorageArray){ + arraysOffline=JSON.parse(localStorageArray); + } + let updateMsg={ + project:rootState.project_slug, + type:'post', + geojson:geojson + }; + arraysOffline.push(updateMsg); + localStorage.setItem("geocontrib_offline",JSON.stringify(arraysOffline)); + router.push({ + name: "offline-signalement", + params: { + slug_type_signal: rootState.feature_type.current_feature_type_slug + }, + }); + + } + else{ + console.log(error) + throw error; + } + }); } }, diff --git a/src/views/Index.vue b/src/views/Index.vue index ca7e4fc7cbf83ef91638e4fdfed935cc7cf5c875..e199657757733fb9d0874e0c72e2a47fd9f04d8b 100644 --- a/src/views/Index.vue +++ b/src/views/Index.vue @@ -15,14 +15,14 @@ <h4 id="les_projets" class="ui horizontal divider header">PROJETS</h4> <div class="flex"> <router-link - v-if="user && user.can_create_project" + v-if="user && user.can_create_project && isOffline()!=true" :to="{ name: 'project_create', params: { action: 'create' } }" class="ui green basic button" > <i class="plus icon"></i> Créer un nouveau projet </router-link> <router-link - v-if="user && user.can_create_project" + v-if="user && user.can_create_project && isOffline()!=true" :to="{ name: 'project_type_list', }" @@ -124,6 +124,9 @@ export default { }, methods: { + isOffline(){ + return navigator.onLine==false; + }, refreshId() { //* change path of thumbnail to update image return "?ver=" + Math.random(); diff --git a/src/views/feature/Feature_edit.vue b/src/views/feature/Feature_edit.vue index 49c6e111cf4ca440b819e290c92e863b9515470b..1dc8b37cb83ade39e4b14299b837b630763b0552 100644 --- a/src/views/feature/Feature_edit.vue +++ b/src/views/feature/Feature_edit.vue @@ -4,7 +4,7 @@ type="application/javascript" :src=" baseUrl + - '/resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.js' + 'resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.js' " ></script> <div class="fourteen wide column"> @@ -72,7 +72,7 @@ <!-- Import GeoImage --> <div v-frag v-if="feature_type && feature_type.geom_type === 'point'"> - <p> + <p v-if="isOffline()!=true"> <button @click="toggleGeoRefModal" id="add-geo-image" @@ -207,45 +207,48 @@ </div> <!-- Pièces jointes --> - <div class="ui horizontal divider">PIÈCES JOINTES</div> - <div id="formsets-attachment"> - <FeatureAttachmentForm - v-for="form in attachmentFormset" - :key="form.dataKey" - :attachmentForm="form" - ref="attachementForm" - /> - </div> + <div v-if="isOffline()!=true"> + <div class="ui horizontal divider">PIÈCES JOINTES</div> + <div v-if="isOffline()!=true" id="formsets-attachment"> + <FeatureAttachmentForm + v-for="form in attachmentFormset" + :key="form.dataKey" + :attachmentForm="form" + ref="attachementForm" + /> + </div> - <button - @click="add_attachement_formset" - id="add-attachment" - type="button" - class="ui compact basic button button-hover-green" - > - <i class="ui plus icon"></i>Ajouter une pièce jointe - </button> + <button + @click="add_attachement_formset" + id="add-attachment" + type="button" + class="ui compact basic button button-hover-green" + > + <i class="ui plus icon"></i>Ajouter une pièce jointe + </button> + </div> <!-- Signalements liés --> - <div class="ui horizontal divider">SIGNALEMENTS LIÉS</div> - <div id="formsets-link"> - <FeatureLinkedForm - v-for="form in linkedFormset" - :key="form.dataKey" - :linkedForm="form" - :features="features" - ref="linkedForm" - /> + <div v-if="isOffline()!=true"> + <div class="ui horizontal divider">SIGNALEMENTS LIÉS</div> + <div id="formsets-link"> + <FeatureLinkedForm + v-for="form in linkedFormset" + :key="form.dataKey" + :linkedForm="form" + :features="features" + ref="linkedForm" + /> + </div> + <button + @click="add_linked_formset" + id="add-link" + type="button" + class="ui compact basic button button-hover-green" + > + <i class="ui plus icon"></i>Ajouter une liaison + </button> </div> - <button - @click="add_linked_formset" - id="add-link" - type="button" - class="ui compact basic button button-hover-green" - > - <i class="ui plus icon"></i>Ajouter une liaison - </button> - <div class="ui divider"></div> <button @click="postForm" type="button" class="ui teal icon button"> @@ -440,6 +443,9 @@ export default { }, methods: { + isOffline(){ + return navigator.onLine==false; + }, initForm() { if (this.currentRouteName === "editer-signalement") { for (let key in this.feature) { diff --git a/src/views/feature/Feature_list.vue b/src/views/feature/Feature_list.vue index e3906d9f6020911433cbd6fcf232f1cd49506ce5..667e974de35c926248f704fa1a6c94f85bf44362 100644 --- a/src/views/feature/Feature_list.vue +++ b/src/views/feature/Feature_list.vue @@ -4,7 +4,7 @@ type="application/javascript" :src=" baseUrl + - '/resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.js' + 'resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.js' " ></script> <div id="feature-list-container" class="ui grid mobile-column"> diff --git a/src/views/feature/Feature_offline.vue b/src/views/feature/Feature_offline.vue new file mode 100644 index 0000000000000000000000000000000000000000..fb8a89bd8e14f663924e90e294a8a43dc60956dd --- /dev/null +++ b/src/views/feature/Feature_offline.vue @@ -0,0 +1,41 @@ +<template> + <div v-frag> + Erreur Réseau lors de l'envoi du signalement. Votre signalement devra être envoyé au serveur quand vous aurez de nouveau accès à internet. + Veuillez à ce moment là cliquer sur Envoyer sur la page principale du projet + <router-link + :to="{ + name: 'project_detail', + params: { slug: $route.params.slug }, + }" + class="header" + >Retour au projet</router-link + > + </div> +</template> + +<script> +import frag from "vue-frag"; + +export default { + name: "Feature_offline", + + directives: { + frag, + }, + + data() { + return { + }; + }, + + computed: { + }, + methods: { + + } +}; +</script> + +<style> + +</style> \ No newline at end of file diff --git a/src/views/project/Project_detail.vue b/src/views/project/Project_detail.vue index 80c6ff8705d1719ca1709e3e8656b731ebf86620..d4a8636f340ea9e0a3189a21f33be644403afac6 100644 --- a/src/views/project/Project_detail.vue +++ b/src/views/project/Project_detail.vue @@ -52,9 +52,19 @@ <h1 class="ui header"> <div class="content"> {{ project.title }} + <div v-if="arraysOffline.length>0">{{arraysOffline.length}} modifications en attente + <button + :disabled="isOffline()" + @click="sendOfflineFeatures()" + class="ui fluid teal icon button" + > + <i class="upload icon"></i> Envoyer au serveur + </button> + + </div> <div class="ui icon right floated compact buttons"> <a - v-if="user && permissions && permissions.can_view_project" + 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" @@ -65,7 +75,7 @@ <i class="inverted grey envelope icon"></i> </a> <router-link - v-if="permissions && permissions.can_update_project" + 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" @@ -156,7 +166,7 @@ v-if=" project && permissions && - permissions.can_create_feature_type + permissions.can_create_feature_type && isOffline()!=true " class=" ui @@ -182,7 +192,7 @@ project && type.is_editable && permissions && - permissions.can_create_feature_type + permissions.can_create_feature_type && isOffline()!=true " class=" ui @@ -208,7 +218,7 @@ <div class="nouveau-type-signalement"> <router-link - v-if="permissions && permissions.can_update_project" + v-if="permissions && permissions.can_update_project && isOffline()!=true" :to="{ name: 'ajouter-type-signalement', params: { slug: project.slug }, @@ -220,7 +230,7 @@ </div> <div class="nouveau-type-signalement"> <a - v-if="permissions && permissions.can_update_project" + v-if="permissions && permissions.can_update_project && isOffline()!=true" class=" ui compact @@ -493,6 +503,7 @@ export default { data() { return { infoMessage: "", + arraysOffline: [], geojsonImport: [], fileToImport: { name: "", size: 0 }, slug: this.$route.params.slug, @@ -520,7 +531,68 @@ export default { refreshId() { return "?ver=" + Math.random(); }, + isOffline(){ + return navigator.onLine==false; + }, + checkForOfflineFeature(){ + let arraysOffline=[]; + let localStorageArray=localStorage.getItem("geocontrib_offline"); + if(localStorageArray){ + arraysOffline=JSON.parse(localStorageArray); + this.arraysOffline=arraysOffline.filter(x=>x.project==this.project.slug); + } + }, + sendOfflineFeatures(){ + var promises = []; + this.arraysOffline.forEach((feature, index, object)=>{ + console.log(feature); + if(feature.type=='post') { + promises.push( + axios + .post(`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}features/`, feature.geojson) + .then((response) => { + console.log(response) + if (response.status === 201 && response.data) { + object.splice(index, 1); + } + }) + .catch((error) => { + console.log(error); + })); + } + else if(feature.type=='put') { + promises.push( + axios + .put(`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}features/${feature.featureId}`, feature.geojson) + .then((response) => { + console.log(response) + if (response.status === 200 && response.data) { + object.splice(index, 1); + } + }) + .catch((error) => { + console.log(error); + })); + } + }); + Promise.all(promises).then(() => { + this.updateLocalStorage(); + window.location.reload(); + } + ); + + }, + updateLocalStorage(){ + let arraysOffline=[]; + let localStorageArray=localStorage.getItem("geocontrib_offline"); + if(localStorageArray){ + arraysOffline=JSON.parse(localStorageArray); + } + let arraysOfflineOtherProject = arraysOffline.filter(x=>x.project!=this.project.slug); + arraysOffline=arraysOfflineOtherProject.concat(this.arraysOffline); + localStorage.setItem("geocontrib_offline",JSON.stringify(arraysOffline)); + }, toNewFeatureType() { this.$router.push({ name: "ajouter-type-signalement", @@ -585,10 +657,14 @@ export default { if (this.project && this.permissions.can_view_project) { this.$store.dispatch("map/INITIATE_MAP"); const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/?output=geojson`; + let self = this; + this.checkForOfflineFeature(); axios .get(url) .then((response) => { - const features = response.data.features; + let features = response.data.features; + self.arraysOffline.forEach(x=>x.geojson.properties.color="red"); + features=response.data.features.concat(self.arraysOffline.map(x=>x.geojson)); const featureGroup = mapUtil.addFeatures(features); if (featureGroup && featureGroup.getLayers().length > 0) { mapUtil @@ -602,6 +678,8 @@ export default { .catch((error) => { throw error; }); + + } if (this.message) { diff --git a/vue.config.js b/vue.config.js index 8e550623b56b7de8f5580c7b8d6bbe6144f5310d..777fbbbaf0383828440b5391b142cb44add79982 100644 --- a/vue.config.js +++ b/vue.config.js @@ -3,15 +3,36 @@ const fs = require('fs') const packageJson = fs.readFileSync('./package.json') const version = JSON.parse(packageJson).version || 0 module.exports = { - publicPath: '/geocontrib/', - configureWebpack: { - plugins: [ - new webpack.DefinePlugin({ - 'process.env': { - PACKAGE_VERSION: '"' + version + '"' + publicPath: '/geocontrib/', + devServer: { + proxy: { + '^/api': { + target: 'https://geocontrib.dev.neogeo.fr/api', + ws: true, + changeOrigin: true + } } - }) - ] - }, - // the rest of your original module.exports code goes here + }, + pwa: { + workboxPluginMode: 'InjectManifest', + workboxOptions: { + swSrc: 'src/service-worker.js', + exclude: [ + /\.map$/, + /config\/config.*\.json$/, + /manifest\.json$/ + ], + }, + themeColor: '#1da025' + }, + configureWebpack: { + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + PACKAGE_VERSION: '"' + version + '"' + } + }) + ] + }, +// the rest of your original module.exports code goes here }