diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f7c8f34b08ba2e23c64b5801f6885b3c1768ac72..bd01e22edd47c77eb35b09eff7b41198f1463721 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,5 @@ stages: + - test-build - Static analysis - build - deploy @@ -8,6 +9,17 @@ variables: SONAR_HOST_URL: "https://sonarqube.neogeo.fr" GIT_DEPTH: 0 +test build: + stage: test-build + image: node:14 + script: + - npm install + - npm run build + except: + - master + - develop + - ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ + build testing docker image: stage: build only: diff --git a/src/assets/js/utils.js b/src/assets/js/utils.js index 4b01c67ef07d0bf803528976e6708657894fa821..0680fcdfad82586d6325e4cc501bf9873abf4d7c 100644 --- a/src/assets/js/utils.js +++ b/src/assets/js/utils.js @@ -24,14 +24,18 @@ export function csvToJson(csv, delimiter) { const [, ...lines] = allLines; for (const line of lines) { - const obj = {}; - const currentLine = line.split(delimiter); + if (line) { + const obj = {}; + const currentLine = line.split(delimiter).map(el => { + return el.replace('\r', ''); + }); - for (let i = 0; i < headers.length; i++) { - obj[headers[i]] = currentLine[i]; - } + for (let i = 0; i < headers.length; i++) { + obj[headers[i]] = currentLine[i]; + } - result.push(obj); + result.push(obj); + } } return JSON.parse(JSON.stringify(result)); } diff --git a/src/assets/styles/base.css b/src/assets/styles/base.css index 5383b368b37b82b882868fa28a18e8d0180401ba..00f8d4f9a991c0cfd25f9bdda21fd88d49e6c5fb 100644 --- a/src/assets/styles/base.css +++ b/src/assets/styles/base.css @@ -81,6 +81,9 @@ body { .tiny-margin { margin: 0.1rem 0 0.1rem 0.1rem !important; } +.tiny-margin-left { + margin-left: 0.1rem !important; +} .ellipsis { text-overflow: ellipsis; overflow: hidden; @@ -163,6 +166,12 @@ body { color: #9f3a38; } +#form-layers .infoslist{ + list-style: none; + padding-left: 0; + color: #38989f; +} + /* Fix semantic ui overflow when is too long */ .layer-item .form div.text { width: 100% @@ -225,13 +234,26 @@ body { padding: 0; } +.infoslist { + margin-top: 0.1rem; + padding: 0; +} + .errorlist > li { list-style: none; color: rgb(177, 55, 55); border: thin solid rgb(197, 157, 157); border-radius: 3px; background-color: rgb(250, 241, 242); - padding: 1rem; + padding: 0.5rem 1rem; +} + +.infoslist > li { + list-style: none; + color: #38989f; + border-radius: 3px; + padding: 0; + text-align: right; } /* ---------------------------------- */ diff --git a/src/components/Feature/Detail/FeatureHeader.vue b/src/components/Feature/Detail/FeatureHeader.vue index d0850cb749761cd9462f53e69a904c8a43046ea4..eb75b473eb99c429536067ffe80928bdea61cbd2 100644 --- a/src/components/Feature/Detail/FeatureHeader.vue +++ b/src/components/Feature/Detail/FeatureHeader.vue @@ -45,7 +45,7 @@ v-if="((permissions && permissions.can_update_feature) || isFeatureCreator) && isOnline" id="currentFeature-delete" class="ui button button-hover-red" - @click="isCanceling = true" + @click="$emit('setIsCancelling')" > <i class="inverted grey trash alternate icon" diff --git a/src/components/Map/EditingToolbar.vue b/src/components/Map/EditingToolbar.vue index 3465c628b02185d7b575e2712c827a20863c2766..1f4d19aa178dca42f6435eba8659e12464bce79b 100644 --- a/src/components/Map/EditingToolbar.vue +++ b/src/components/Map/EditingToolbar.vue @@ -4,7 +4,11 @@ <div class="leaflet-bar"> <a class="leaflet-draw-draw-polygon active" - title="Dessiner un polygone" + :title=" + editionService.geom_type === 'polygon' ? 'Dessiner un polygone' : + editionService.geom_type === 'linestring' ? 'Dessiner une ligne' : + 'Dessiner un point' + " > <img v-if="editionService.geom_type === 'linestring'" diff --git a/src/components/Project/Detail/ProjectHeader.vue b/src/components/Project/Detail/ProjectHeader.vue index c56cb750c0414bee19ffb6b72641c8d9a73d1862..35d1871095f4b433b15415a0a40d81292cd37f68 100644 --- a/src/components/Project/Detail/ProjectHeader.vue +++ b/src/components/Project/Detail/ProjectHeader.vue @@ -255,11 +255,12 @@ export default { this.arraysOfflineErrors.push(feature); }) ); - this.DISPLAY_LOADER('Envoi des signalements en cours.'); + this.$store.commit('DISPLAY_LOADER', 'Envoi des signalements en cours.'); Promise.all(promises).then(() => { - this.$emit('update-local-storage'); - this.$emit('retrieve-info'); + this.$emit('updateLocalStorage'); + this.$emit('retrieveInfo'); + this.$store.commit('DISCARD_LOADER'); }); }, @@ -272,24 +273,20 @@ export default { .project-header { - .row { - margin-top: 3em; + .row .right-column { + display: flex; + flex-direction: column; - .right-column { - display: flex; - flex-direction: column; - - .ui.buttons { - justify-content: flex-end; - a.ui.button { - flex-grow: 0; /* avoid stretching buttons */ - } + .ui.buttons { + justify-content: flex-end; + a.ui.button { + flex-grow: 0; /* avoid stretching buttons */ } } - .centered { - margin: auto; - text-align: center; - } + } + .centered { + margin: auto; + text-align: center; } } diff --git a/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue b/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue index 4e624f10378ccc3b988577ab543db618af3d6ee8..7416a8f72d95420e4d3ab9fc506a6daf38f37ad6 100644 --- a/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue +++ b/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue @@ -85,7 +85,7 @@ </div> <div v-if="checkedFeatures.length > 0 && massMode === 'modify'" - class="ui dropdown button compact button-hover-green margin-left-25" + class="ui dropdown button compact button-hover-green tiny-margin-left" data-tooltip="Modifier le statut des Signalements" data-position="bottom right" @click="toggleModifyStatus" @@ -107,7 +107,7 @@ v-for="status in availableStatus" :key="status.value" class="item" - @click="modifyStatus(status.value)" + @click="$emit('modify-status', status.value)" > {{ status.name }} </span> @@ -116,10 +116,10 @@ </div> <div v-if="checkedFeatures.length > 0 && massMode === 'delete'" - class="ui button compact button-hover-red margin-left-25" + class="ui button compact button-hover-red tiny-margin-left" data-tooltip="Supprimer tous les signalements sélectionnés" data-position="bottom right" - @click="toggleDeleteModal" + @click="$emit('toggle-delete-modal')" > <i class="grey trash fitted icon" @@ -320,6 +320,14 @@ export default { }, }, + mounted() { + window.addEventListener('mousedown', this.clickOutsideDropdown); + }, + + destroyed() { + window.removeEventListener('mousedown', this.clickOutsideDropdown); + }, + methods: { resetPaginationNfetchFeatures() { this.$emit('reset-pagination'); @@ -330,6 +338,20 @@ export default { this.showAddFeature = !this.showAddFeature; this.showModifyStatus = false; }, + + toggleModifyStatus() { + this.showModifyStatus = !this.showModifyStatus; + this.showAddFeature = false; + }, + + 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); + } + }, } }; diff --git a/src/registerServiceWorker.js b/src/registerServiceWorker.js index d9165a280ec265551fe559549fbfdab19a934813..e2cd38783734eb6519d7705e7c8f1379eb914803 100644 --- a/src/registerServiceWorker.js +++ b/src/registerServiceWorker.js @@ -26,15 +26,19 @@ if (process.env.NODE_ENV === 'production') { console.log('New content is downloading.'); }, 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; + if (!navigator.webdriver) { + 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); + } else { + console.log('Execution dans un navigateur controlé par un agent automatisé, la mise à jour n\'est pas appliqué pendant le test.'); } - // 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/services/map-service.js b/src/services/map-service.js index 7a9afd5e6c7d5643bb181dec4c62aa067b78472b..e88fceab2a2c826dca7ab2a0baa32258168f09bc 100644 --- a/src/services/map-service.js +++ b/src/services/map-service.js @@ -116,7 +116,7 @@ const mapService = { overlay.setPosition(undefined); closer.blur(); return false; - }; + }; } this.map.addOverlay(this.overlay); @@ -244,8 +244,11 @@ const mapService = { addLayers: function (layers, serviceMap, optionsMap, schemaType) { this.layers = layers; if (layers) { //* if admin has defined basemaps for this project + let count = 0; layers.forEach((layer) => { + if (layer) { + count +=1; const options = layer.options; if (options) { options.noWrap = true; @@ -268,6 +271,7 @@ const mapService = { dictLayersToLeaflet[layer.id] = layerTms; } } + dictLayersToLeaflet[layer.id].setZIndex(count); } }); } else { //* else when no basemaps defined @@ -358,6 +362,7 @@ const mapService = { }); this.mvtLayer.featureTypes = featureTypes; this.mvtLayer.project_slug = projectSlug; + this.mvtLayer.setZIndex(30); this.map.addLayer(this.mvtLayer); window.layerMVT = this.mvtLayer; }, @@ -462,7 +467,7 @@ const mapService = { source: drawSource, style: styleFunction, }); - + olLayer.setZIndex(29); this.map.addLayer(olLayer); return drawSource; }, diff --git a/src/store/modules/map.store.js b/src/store/modules/map.store.js index 1a87098abcbd86423c00101f56f2b19fcc7061c1..f4a65434bb4e026166436892f73d24b23752f5be 100644 --- a/src/store/modules/map.store.js +++ b/src/store/modules/map.store.js @@ -121,25 +121,29 @@ const map = { }, async SAVE_BASEMAPS({ state, rootState, dispatch }, newBasemapIds) { - const DJANGO_API_BASE = this.state.configuration.VUE_APP_DJANGO_API_BASE; - function postOrPut(basemap) { - basemap['project'] = rootState.projects.project.slug; - if (newBasemapIds.includes(basemap.id)) { - return axios - .post(`${DJANGO_API_BASE}base-maps/`, basemap) - .then((response) => response) - .catch((error) => { - console.error(error); - return error; - }); - } else { - return axios - .put(`${DJANGO_API_BASE}base-maps/${basemap.id}/`, basemap) - .then((response) => response) - .catch((error) => { - console.error(error); - return error; - }); + //* send new basemaps synchronously to create their ids in the order they were created in the form + let promisesResult = []; + function postOrPut(basemapsToSend) { + if (basemapsToSend.length > 0) { + let basemap = basemapsToSend.shift(); //* remove and return first item in array + basemap['project'] = rootState.projects.project.slug; + let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}base-maps/`; + if (!newBasemapIds.includes(basemap.id)) url += `${basemap.id}/`; + promisesResult.push( + axios({ + url, + method: newBasemapIds.includes(basemap.id) ? 'POST' : 'PUT', + data: basemap, + }) + .then((response) => { + postOrPut(basemapsToSend); + return response; + }) + .catch((error) => { + postOrPut(basemapsToSend); + return error; + }) + ); } } @@ -149,14 +153,11 @@ const map = { .then((response) => response); } - const promisesResult = await Promise.all( - [ - ...state.basemaps.map((basemap) => postOrPut(basemap)), - ...state.basemapsToDelete.map((basemapId) => deleteBMap(basemapId)) - ] - ); + postOrPut([...state.basemaps]); + //* delete basemaps + const deletedResult = await Promise.all(state.basemapsToDelete.map((basemapId) => deleteBMap(basemapId))); state.basemapsToDelete = []; - return promisesResult; + return [...promisesResult, ...deletedResult]; }, DELETE_BASEMAP({ commit }, basemapId) { diff --git a/src/views/Feature/FeatureDetail.vue b/src/views/Feature/FeatureDetail.vue index f6e76a9e232606c23e033a21574bb9e7f92875fc..946b4629a960d30fa743f8ddb91a28f4139bdde2 100644 --- a/src/views/Feature/FeatureDetail.vue +++ b/src/views/Feature/FeatureDetail.vue @@ -9,7 +9,9 @@ > <div class="row"> <div class="sixteen wide column"> - <FeatureHeader /> + <FeatureHeader + @setIsCancelling="isCanceling = true" + /> </div> </div> <div class="row"> @@ -52,7 +54,7 @@ </div> <div v-if="isCanceling" - class="ui dimmer modals page transition visible active" + class="ui dimmer modals transition visible active" style="display: flex !important" > <div @@ -234,9 +236,9 @@ export default { deleteFeature() { this.$store .dispatch('feature/DELETE_FEATURE', { feature_id: this.currentFeature.feature_id }) - .then((response) => { + .then(async (response) => { if (response.status === 204) { - this.GET_PROJECT_FEATURES({ + await this.GET_PROJECT_FEATURES({ project_slug: this.$route.params.slug }); this.goBackToProject(); diff --git a/src/views/Feature/FeatureEdit.vue b/src/views/Feature/FeatureEdit.vue index f024fc524b7464e29341c37fc4cfe41c064e3855..567fd72ba19ce0b15e7335b5f033acb9142d9fbc 100644 --- a/src/views/Feature/FeatureEdit.vue +++ b/src/views/Feature/FeatureEdit.vue @@ -32,6 +32,17 @@ :name="form.title.html_name" @blur="updateStore" > + <ul + id="infoslist-title" + class="infoslist" + > + <li + v-for="info in form.title.infos" + :key="info" + > + {{ info }} + </li> + </ul> <ul id="errorlist-title" class="errorlist" @@ -379,9 +390,10 @@ export default { form: { title: { errors: [], + infos: [], id_for_label: 'name', field: { - max_length: 30, + max_length: 128, }, html_name: 'name', label: 'Nom', @@ -491,6 +503,16 @@ export default { }, }, + watch: { + 'form.title.value': function(newValue) { + if (newValue.length === 128) { + this.form.title.infos.push('Le nombre de caractères et limité à 128.'); + } else { + this.form.title.infos = []; + } + } + }, + created() { this.$store.commit( 'feature-type/SET_CURRENT_FEATURE_TYPE_SLUG', diff --git a/src/views/FeatureType/FeatureTypeDetail.vue b/src/views/FeatureType/FeatureTypeDetail.vue index 6ff4e62769af70ea10197250a31e59d0db177112..58303368fb891af77f861fecb6985e2ed83d9ac8 100644 --- a/src/views/FeatureType/FeatureTypeDetail.vue +++ b/src/views/FeatureType/FeatureTypeDetail.vue @@ -611,12 +611,11 @@ export default { const headersLine = csv .split('\n')[0] + .replace(/(\r\n|\n|\r)/gm, '') .split(delimiter) .filter(el => { return el === 'lat' || el === 'lon'; }); - // Look for 2 decimal fields in first line of csv - // corresponding to lon and lat if (headersLine.length !== 2) { this.importError = 'Le fichier ne semble pas contenir de champs de coordonnées.'; return false; @@ -664,7 +663,8 @@ export default { //* if field type is list, it's not possible to guess from value type if (field_type === 'list') { //*then check if the value is an available option - if (!options.includes(fieldInFeature)) { + if (fieldInFeature && !options.includes(fieldInFeature)) { + this.importError = `Le champ ${name} contient une valeur invalide.`; return false; } } else if (customType !== field_type) { @@ -694,7 +694,7 @@ export default { reader.addEventListener('load', (e) => { // bypass json check for files larger then 10 Mo let jsonValidity; - if (parseFloat(fileConvertSizeToMo(files[0])) <= 10) { + if (parseFloat(fileConvertSizeToMo(files[0].size)) <= 10) { jsonValidity = this.checkJsonValidity(JSON.parse(e.target.result)); } else { jsonValidity = true; diff --git a/src/views/FeatureType/FeatureTypeEdit.vue b/src/views/FeatureType/FeatureTypeEdit.vue index 67d6def4ac8f5dab0ad86109b2c411d25e4f0afa..303f3d2f59107f7d8e031dae3e24aa8308d13d28 100644 --- a/src/views/FeatureType/FeatureTypeEdit.vue +++ b/src/views/FeatureType/FeatureTypeEdit.vue @@ -244,6 +244,7 @@ export default { slug: this.$route.params.slug, reservedKeywords: [ // todo : add keywords for mapstyle (strokewidth...) + 'id', 'title', 'description', 'status', diff --git a/src/views/Project/FeaturesListAndMap.vue b/src/views/Project/FeaturesListAndMap.vue index 6b6af5454d03509df24be6f07478676b40d5138d..ec2d2bd5012c056ab30e1f0f0b4dc52f719368e7 100644 --- a/src/views/Project/FeaturesListAndMap.vue +++ b/src/views/Project/FeaturesListAndMap.vue @@ -12,6 +12,8 @@ @reset-pagination="resetPagination" @fetch-features="fetchPagedFeatures" @show-map="setShowMap" + @modify-status="modifyStatus" + @toggle-delete-modal="toggleDeleteModal" /> <div @@ -143,8 +145,6 @@ export default { pagination: { ...initialPagination }, projectSlug: this.$route.params.slug, showMap: true, - showAddFeature: false, - showModifyStatus: false, sort: { column: '', ascending: true, @@ -223,12 +223,9 @@ export default { } else { this.initMap(); } - - 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'); }, @@ -260,24 +257,10 @@ export default { } }, - toggleModifyStatus() { - this.showModifyStatus = !this.showModifyStatus; - this.showAddFeature = false; - }, - toggleDeleteModal() { this.isDeleteModalOpen = !this.isDeleteModalOpen; }, - 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) { if (this.checkedFeatures.length > 0) { const feature_id = this.checkedFeatures[0]; diff --git a/src/views/Project/ProjectDetail.vue b/src/views/Project/ProjectDetail.vue index 452a9067c61469a8eeefc0884de1f0d0fd6eb5ea..58ff359a5d337ebc6dc58fcbfdb815f183517a25 100644 --- a/src/views/Project/ProjectDetail.vue +++ b/src/views/Project/ProjectDetail.vue @@ -437,7 +437,7 @@ export default { await this.INITIATE_MAP(this.$refs.map); this.checkForOfflineFeature(); let project_id = this.$route.params.slug.split('-')[0]; - const mvtUrl = `${this.API_BASE_URL}features.mvt/`; + const mvtUrl = `${this.API_BASE_URL}features.mvt`; mapService.addVectorTileLayer( mvtUrl, project_id,